1. 程式人生 > >討論過後而引發對EF 6.x和EF Core查詢緩存的思考

討論過後而引發對EF 6.x和EF Core查詢緩存的思考

entity order by 跟著 framework 二次 不一定 write 當前 希望

前言

最近將RabbitMQ正式封裝引入到.NET Core 2.0項目當中,之前從未接觸過這個高大上的東東跟著老大學習中,其中收獲不少,本打算再看看RabbitMQ有時間寫寫,回來後和何鎮汐大哥探討了一點關於EF和EF Core的內容,於是乎本文就出來了。EF 6.x和EF Core中的查詢緩存想必大家都有耳聞或者了解,從數據庫中查詢出來的實體會形成快照在內存中Copy一份且被上下文跟蹤,接下來我們要講的內容就是這個,我們來看看。

EF 6.x和EF Core查詢緩存思考

首先我利用EF Core通過一個例子來進入今天的主題,在此過程中您也可多一點思考空間並對照和您想象中的結果是否有出入或者不一致的地方。

技術分享圖片
            var context = new EFCoreDbContext();
            var blog1 = context.Blogs.Find(3);
            Console.WriteLine($"數據庫原始值為{blog1.Name}");
            blog1.Name = "Jeffcky";   
            var blog2 = context.Blogs.FirstOrDefault(d => d.Id == 3);
            Console.WriteLine($"查詢數據庫後的值為{blog2.Name}"
技術分享圖片

上述我們通過Find方法查詢出Blog1,然後對Name進行賦值,緊接著我們再次查詢主鍵等於3的實體,您猜想一下blog2中的Name會等於多少呢?

技術分享圖片

我們看看如上圖所示結果為Jeffcky,有的童鞋就問了,我們進行第一次查詢時值為3,然後我們僅僅只是賦值為Jeffcky並未提交,當我們再次查詢相同實體時結果卻為Jeffcky呢?難道不應該是3麽,對吧。稍微對EF或者EF Core有所了解的大佬們明白第一次查詢時會有快照,當下次查詢時EF或者EF Core會在內存中根據第一次查詢所對應的哈希值和主鍵去查找,此時查找到則直接利用內存中的對象,所以此時blog2的Name為Jeffcky。問題是不是到此就結束了呢?如果是這樣,那我大半夜還浪費這時間寫這篇博客!接下來我們再來看看兩次查詢所生成的SQL如何?

技術分享圖片

技術分享圖片

在EF 6.x和EF Core中 通過Find方法基於主鍵查詢查詢可重用,什麽意思呢?如上第一個查詢采用參數化查詢,也就說如果我們下次再利用Find方法查詢那麽將不會到數據庫查詢,而是直接從內存中返回,有的童鞋就想了,恩,挺好,這樣顯著提高了查詢性能,我只能說不一定看對應場景,如果對於非常頻繁的查詢我個人覺得不建議用此方法,因為還有對實體的更新操作啊,此時數據更新了卻在內存中的值沒有更新顯示到UI上也就是說是過期了值,那麽您覺得這個時候用Find方法還可取嗎。對於第二個查詢則直接采取賦值的形式(在我即將出版的《你必須掌握的EntityFramework 6.x和Core 2.0》書中有講解EF 6.x查詢的很大問題)這都不是事,問題是第二次我們利用FirstOrDefault方法查詢此時居然走數據庫了,不信,您可以看看如下利用SQL Profiler監控得到的SQL語句。

技術分享圖片

這就讓人有點費解了,第二個查詢既然是到數據庫中查詢那為何我們得到的值當前第一次查詢出來修改但未被提交的值呢?這不是自相矛盾麽,EF Core這樣設計的意義何在,我也想不通,我能想到的是在同一上下文中大部分情況下不會對同一實體查詢多次,但是誰能保證呢。 在EF Core中除了Find會進行翻譯緩存,其他比如First、FirstOrDefault、Last、LastOrDefault都會到數據庫中查詢,當我們利用Last或者LastOrDefault查詢時會出現更有意思的事情,我們看看生成的SQL語句:

var blog3 = context.Blogs.Last();

技術分享圖片

EF Core大哥哥們這麽簡單的查詢問題都沒測試到麽,你翻譯給我讓我誤以為這是返回所有列表了,結果一看查詢出來的值卻是對的,我想這是EF Core大哥哥們忘記加上TOP 1和ORDER BY了,到github上一搜索原來有大神提過這個ISSUE(https://github.com/aspnet/EntityFrameworkCore/issues/10493)在2.1版本發布會解決這個問題。好了我們回到主題所遇到的問題,在同一上下文查詢同一實體,第一次查詢如果我們利用Find方法查詢會進行翻譯緩存,待下一次再次查詢時不會到數據庫中查詢,如果是下一次查詢不是利用Find方法查詢,比如FirstOrDefault此時會到數據庫中查詢但是此時的值卻不是數據庫中的值而是當前被修改而未被提交的值,那麽我們如何獲取數據庫中的值而不是當前修改而未被提交的值呢?請往下看。

EF和EF Core獲取數據庫值而不是當前修改未被提交的值

我們討論了問題的出現,接下來我們嘗試利用方法來解決,我們可以對上下文所跟蹤的實體進行移除通過Local.Remove,如下:

技術分享圖片
           var context = new EFCoreDbContext();

            var blog1 = context.Blogs.Find(3);
            Console.WriteLine($"數據庫原始值為{blog1.Name}");
            blog1.Name = "Jeffcky";
            context.Blogs.Local.Remove(blog1);
            var blog2 = context.Blogs.FirstOrDefault(d => d.Id == 3);
            Console.WriteLine($"查詢數據庫後的值為{blog2.Name}");
技術分享圖片

Local方法意味獲取在本地所添加、修改等的實體,我們獲取被上下文所本地被上下文所跟蹤的實體然後移除,繼而再進行查詢是不是就可以移除呢?不好意思移除不了。

技術分享圖片

利用AsNoTracking方法

這個算是最簡單的方法之一了,僅僅對於查詢而言通過此方法不會形成快照從而提高查詢性能。

技術分享圖片
            var context = new EFCoreDbContext();

            var blog1 = context.Blogs.Find(3);
            Console.WriteLine($"數據庫原始值為{blog1.Name}");
            blog1.Name = "Jeffcky";
            var blog2 = context.Blogs.AsNoTracking().FirstOrDefault(d => d.Id == 3);
            Console.WriteLine($"查詢數據庫後的值為{blog2.Name}");
技術分享圖片

技術分享圖片

將實體狀態標記為Detached不被上下文跟蹤

技術分享圖片
           var context = new EFCoreDbContext();

            var blog1 = context.Blogs.Find(3);
            Console.WriteLine($"數據庫原始值為{blog1.Name}");
            blog1.Name = "Jeffcky";
            context.Entry(blog1).State = EntityState.Detached;
            var blog2 = context.Blogs.FirstOrDefault(d => d.Id == 3);
            Console.WriteLine($"查詢數據庫後的值為{blog2.Name}");
技術分享圖片

通過Reload方法刷新實體

技術分享圖片
            var context = new EFCoreDbContext();

            var blog1 = context.Blogs.Find(3);
            Console.WriteLine($"數據庫原始值為{blog1.Name}");
            blog1.Name = "Jeffcky";
            context.Entry(blog1).Reload();
            var blog2 = context.Blogs.FirstOrDefault(d => d.Id == 3);
            Console.WriteLine($"查詢數據庫後的值為{blog2.Name}");
技術分享圖片

通過GetDatabaseValues方法直接獲取數據庫中值

技術分享圖片
            var context = new EFCoreDbContext();

            var blog1 = context.Blogs.Find(3);
            Console.WriteLine($"數據庫原始值為{blog1.Name}");
            blog1.Name = "Jeffcky";
            var blog2 = (Blog)context.Entry(blog1).GetDatabaseValues().ToObject();
            Console.WriteLine(blog2.Name);
            //或者
            var db = context.Entry(blog1).GetDatabaseValues();
            Console.WriteLine(db["Name"]);
技術分享圖片

總結

好了今天的內容就到此為止了,無論是EF 6.x還是EF Core,只要我們對一些原理足夠了解才不至於出現讓人意想不到的問題。希望本文對您有所幫助,下節開始講講RabbitMQ,我們下節再會。

討論過後而引發對EF 6.x和EF Core查詢緩存的思考