第八章 POCO之获取原始对象与手工同步对象图和变化跟踪器

8-6 获取原始对象

问题

你正在使用POCO,想从数据库获取原始对象。

解决方案

假设你有如图8-7所示的模型。你正在离线环境下工作,你想应用在获取客户端修改之前,使用Where从句和FirstDefault()方法从数据库中获取原始对象。

图8-7.包含一个单独实体Item的模型

按代码清单8-9的方式,在获取实体之后,使用新值更新实体并将其保存到数据库中。

代码清单8-9. 获取最新添加的实体并使用Entry()方法替换它的值

class Program
{
    static void Main(string[] args)
    {
        RunExample();
    }

    static void RunExample()
    {
        int itemId = 0;
        using (var context = new EFRecipesEntities())
        {
            var item = new Item
            {
                Name = "Xcel Camping Tent",
                UnitPrice = 99.95M
            };
            context.Items.Add(item);
            context.SaveChanges();

            //为下一步保存itemId
            itemId = item.ItemId;
            Console.WriteLine("Item: {0}, UnitPrice: {1}",
                     item.Name, item.UnitPrice.ToString("C"));
        }

        using (var context = new EFRecipesEntities())
        {
            //假设这是一个更新,我们获取的item使用一个新的价格
            var item = new Item
            {
                ItemId = itemId,
                Name = "Xcel Camping Tent",
                UnitPrice = 129.95M
            };
            var originalItem = context.Items.Where(x => x.ItemId ==  itemId).FirstOrDefault<Item>();
            context.Entry(originalItem).CurrentValues.SetValues(item);
            context.SaveChanges();
        }
        using (var context = new EFRecipesEntities())
        {
            var item = context.Items.Single();
            Console.WriteLine("Item: {0}, UnitPrice: {1}", item.Name,
                               item.UnitPrice.ToString("C"));
        }
        Console.WriteLine("Enter input as exit to exit.:");
        string line = Console.ReadLine();
        if (line == "exit")
        {
            return;
        };
    }
}

public partial class Item
{
    public int ItemId { get; set; }
    public string Name { get; set; }
    public decimal UnitPrice { get; set; }
}
 public partial class EFRecipesEntities : DbContext
{
    public EFRecipesEntities()
        : base("name=EFRecipesEntities")
    {
    }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        throw new UnintentionalCodeFirstException();
    }
    public DbSet<Item> Items { get; set; }
}


代码清单的输出如下:

Item: Xcel Camping Tent, UnitPrice: $99.95
Item: Xcel Camping Tent, UnitPrice: $129.95


原理

在代码清单8-9中,我们插入一个item到模型中并保存到数据库中。然后我们假装接收到了一个更新的item,也许是来至一个Silverlight客户端。

接下来,我们要更新这个item到数据库中。为了实现这个操作,我们从数据库中获取这个对象到上下文中。在这个过程中,我们使用了where从句和FirstOrDefault方法。然后,我们使用方法Entry(),它能访问整个实体,并且,能在实体上应用很多方法。我们应用了CureentValues.SetVaules方法,用从客户端接受到的新值来替换原始值。最后,我们调用SaveChages保存实体。

8-7 手工同步对象图和变化跟踪器

问题

你想手工同步你的POCO类和更化跟踪器。

更化跟踪器会访问实体框架正在存储的,且正在被跟踪的实体的信息。这些信息不只是你存储在实体属性中的值,还包含实体的当前状态、从数据库获取的原始值、哪些属性被修改了以及别的数据。变化跟踪器提供额外操作的访问,这些操作能在实体上执行,例如,重新从数据库中加载值 ,以确保你有最新数据。

实体框架有两种不的方法来跟踪你的对象:基于快照的变化跟踪和变化跟踪代理。

基于快照的变化跟踪

POCO类不包含任何的,当属性值发生更改时,通知实体框架的逻辑。因为,当一个属性的值发生变化时,没有任何可以通知的方式。当实体框架第一次遇到一个对象时,它会为每一个属性的值在内存中生成一个快照。当一个对象从查询中返回,或者我们添加一个对象到DbSet中时,快照就被生成了。当实体框架需要知道发生了哪些变化时,它会浏览每一个对象并用他们的当前值和快照比较。这个处理过程是被一个在变化跟踪器中名为DetectChanges的方法触发的。

变化跟踪代理

变化跟踪的另一项技术是变化跟踪代理,它能使实体框架具有接收变更通知的能力。 变化跟踪代理是使用实现延迟加载时动态创建代理的机制来实现的,这个动态创建的代理,不只提供延迟加载的功能,当对象发生改变时,它还能通知上下文对象。为了使用变化跟踪代理,你创建的类必须满足,实体框架能在运行时创建一个派生至你的POCO类的动态类型,在这个类型中,它重载了每个属性。这个动态类型叫做动态代理,在重载属性中包含当属性值发生改变时,通知实体框架的逻辑。

基于快照的变化跟踪,依赖实体框架在变化发生时能执行检测。DbContext API的默认行为是,通过DbContext上的事件来自动执行检测。DetectChanges不仅仅是更新上下文的状态管理信息,这些状态管理信息能让更改持久化到数据库中,当你有一个引用类型导航属性,集合类型导航属性和外键的组合时,它还能执行关系修正。清晰认识变化什么时候被检测,这些变化要做什么,以及如何控制它们是非常重要的。

实体框架需要知道变更最明显的时间是在执行SaveChanges期间。还有很多地方也需要知道变更的情况。比如,询问更化跟踪器实体对象的当前状态时,它需要扫描并检测任何发生的变更。扫描和检没并不只发生在我们讨论的问题中,当你执行DbContext中的很多API都能引起DetectChanges方法的运行。在绝大多数情况下,DetectChanges方法足够快,不会引起性能问题。但是,如果在内存中有大量的对象或者在DbContext中接二连三地执行操作,自变更检测行为就会成为性能关注点。幸运的是,有选项可以把这个自动变更检测关闭掉,并在需要时手工调用它。但稍有不慎,可能会导致意想不到的后果。实体框架精心地为你提供了关闭自动更变检测的功能。如果在性能差的地方使用它时,你将负担起调用DetectChages方法的责任。一旦这个部分的代码执行完毕,就得通过DbContext.Configuration.AutoDetectChangesEnable标识把它启用。

解决方案

假设你有如图8-8所示的模型。它描述了演示者和为各种会议准备的演讲。

图8-8. 演讲者和他准备的演讲之间多对多关系模型

首先需要注意的是,在我们的模型中,Speaker和 Talk之间是多对多关联。我们通过独立关联来实现(在数据库中使用SpeakerTalk链接表)这个模型,让它支持一个演讲者对应多场演讲,一场演讲对应多位演讲者。

我们想手工同步对象图和变化跟踪器。通过调用DetectChanges()方法来实现。同时,将演示同步是如何进行的。

按代码清单8-10的方法,手工同步你的POCO实体对象图和变化跟踪器。

代码清单8-10. 当需要手工同步变化跟踪器时,显式使用DetectChages()方法

class Program
{
    static void Main(string[] args)
    {
        RunExample();
    }

    static void RunExample()
    {
        using (var context = new EFRecipesEntities())
        {
            context.Configuration.AutoDetectChangesEnabled = false;
            var speaker1 = new Speaker { Name = "Karen Stanfield" };
            var talk1 = new Talk { Title = "Simulated Annealing in C#" };
            speaker1.Talks = new List<Talk> { talk1 };

            //关联尚未完成
            Console.WriteLine("talk1.Speaker is null: {0}",
                                talk1.Speakers == null);

            context.Speakers.Add(speaker1);

            //现在关联已经修正
            Console.WriteLine("talk1.Speaker is null: {0}",
                                talk1.Speakers == null);
            Console.WriteLine("Number of added entries tracked: {0}",
                                context.ChangeTracker.Entries().Where(e => e.State == System.Data.Entity.EntityState.Added).Count());
            context.SaveChanges();
            //修改talk的标题
            talk1.Title = "AI with C# in 3 Easy Steps";
            Console.WriteLine("talk1's state is: {0}",
                                context.Entry(talk1).State);
            context.ChangeTracker.DetectChanges();
            Console.WriteLine("talk1's state is: {0}",
                                context.Entry(talk1).State);
            context.SaveChanges();
        }

        using (var context = new EFRecipesEntities())
        {
            foreach (var speaker in context.Speakers.Include("Talks"))
            {
                Console.WriteLine("Speaker: {0}", speaker.Name);
                foreach (var talk in speaker.Talks)
                {
                    Console.WriteLine("\tTalk Title: {0}", talk.Title);
                }
            }
        }
    }
}
public partial class Speaker
{
    public int SpeakerId { get; set; }
    public string Name { get; set; }
    public ICollection<Talk> Talks { get; set; }
}
public partial class Talk
{
    public int TalkId { get; set; }
    public string Title { get; set; }
    public System.DateTime CreateDate { get; set; }
    public System.DateTime RevisedDate { get; set; }
    public ICollection<Speaker> Speakers { get; set; }
}
public partial class EFRecipesEntities : DbContext
{
    public EFRecipesEntities()
        : base("name=EFRecipesEntities")
    {
    }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        throw new UnintentionalCodeFirstException();
    }

    public DbSet<Speaker> Speakers { get; set; }
    public DbSet<Talk> Talks { get; set; }

    public override int SaveChanges()
    {
        var changeSet = this.ChangeTracker.Entries().Where(e => e.Entity is Talk);
        if (changeSet != null)
        {
            foreach (var entry in changeSet.Where(c => c.State == System.Data.Entity.EntityState.Added).Select(a => a.Entity as Talk))
            {
                entry.CreateDate = DateTime.UtcNow;
                entry.RevisedDate = DateTime.UtcNow;
            }
            foreach (var entry in changeSet.Where(c => c.State == System.Data.Entity.EntityState.Modified).Select(a => a.Entity as Talk))
            {
                entry.RevisedDate = DateTime.UtcNow;
            }
        }
        return base.SaveChanges();
    }
}

代码清单8-10的输出如下:

talk1.Speaker is null: True
talk1.Speaker is null: False
Number of added entries tracked: 2
talk1's state is: Unchanged
talk1's state is: Modified
Speaker: Karen Stanfield
    Talk Title: AI with C# in 3 Easy Steps


原理

代码清单8-10中的代码有点复杂,让我们一步一步的来讲解。首先,关闭自动跟踪,创建speaker和talk实例,然后把talk添加到speaker的导航属性集合Talks中。此时,talk已经是speaker的导航属性集合Talks的一部分,但是speaker还不是talk的导航属性集合Speakers的一部分。关联中的另一边还没有被修正。

接下来,我们使用Add方法,将speaker1添加到上下文中。从输出的第二行可以看出,现在,talk的导航属性集合Speakers已经正确。实体框架已经修正了关联中的另一边。在这里实体框架做了两件事,第一件事是它通知对象状态管理器,有三个对象被创建,虽然最终输出结果不是三个,这是因为它把多对多关联看作是一个独立关系,而不是一个单独的实体。因此,输出结果为2。这些对象分别是speaker和talk。没有多对多关联对应的对象。这是因为变化跟踪器没有返回独立关系的状态。第二件事是实体框架修正了talk中的导航属性Speakers。

当我们调用SaveChages()方法时,实体框架会使用重载版本的Savechanges。在这个方法中,我们更新属性 CreateDate和RevisedDate。在调用SaveChanges()方法之前,实体框架会调用DetectChanges()来查找发生的变更。在代码清单8-10中,我们重写了SaveChages()方法。

DetectChanges()方法依赖一个快照来比较每一个实体的每一个属性的原始值和当前值。这个过程能判断出对象图中那些变化发生了。对于一个大的对象图,这个比较过程可能会比较耗时。

《Entity Framework 6 Recipes》中文翻译系列