你必须知道的EntityFramework 6.x和EntityFramework Core变更追踪状态
前言
只要有时间就会时不时去看最新EF Core的进展情况,同时也会去看下基础,把握好基础至关重要,本节我们对比看看如标题EF 6.x和EF Core的不同,希望对正在学习EF Core的同行能有所帮助,同时也希望通过本文能对您心中可能产生的疑惑进行解答,本文略长,请耐心阅读。
深入探讨EF 6.x和EF Core变更追踪状态话题
请注意虽然EF 6.x和EF Core在使用方式上没有什么不同,但是内置实现却有所不同,了解它们的不同很重要,同时也希望更多同行选择EF Core,EF 6.x令人诟病毋庸置疑,在使用上不了解基本知识就会出很大的问题,这一点我已经明确阐述过,EF Core为我们做了许多,只是我们并未知道而已,看完本文相信您会认同我说的这句话,为了便于大家理解我们用实例来说明。
EntityState状态设置和使用对应方法是否匹配?
无论是在EntityFramework 6.x还是EntityFramework Core中DbSet上始终包含有Add、Attath、Remove等方法,当我们调用Add方法来添加对象时,此时内部则是将对象状态置为添加状态(Add),如果我们调用Remove方法删除对象,内部则是将对象置为删除状态(Deleted),在EF 6.x中没有Update方法,若是更新所有列则是只需将对象状态修改为Modified,无论怎样都是通过EntityState来根据我们的操作来设置对象相应的状态,下面我们一起来看下例子。
using (var ctx = new EfDbContext()) { var customer = new Customer() { Id = 1, Name = "Jeffcky" }; ctx.Entry(customer).State = EntityState.Modified; ctx.Customers.Add(customer); var state = ctx.Entry(customer).State; var result = ctx.SaveChanges(); };
如上示例是在EF 6.x中,我们首先实例化一个customer,然后将其状态修改为Modified,最后我们调用Add方法添加到上下文中,此时我们得到customer的状态会是怎样的呢?
没毛病,对不对,最终调用Add方法其状态将覆盖我们手动通过Entry设置的Modified状态,接下来我们将如上通过Entry方法修改为如下:
ctx.Entry(customer).State = EntityState.Unchanged;
如果我们这样做了,结果当然也是好使的,那要是我们继续修改为如下形式呢?
using (var ctx = new EfDbContext()) { var customer = new Customer() { Id = 1, Name = "Jeffcky" }; ctx.Entry(customer).State = EntityState.Deleted; ctx.Customers.Add(customer); var state = ctx.Entry(customer).State; var result = ctx.SaveChanges(); };
结果还是Added状态,依然好使,我们继续进行如下修改。
using (var ctx = new EfDbContext()) { var customer = new Customer() { Id = 1, Name = "Jeffcky" }; ctx.Entry(customer).State = EntityState.Added; ctx.Customers.Attach(customer); var state = ctx.Entry(customer).State; var result = ctx.SaveChanges(); };
恩,还是没问题,这里我们可以得出我们通过Entry方法手动设置未被跟踪对象的状态后,最后状态会被最终调用的方法所覆盖,一切都是明朗,还没完全结束。那接下来我们添加导航属性看看呢?
using (var ctx = new EfDbContext()) { var customer = new Customer() { Id = 1, Name = "Jeffcky" }; var order = new Order() { Id = 1, CustomerId = 1 }; customer.Orders.Add(order); ctx.Entry(order).State = EntityState.Modified; ctx.Customers.Attach(customer); var state = ctx.Entry(order).State; var result = ctx.SaveChanges(); };
反观上述代码,我们实例化customer和order对象,并将order添加到customer导航属性中,接下来我们将order状态修改为Modified,最后调用Attath方法附加customer,根据我们上述对单个对象的结论,此时order状态理论上应该是Unchanged,但是真的是这样?
和我们所期望的截然相反,此时通过调用attach方法并未将我们手动通过Entry方法设置状态为Modified覆盖,换言之此时造成了对象状态不一致问题,这是EF 6.x的问题,接下来我们再来看一种情况,你会发现此时会抛出异常,抛出的异常我也看不懂,也不知道它想表达啥意思(在EF Core中不会出现这样的情况,我就不占用一一篇幅说明,您可自行实践)。
using (var ctx = new EfDbContext()) { var customer = new Customer() { Id = 1, Name = "Jeffcky" }; var order = new Order() { Id = 1 }; customer.Orders.Add(order); ctx.Entry(order).State = EntityState.Deleted; ctx.Customers.Attach(customer); var state = ctx.Entry(order).State; var result = ctx.SaveChanges(); };
由上我们得出什么结论呢?在EF 6.x中使用Entry设置对象状态和调用方法对相关的对象影响将出现不一致的情况,接下来我们来对比EF 6.x和EF Core在使用上的区别,对此我们会有深刻的理解,如果我们还沿袭EF 6.x那一套,你会发现居然不好使,首先我们来看EF 6.x例子。
using (var ctx = new EfDbContext()) { var customer = new Customer() { Name = "Jeffcky", Email = "2752154844@qq.com", Orders = new List<Order>() { new Order() { Code = "order", CreatedTime = DateTime.Now, ModifiedTime = DateTime.Now, Price = 100, Quantity = 10 } } }; ctx.Customers.Add(customer); var result = ctx.SaveChanges(); };
这里需要说明的是我将customer和order是配置了一对多的关系,从如上例子也可看出,我们调用SaveChanges方法毫无疑问会将customer和order插入到数据库表中,如下:
接下来我们手动通过Entry方法设置customer状态为Added,再来看看,如下:
using (var ctx = new EfDbContext()) { var customer = new Customer() { Name = "Jeffcky", Email = "2752154844@qq.com", Orders = new List<Order>() { new Order() { Code = "order", CreatedTime = DateTime.Now, ModifiedTime = DateTime.Now, Price = 100, Quantity = 10 } } }; ctx.Entry(customer).State = EntityState.Added; var result = ctx.SaveChanges(); };
对照如上我们再来看看在EF Core中是如何处理的呢?直接调用Add方法就不浪费时间演示了,用过EF Core的都知道必然好使,我们看看手动设置状态。
public static void Main(string[] args) { using (var context = new EFCoreDbContext()) { var blog = GetBlog(); var post = new Post() { CommentCount = 10, CreatedTime = DateTime.Now, ModifiedTime = DateTime.Now, Name = "Jeffcky" }; context.Entry(blog).State = EntityState.Added; var result = context.SaveChanges(); } Console.ReadKey(); } static Blog GetBlog() { return new Blog() { IsDeleted = false, CreatedTime = DateTime.Now, ModifiedTime = DateTime.Now, Name = "Jeffcky", Status = 0, Url = "http://www.blogs/com/createmyself" }; }
通过实践证明此时不会将Post添加到表中,为什么会如此呢?因为我们只是手动设置了blog的状态为Added,而未对Post进行设置,看到这里想必您知道了EF 6.x和EF Core的不同。EF团队之所以这么做的目的在于如EF 6.x一样手动设置根对象的状态其导航属性即相应关联的对象也会设置,这样做会造成混乱,当我们添加对象时其导航属性也会对应添加,虽然看起来很自然,也适应一些情况,但是对象模型并不清楚主体和依赖关系,所以在EF Core中则发生了改变,通过Entry方法只会对传入对象的状态有所影响而对关联的对象不会发生任何改变,这点尤其重要,我们在使用EF Core时要格外注意,额外多说一句在EF Core通过Entry().State这个APi设置状态只会对单个对象产生影响不会对关联对象产生任何影响即忽略关联对象。
EntityFramework Core为什么在上下文中添加对应方法?
不知道使用过EF Core的您有没有发现,在EF 6.x中我们发现在上下文中并没有如暴露的DbSet上的方法比如Add、AddRange、Remove、RemoveRange等等,但是在EF Core则存在对应的方法,不知道您发现过没有,我虽然发现,但是一直不明白为何如此这样做,这样做的目的在哪里呢?我还特意看了EF Core实现源码,结果发现其内部好像还是调用了暴露在DbSet上的方法,如果我没记错的话,这样不是多此一举,吃饱了撑着了吗,有这个时间实现这样一个玩意,那怎么不早早实现通过Incude进行过滤数据呢?EF Core团队在github上讨论当前这不是优先级比较高的特性,其实不然,很多时候我们需要通过导航属性来筛选数据,少了这一步,我们只能加载到内存中再进行过滤。好了回到话题,我也是偶然看到一篇文章,才发现这样设计的目的何在,接下来我们首先来看看在EF 6.x中的上下文中没有对应的方法结果造成的影响是怎样的呢?通过实例我们一看便知。
using (var ctx = new EfDbContext()) { var order = ctx.Orders.FirstOrDefault(); var newOrder = new Order() { CustomerId = order.CustomerId, CreatedTime = DateTime.Now, ModifiedTime = DateTime.Now, Code = "addOrder", Price = 200, Quantity = 1000 }; ctx.Orders.Add(newOrder); var result = ctx.SaveChanges(); };
特意给出如上表中数据来进行对比,如上代码我们查询出第一个Order即上图标注,然后我们重新实例化一个Order进行添加,此时您能想象到会发生什么吗?瞧瞧吧。
结果是添加到表中了,但是但是但是,重要的事情说三遍,仔细看看数据和我们要添加的Order数据对照看看,万万没想到,此时得到的数据是主键等于1的数据也就是旧数据。让我们再次回到EF Core中演示上述例子。
using (var context = new EFCoreDbContext()) { var post = context.Posts.FirstOrDefault(); var newPost = new Post() { CreatedTime = Convert.ToDateTime("2018-06-01"), ModifiedTime = Convert.ToDateTime("2018-06-01"), Name = "《你必须掌握的Entity Framework 6.x与Core 2.0》书籍出版", CommentCount = 0, BlogId = post.BlogId }; context.Add(newPost); var result = context.SaveChanges(); }
如上代码重新实例化一个Blog并添加到表中数据和如上图中数据完全不一样,我们通过上下文中暴露的Add方法来添加Blog,我们来看看最终在表中的数据是怎样的呢?
在EF Core上下文中有了Add,Attach、Remove方法以及Update和四个相关的Range方法(AddRange等等)和暴露在DbSet上的方法一样。 同时在上下文中的方法更加聪明了。 它们现在可以确定类型并自动将实体对象关联到我们想要的的DbSet。不能说很方便,而是非常方便,因为它允许我们编写通用代码而完全不需要再实例化DbSet,当然我们也可以这样做,只不过现在又多了一条康庄大道罢了,代码简易且易于发现。
即使是如下动态对象,EF Core也能正确关联到对应的对象,您亲自实践便知。
using (var context = new EFCoreDbContext()) { dynamic newBlog = new Blog() { IsDeleted = true, CreatedTime = Convert.ToDateTime("2018-06-01"), ModifiedTime = Convert.ToDateTime("2018-06-01"), Name = "《你必须掌握的Entity Framework 6.x与Core 2.0》书籍出版", Status = 0, Url = "http://www.cnblogs.com/CreateMyself/p/8655069.html" }; context.Add(newBlog); var result = context.SaveChanges(); }
让我们再来看看一种情况来对比EF 6.x和EF Core在使用方式上的不同,首先我们来看看EF 6.x例子:
using (var ctx = new EfDbContext()) { var customer = ctx.Customers.Include(d => d.Orders).FirstOrDefault(); var newOrder = new Order() { CreatedTime = DateTime.Now, ModifiedTime = DateTime.Now, Code = "addOrder", Price = 200, Quantity = 1000, CustomerId = customer.Id }; ctx.Orders.Attach(newOrder); var result = ctx.SaveChanges(); };
此时我们能够看到我们只是通过Attatch方法附加了newOrder,然后进行通过SaveChanges进行提交,此时并未提交到数据库表中,那EF Core处理机制是不是也一样呢?我们来看看:
using (var context = new EFCoreDbContext()) { var blog = context.Blogs.FirstOrDefault(); var newPost = new Post() { CreatedTime = Convert.ToDateTime("2018-06-01"), ModifiedTime = Convert.ToDateTime("2018-06-01"), Name = "《你必须掌握的Entity Framework 6.x与Core 2.0》书籍出版", CommentCount = 0, BlogId = blog.Id }; context.Attach(newPost); var result = context.SaveChanges(); }
很惊讶是不是,在EF Core最终添加到数据库表中了,依照我们对EF 6.x的理解,通过Attach方法只是将实体对象状态修改为Unchanged,如果我们想添加对象那么必须调用Add方法,此时对象状态将变为Added状态,也就是说在EF 6.x中如果我们调用Attatch方法,但是需要将对象添加到数据库表中,此时必须要调用Add方法,反之如果我们调用Add方法,那么调用Attath方法附加对象则多此一举,但是在EF Core中这种情况通过上述演示很显然发生了改变。那么EF Core内部是根据什么来判断的呢?我们来看如下源代码:
通过上述源代码不难看出在EF Core对于未设置主键都将视为添加换句话说则是如果调用Attach方法附加一个未被跟踪的对象时且主键值未被填充时,EF Core将其视为添加,所以如果我们需要添加对象时此时可直接调用Attach而无需调用Add方法。如果您依然不信,您可自行进行如下测试,也同样会被添加到表中。
public static void Main(string[] args) { using (var context = new EFCoreDbContext()) { var blog = GetBlog(); context.Attach(blog); var result = context.SaveChanges(); } Console.ReadKey(); } static Blog GetBlog() { return new Blog() { IsDeleted = false, CreatedTime = DateTime.Now, ModifiedTime = DateTime.Now, Name = "Jeffcky", Status = 0, Url = "http://www.blogs/com/createmyself" }; }
EF Core团队这么做的目的是什么呢?大部分情况下通过调用Attach方法将可抵达图中所有未跟踪实体的状态设置为Unchanged,除非实体对象的主键我们没有设置且正在使用值生成,对于更新/修改同理。很显然 这对许多断开连接的实体的常见情况非常有用。但是,在许多情况下它不起作用,因为在特殊情况下是根实体,即使我们未设置主键也强制它的状态保持不变,这样做显然不合理。如果我们通过未设置主键调用Attach并最终添加它,这被认为是意外行为,我们需要放开对根实体的特殊封装,通过调用Attach方法来改变这种行为,这会使得Attach变得更加适用,它并不是突破性的改变。
Jeff自问自答模式来了,那么我们是否允许我们多次调用Attach来附加实体对象呢?您觉得是否可行呢?我们来验证下:
using (var context = new EFCoreDbContext()) { var blog = GetBlog(); context.Attach(blog); context.Attach(blog); var result = context.SaveChanges(); }
EntityFramework Core为什么添加无连接跟踪图(Disconnected TrackGraph)?
追踪图是EF中全新的概念,它提供了我们对对象状态的完全控制,TrackGraph遍历图(即遍历图中的每个对象)并将指定的函数应用于每个对象。 该函数是TrackGraph方法的第二个参数。此特性的出现也是为了调用对应方法和手动设置状态而造成的混乱而给。比如我们想实现如EF 6.x一样,当调用Attach方法时不添加实体,那么我们可以如下这样做。
using (var context = new EFCoreDbContext()) { var blog = GetBlog(); context.ChangeTracker.TrackGraph(blog, node => { if (!node.Entry.IsKeySet) { node.Entry.State = EntityState.Unchanged; } }); context.Attach(blog); var result = context.SaveChanges(); }
总结
本文我们详细讲解了EF 6.x和EF Core在实体状态上使用方式的不同且讲解了我们需要注意到二者的不同,接下来我们会继续回顾基础,感谢您的阅读,我们下节再会。
修正
关于本文用EF 6.x添加数据出现旧数据问题是我写的代码有问题,特此致歉,不知道是在什么场景会产生旧数据的问题,之前确实看过一篇文章,但是示例忘记了。