你必须知道的EntityFramework 6.x和EntityFramework Core变动追踪状态

前言

只要有时间就会时不时去看最新EF Core的进展状况,同时也会去看下基础,把握好基础相当重要,本节咱们对比看看如标题EF 6.x和EF Core的不一样,但愿对正在学习EF Core的同行能有所帮助,同时也但愿经过本文能对您心中可能产生的疑惑进行解答,本文略长,请耐心阅读。html

深刻探讨EF 6.x和EF Core变动追踪状态话题

请注意虽然EF 6.x和EF Core在使用方式上没有什么不一样,可是内置实现却有所不一样,了解它们的不一样很重要,同时也但愿更多同行选择EF Core,EF 6.x使人诟病毋庸置疑,在使用上不了解基本知识就会出很大的问题,这一点我已经明确阐述过,EF Core为咱们作了许多,只是咱们并未知道而已,看完本文相信您会认同我说的这句话,为了便于你们理解咱们用实例来讲明。node

EntityState状态设置和使用对应方法是否匹配?

不管是在EntityFramework 6.x仍是EntityFramework Core中DbSet上始终包含有Add、Attath、Remove等方法,当咱们调用Add方法来添加对象时,此时内部则是将对象状态置为添加状态(Add),若是咱们调用Remove方法删除对象,内部则是将对象置为删除状态(Deleted),在EF 6.x中没有Update方法,如果更新全部列则是只需将对象状态修改成Modified,不管怎样都是经过EntityState来根据咱们的操做来设置对象相应的状态,下面咱们一块儿来看下例子。git

            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的状态会是怎样的呢?github

没毛病,对不对,最终调用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状态,依然好使,咱们继续进行以下修改。post

            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中不会出现这样的状况,我就不占用一一篇幅说明,您可自行实践)。spa

            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添加数据出现旧数据问题是我写的代码有问题,特此致歉,不知道是在什么场景会产生旧数据的问题,以前确实看过一篇文章,可是示例忘记了。

相关文章
相关标签/搜索