你必須知道的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添加數據出現舊數據問題是我寫的代碼有問題,特此致歉,不知道是在什麼場景會產生舊數據的問題,以前確實看過一篇文章,可是示例忘記了。

相關文章
相關標籤/搜索