前言
一直以來寫的博文都是比較溫婉型的博文,今天這篇博文算是一篇批判性博文,有問題歡迎探討,如標題,你到底會不會用EntityFramework啊。
你到底會不會用EntityFramework啊
面試過三年至六年的同行,作為過面試者到如今作為面試官也算是老大對我的信任,對來面試的面試者的任何一位同行絕沒有刁難之意,若還裝逼那就沒有什么意義。我也基本不看面試者的項目經歷,因為我個人覺得每個面試者所在公司所做項目都不一樣,可能面試者項目所做的業務我一點都不知道,而我所關心的是項目當中所用到的技術,稍微看下了簡歷讓面試者簡單做個自我介紹,這算是基本流程吧。然后直接問面試者最擅長的技術是哪些?比如ASP.NET MVC、比如ASP.NET Web APi、比如EntityFramework,再比如數據庫等等。如果面試者沒有特別擅長的技術那我就簡歷上提出所熟悉和項目當中用到的技術進行提問。這里暫且不提其他技術,單單說EntityFramework,面試的面試者大部分都有用過EntityFramework,我就簡單問了下,比如您用的EntityFramework版本是多少?答案是不知道,這個我理解,可能沒去關心過這個問題,再比如我問您知道EntityFramework中有哪些繼承策略,然后面試者要么是一臉懵逼,要么是不知道,要么回了句我們不用。這個我也能理解,重點來了,我問您在EntityFramwork中對於批量添加操作是怎么做的,無一例外遍歷循環一個一個添加到上下文中去,結果令我驚呆了,或許是只關注於實現,很多開發者只關注這個能實現就好了,這里不過多探討這個問題,每個人觀點不一樣。
大部分人用EntityFramework時出現了問題,就吐槽EntityFramework啥玩意啊,啥ORM框架啊,各種問題,我只能說您根本不會用EntityFramework,甚至還有些人並發測試EntityFramework的性能,是的,沒錯,EntityFramework性能不咋的(這里我們只討論EF 6.x),或者說在您實際項目當中有了點並發發現EF出了問題,又開始抱怨EF不行了,同時對於輕量級、跨平台、可擴展的EF Core性能秒殺EF,即使你並發測試EF Core性能也就那么回事,我想說的是你並發測試EF根本沒有任何意義,請好生理解EF作為ORM框架出現的意義是什么,不就是為了讓我們關注業務么,梳理好業務對象,在EF中用上下文操作對象就像直接操作表一樣。然后我們回到EF抵抗並發的問題,有的童鞋認為EF中給我提供了並發Token和行版本以及還有事務,這不就是為了並發么,童鞋對於並發Token和行版本這是對於少量的請求可能存在的並發EF團隊提出的基本解決方案,對於事務無論是同一上文抑或是跨上下文也好只是為了保證數據一致性罷了。要是大一點的並發來了,您難道還讓EF不顧一切沖上去么,這無疑是飛蛾撲火自取滅亡,你到底會不會用EntityFramework啊。EF作為概念上的數據訪問層應該是處於最底層,如果我們項目可預見沒有所謂的並發問題,將上下文直接置於最上層比如控制器中並沒有什么問題,但是項目比較大,隨着用戶量的增加,我們肯定是可預知的,這個我們需要從項目架構層面去考慮,此時在上下文上游必定還有其他比如C#中的並發隊列或者Redis來進行攔截使其串行進行。
有些人號稱是對EntityFramwork非常了解,認為不就是增、刪、該、查么,但是有的時候用出了問題就開始自我開解,我這么用沒有任何問題啊,我們都知道在EF 6.x中確實有很多坑,這個時候就借這個緣由洗白了,這不是我的鍋,結果EF背上了無名之鍋,妄名之冤。是的,您沒有說錯,EF 6.x是有很多坑,您避開這些坑不就得了,我只能說這些人太浮於表面不了解基本原理就妄下結論,您到底會不會用EntityFramework啊。好了來,免說我紙上談兵,我來舉兩個具體例子,您看自己到底會不會用。
EntityFramework 6.x查詢
static void Main(string[] args) { using (var ctx = new EfDbContext()) { ctx.Database.Log = Console.WriteLine; var code = "Jeffcky"; var order = ctx.Orders.FirstOrDefault(d => d.Code == code); }; Console.ReadKey(); }
這樣的例子用過EF 6.x的童鞋估計用爛了吧,然后查詢出來的結果讓我們也非常滿意至少是達到了我們的預期,我們來看看生成的SQL語句。
請問用EF的您發現什么沒有,在WHERE查詢條件加上了一堆沒有用的東西,我只是查詢Code等於Jeffcky的實體數據,從生成的SQL來看可查詢Code等於Jeffcky的也可查詢Code等於空的數據,要是我們如下查詢,生成如上SQL語句我覺得才是我們所預期的對不對。
using (var ctx = new EfDbContext()) { ctx.Database.Log = Console.WriteLine; var code = "Jeffcky"; var orders = ctx.Orders.Where(d => d.Code == null || d.Code == code).ToList(); };
如果您真的會那么一點點用EntityFramework,那么請至少了解背后生成的SQL語句吧,這是其中之一,那要是我們直接使用值查詢呢,您覺得是否和利用參數生成的SQL語句是一樣的呢?
using (var ctx = new EfDbContext()) { ctx.Database.Log = Console.WriteLine; var order = ctx.Orders.FirstOrDefault(d => d.Code == "Jeffcky"); };
出乎意料吧,利用值查詢在WHERE條件上沒有過多的條件過濾,而利用參數查詢則是生成過多的條件篩選,到這里是不是就到此為止了呢,如果您對於參數查詢不想生成對空值的過濾,我們在上下文構造函數中可關閉這種所謂【語義可空】判斷,如下:
public class EfDbContext : DbContext { public EfDbContext() : base("name=ConnectionString") { Configuration.UseDatabaseNullSemantics = true; } }
// 摘要:
// 獲取或設置一個值,該值指示當比較兩個操作數,而它們都可能為 null 時,是否展示數據庫 null 語義。默認值為 false。例如:如果 UseDatabaseNullSemantics
// 為 true,則 (operand1 == operand2) 將轉換為 (operand1 = operand2);如果 UseDatabaseNullSemantics
// 為 false,則將轉換為 (((operand1 = operand2) AND (NOT (operand1 IS NULL OR operand2
// IS NULL))) OR ((operand1 IS NULL) AND (operand2 IS NULL)))。
//
// 返回結果:
// 如果啟用數據庫 null 比較行為,則為 true;否則為 false。
在EF 6.x中對於查詢默認情況下會進行【語義可空】篩選,通過如上分析,不知您們是否知道如上的配置呢。
EntityFramework 6.x更新
EF 6.x更新操作又是用熟透了吧,在EF中沒有Update方法,而在EF Core中存在Update和UpdateRange方法,您是否覺得更新又是如此之簡單呢?我們下面首先來看一個例子,看看您是否真的會用。
static Customer GetCustomer() { var customer = new Customer() { Id = 2, CreatedTime = DateTime.Now, ModifiedTime = DateTime.Now, Email = "2752154844@qq.com", Name = "Jeffcky1" }; return customer; }
如上實體如我們請求傳到后台需要修改的實體(假設該實體在數據庫中存在哈),這里我們進行寫死模擬。接下來我們來進行如下查詢,您思考一下是否能正常更新呢?
using (var ctx = new EfDbContext()) { var customer = GetCustomer(); var dataBaseCustomer = ctx.Customers.FirstOrDefault(d => d.Id == customer.Id); if (dataBaseCustomer != null) { ctx.Customers.Attach(customer); ctx.Entry(customer).State = EntityState.Modified; if (ctx.SaveChanges() > 0) { Console.WriteLine("更新成功"); } else { Console.WriteLine("更新失敗"); } } };
首先我們根據傳過來的實體主鍵去數據庫中查詢是否存在,若存在則將傳過來的實體附加到上下文中(因為此時請求過來的實體還未被跟蹤),然后將其狀態修改為已被修改,最后提交,解釋的是不是非常合情合理且合法,那是不是就打印更新成功了呢?
看到上述錯誤想必有部分童鞋一下子就明白問題出在哪里,當我們根據傳過來的實體主鍵去數據庫查詢,此時在數據庫中存在就已被上下文所跟蹤,然后我們又去附加已傳過來的實體且修改狀態,當然會出錯因為在上下文已存在相同的對象,此時必然會產生已存在主鍵沖突。有的童鞋想了直接將傳過來的實體狀態修改為已修改不就得了么,如下:
using (var ctx = new EfDbContext()) { var customer = GetCustomer(); ctx.Entry(customer).State = EntityState.Modified; if (ctx.SaveChanges() > 0) { Console.WriteLine("更新成功"); } else { Console.WriteLine("更新失敗"); } };
如此肯定能更新成功了,我想都不會這么干吧,要是客戶端進行傳過來的主鍵在數據庫中不存在呢(至少我們得保證數據是已存在才修改),此時進行如上操作將拋出如下異常。
此時為了解決這樣的問題最簡單的方法之一則是在查詢實體是否存在時直接通過AsNoTracking方法使其不能被上下文所跟蹤,這樣就不會出現主鍵沖突的問題。
var dataBaseCustomer = ctx.Customers .AsNoTracking() .FirstOrDefault(d => d.Id == customer.Id);
我們繼續往下探討 ,此時我們將數據庫Email修改為可空(映射也要對應為可空,否則拋出驗證不通過的異常,你懂的),如下圖:
然后將前台傳過來的實體進行如下修改,不修改Email,我們注釋掉。
static Customer GetCustomer() { var customer = new Customer() { Id = 2, CreatedTime = DateTime.Now, ModifiedTime = DateTime.Now, //Email = "2752154844@qq.com", Name = "Jeffcky1" }; return customer; }
我們接着再來進行如下查詢試試看。
using (var ctx = new EfDbContext()) { var customer = GetCustomer(); var dataBaseCustomer = ctx.Customers .AsNoTracking() .FirstOrDefault(d => d.Id == customer.Id); if (dataBaseCustomer != null) { ctx.Customers.Attach(customer); ctx.Entry(customer).State = EntityState.Modified; if (ctx.SaveChanges() > 0) { Console.WriteLine("更新成功"); } else { Console.WriteLine("更新失敗"); } } };
此時Email為可空,因為我們設置實體狀態為Modified,此時將對實體進行全盤更新,所以對於設置實體狀態為Modified是針對所有列更新,要是我們只想更新指定列,那這個就不好使了,此時我們可通過Entry().Property()...來手動更新指定列,比如如下:
using (var ctx = new EfDbContext()) { var customer = GetCustomer(); var dataBaseCustomer = ctx.Customers .AsNoTracking() .FirstOrDefault(d => d.Id == customer.Id); if (dataBaseCustomer != null) { ctx.Customers.Attach(customer); ctx.Entry(customer).Property(p => p.Name).IsModified = true; ctx.Entry(customer).Property(p => p.Email).IsModified = true; if (ctx.SaveChanges() > 0) { Console.WriteLine("更新成功"); } else { Console.WriteLine("更新失敗"); } } };
我們繼續往下走。除了上述利用AsNoTracking方法外使其查詢出來的實體未被上下文跟蹤而成功更新,我們還可以使用手動賦值的方式更新數據,如下:
using (var ctx = new EfDbContext()) { var customer = GetCustomer(); var dataBaseCustomer = ctx.Customers .FirstOrDefault(d => d.Id == customer.Id); if (dataBaseCustomer != null) { dataBaseCustomer.CreatedTime = customer.CreatedTime; dataBaseCustomer.ModifiedTime = customer.ModifiedTime; dataBaseCustomer.Email = customer.Email; dataBaseCustomer.Name = customer.Name; if (ctx.SaveChanges() > 0) { Console.WriteLine("更新成功"); } else { Console.WriteLine("更新失敗"); } } };
如上也能更新成功而不用將查詢出來的實體未跟蹤,然后將前台傳過來的實體進行附加以及修改狀態,下面我們刪除數據庫中創建時間和修改時間列,此時我們保持數據庫中數據和從前台傳過來的數據一模一樣,如下:
static Customer GetCustomer() { var customer = new Customer() { Id = 2, Email = "2752154844@qq.com", Name = "Jeffcky1" }; return customer; }
接下來我們再來進行如下賦值修改,您會發現此時更新失敗的:
using (var ctx = new EfDbContext()) { var customer = GetCustomer(); var dataBaseCustomer = ctx.Customers .FirstOrDefault(d => d.Id == customer.Id); if (dataBaseCustomer != null) { dataBaseCustomer.Email = customer.Email; dataBaseCustomer.Name = customer.Name; if (ctx.SaveChanges() > 0) { Console.WriteLine("更新成功"); } else { Console.WriteLine("更新失敗"); } } };
這是為何呢?因為數據庫數據和前台傳過來的數據一模一樣,但是不會進行更新,毫無疑問EF這樣處理是明智且正確的,無需多此一舉更新,那我們怎么知道是否有不一樣的數據進行更新操作呢,換句話說EF怎樣知道數據未發生改變就不更新呢?我們可以用上下文屬性中的ChangeTacker中的HasChanges方法,如果上下文知道數據未發生改變,那么直接返回成功,如下:
using (var ctx = new EfDbContext()) { var customer = GetCustomer(); var dataBaseCustomer = ctx.Customers .FirstOrDefault(d => d.Id == customer.Id); if (dataBaseCustomer != null) { dataBaseCustomer.Email = customer.Email; dataBaseCustomer.Name = customer.Name; if (!ctx.ChangeTracker.HasChanges()) { Console.WriteLine("更新成功"); return; } if (ctx.SaveChanges() > 0) { Console.WriteLine("更新成功"); } else { Console.WriteLine("更新失敗"); } } };
好了到此為止我們已經看到關於更新已經有了三種方式,別着急還有最后一種,通過Entry().CurrentValues.SetValues()方式,這種方式也是指定更新,將當前實體的值設置數據庫中查詢出來所被跟蹤的實體的值。如下:
using (var ctx = new EfDbContext()) { var customer = GetCustomer(); var dataBaseCustomer = ctx.Customers .FirstOrDefault(d => d.Id == customer.Id); if (dataBaseCustomer != null) { ctx.Entry(dataBaseCustomer).CurrentValues.SetValues(customer); if (ctx.SaveChanges() > 0) { Console.WriteLine("更新成功"); } else { Console.WriteLine("更新失敗"); } } };
關於EF更新方式講了四種,其中有關細枝末節就沒有再細說可自行私下測試,不知道用過EF的您們是否四種都知道以及每一種對應的場景是怎樣的呢?對於數據更新我一般直接通過查詢進行賦值的形式,當然我們也可以用AutoMapper,然后通過HasChanges方法來進行判斷。
EntityFramework 6.x批量添加
對於批量添加已經是EF 6.x中老掉牙的話題,但是依然有很多面試者不知道,我這里再重新講解一次,對於那些私下不學習,不與時俱進的童鞋好歹也看看前輩們(不包括我)總經的經驗吧,不知道為何這樣做,至少回答答案是對的吧。看到下面的批量添加數據代碼是不是有點想打人。
using (var ctx = new EfDbContext()) { for (var i = 0; i <= 100000; i++) { var customer = new Customer { Email = "2752154844@qq.com", Name = i.ToString() }; ctx.Customers.Add(customer); ctx.SaveChanges(); } };
至於原因無需我過多解釋,如果您這樣操作,那您這一天的工作大概也就是等着數據添加完畢,等啊等。再不濟您也將SaveChanges放在最外層一次性提交啊,這里我就不再測試,浪費時間在這上面沒必要,只要您稍微懂點EF原理至少會如下這么使用。
var customers = new List<Customer>(); using (var ctx = new EfDbContext()) { for (var i = 0; i <= 100000; i++) { var customer = new Customer { Email = "2752154844@qq.com", Name = i.ToString() }; customers.Add(customer); } ctx.Customers.AddRange(customers); ctx.SaveChanges(); };
如果您給我的答案如上,我還是認可的,要是第一種真的說不過去了啊。經過如上操作依然有問題,我們將所有記錄添加到同一上下文實例,這意味着EF會跟蹤這十萬條記錄, 對於剛開始添加的幾個記錄,會運行得很快,但是當越到后面數據快接近十萬時,EF正在追蹤更大的對象圖,您覺得恐怖不,這就是您不懂EF原理的代價,還對其進行詬病,吐槽性能可以,至少保證您寫的代碼沒問題吧,我們進一步優化需要關閉自調用的DetectChanges方法無需進行對每一個添加的實體進行掃描。
var customers = new List<Customer>(); using (var ctx = new EfDbContext()) { bool acd = ctx.Configuration.AutoDetectChangesEnabled; try { ctx.Configuration.AutoDetectChangesEnabled = false; for (var i = 0; i <= 100000; i++) { var customer = new Customer { Email = "2752154844@qq.com", Name = i.ToString() }; customers.Add(customer); } ctx.Customers.AddRange(customers); ctx.SaveChanges(); } finally { ctx.Configuration.AutoDetectChangesEnabled = acd; } };
此時我們通過局部關閉自調用DetectChanges方法,此時EF不會跟蹤實體,這樣將不會造成全盤掃描而使得我們不會處於漫長的等待,如此優化將節省大量時間。如果在我們了解原理的前提下知道添加數據到EF上下文中,隨着數據添加到集合中也會對已添加的數據進行全盤掃描,那我們何不創建不同的上下文進行批量添加呢?未經測試在這種情況下是否比關閉自調用DetectChanges方法效率更高,僅供參考,代碼如下:
public static class EFContextExtensions { public static EfDbContext BatchInsert<T>(this EfDbContext context, T entity, int count, int batchSize) where T : class { context.Set<T>().Add(entity); if (count % batchSize == 0) { context.SaveChanges(); context.Dispose(); context = new EfDbContext(); } return context; } }
static void Main(string[] args) { var customers = new List<Customer>(); EfDbContext ctx; using (ctx = new EfDbContext()) { for (var i = 0; i <= 100000; i++) { var customer = new Customer { Email = "2752154844@qq.com", Name = i.ToString() }; ctx = ctx.BatchInsert(customer, i, 100); } ctx.SaveChanges(); }; Console.ReadKey(); }
總結
不喜勿噴,敢問您到底會不會用EntityFramework啊,EF 6.x性能令人詬病但是至少得保證您寫的代碼沒問題吧,對於復雜SQL查詢可以EF非常雞肋,但是我們可結合Dapper使用啊,您又擔心EF 6.x坑太多,那請用EntityFramework Core吧,您值得擁有。謹以此篇批判那些不會用EF的同行,還將EF和並發扯到一塊,EF不是用來抵抗並發,它的出現是為了讓我們將重心放在梳理業務對象,關注業務上,有關我對EF 6.x和EF Core 2.0理解全部集成到我寫的書《你必須掌握的EntityFramework 6.x與Core 2.0》下個月可正式購買,想了解的同行可關注下,謝謝。
后續
看了很多前輩精彩的評論,我個人覺得既然用了EF那就得提前知道這些基礎知識或者基本原理,出了問題歸結於EF,那就有點說不過去了,再者網上的前輩們在項目中總結的經驗和老外的技術文檔比比皆是,為何不花點時間提前了解下是否滿足項目需求呢。我在EF這方面不是專家,更談不上精通,只不過經常看看國內和國外的技術文檔,自己私下親自實踐罷了。最后總結起來一點則是選擇適合自己項目的才是最好的,別太依賴EF,EF解決不了所有問題。