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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM