本文將在技術層面挑戰園子里的權威大牛們,言語不敬之處敬請包涵。本文旨為技術交流,歡迎拍磚。
園子里面分享和推薦Entity Framework(以下簡稱EF)的Repository(倉儲)設計模式的文章真不少,其中還有很多大牛很詳細描述怎么去實現。但是這些文章真是害人不淺。我現在想問問這些大牛們,你們現在的項目真的還在這樣用嗎?
下面是在找找看里面隨便挑的幾篇,如果你從未了解過EF Repository,你可以看看:
- 分享基於Entity Framework的Repository模式設計(附源碼)
- EF中Repository模式應用場景
- MVC+LINQToSQL的Repository模式之(三)Repository模式實現統一CURD操作,實現EF中的Find主鍵查找
- Using Repository and Unit of Work patterns with Entity Framework 4.0
我與EF的緣分
大約2年以前,我開始接觸Entity Framework,當時公司大牛推薦了幾篇EF的Repository設計模式的文章,公司內部也有一個使用EF Repository的框架,當時公司.NET項目基本都用那個框架,我們項目當然也不例外。
大約用了半年,做了2-3個項目,我越來越感覺EF倉儲模式用起來一點都方便,代碼寫起來很別扭。但那會還不知道為什么,因為那會我也沒有完全理解倉儲模式到底是個什么。
2012年7月,我開始獨立帶領一個中型項目開發團隊。數據庫訪問的架構仍然延續了公司一直推崇的EF+Repository模式。當開發進行到一半的時候,我感覺我們的數據庫訪問的代碼已經很亂了。service層在訪問數據庫,UI層也在訪問數據庫。由於同時使用了構造函數方式的依賴注入,循環依賴的問題也讓代碼變得難以維護。但是直到項目結束我才明白應該怎么使用EF。
在隨后的一個項目中,我完全拋棄了EF+Repository模式,使用一種全新的架構。在新架構上開發了一個多月,我意識到我們確實用對了,於是我跟公司推薦。但公司大牛們並不認可。幾個月后,有另一位項目經理也提出EF+Repository模式用起來太惡心。
在接下來大半年時間到現在,我帶的多個項目都采用了新的架構,用起來實在太爽了,我也多次建議在公司推廣。后來公司大牛基本也都否認了EF+Repository模式,但由於各種原因,其他項目組使用我們新架構的只占少數。
寫這么長一段經歷我只想說“大牛”們的權威真的影響好大,但只有真理才經得起時間的考驗。下面我將詳細闡述我對EF+Repository模式的理解。
EF+Repository模式錯在哪里
1. 沒有理解到什么是repository設計模式。
首先我想說repository設計模式的原理。repository即倉庫,通常我們會按照數據庫中的表來建repository。repository外一定有一個包裹器,因為所有對repository的增刪改都不會立即提交到數據庫,而需要調用包裹器的提交才會真正跟數據庫進行通訊。
當你看這篇文章(分享基於Entity Framework的Repository模式設計(附源碼))的時候,乍一看,確實是按照repository來設計的啊。IUnitOfWork即包裹器,數據的增刪改查都放到了這個包裹器里面。所以啊,很多人就這樣被欺騙了。
實際上這篇文章的中的repository框架性的代碼沒有一行有意義,因為EF本身即是按照repository來設計的。這個觀點有很多人知道,但更多的人不知道。請看下面這行代碼:
var db = new DemoDbContext();
這里的db即相當於是repository的包裹器。通過包裹器可以非常輕松的訪問到其下的每一個repository,這也絕對是最簡單的訪問方式了,如:
db.Users....
db.Products....
而這篇文章中,如果要訪問repository,還要通過IOC。不知道繞這么大個圈子干嘛,不是自己給自己找事嗎。
2. EF本身就是repository設計模式的
倉儲模式最大的優點就是所有的數據訪問首先是通過倉庫的,對倉庫的增刪改都不會立即提交到數據庫,而只有當調用了倉庫包裹器,這些增刪改的操作才會一次提交到數據庫。
在EF中,DbSet<TEntity>即是定義的倉庫,DbContext即是倉庫包裹器,相當於UnitOfWork。所有對DbSet<TEntity>的增刪改都只有在調用了DbContext的SaveChanges之后才會提交到數據庫。這樣看,EF是不是完全實現了Repository了呢?
另外,DbContext的SaveChanges本身包含了一個事務機制,再結合TransactionScope類,我認為EF真的是完美解決了所有事務問題。請看我之前的一篇博客中分享的基於EF事務機制的架構:http://www.cnblogs.com/leotsai/p/how-to-use-entity-framework-transaction-scope.html
我真不明白,這么多大牛們為什么要把EF本身的Repository再包裹一遍。關鍵是包裹一遍的代碼不但丑,不但難用,而且有BUG!
EF+Repository模式的三大缺點
1. 代碼丑
首先整個+Repository這一層的代碼都是多余的,包括UnitOfWork類和繼承自IRepository的類。 在這個架構下寫代碼,你會寫大量重復的沒有意義的IRepository的實現。而在service層,由於各個repository是獨立的,但是實體類之間又是有關系的(導航屬性),有時你的代碼通過導航屬性在訪問另一個repository,有時又不是,有時為了統一全部從repository里面讀數據,你不得不先定義要用到的所有repository,然后再寫很多看不懂的inner join。當業務邏輯復雜的時候,這樣的代碼真的是要讓維護人員崩潰。
2. 難用
首先你要寫大量重復的代碼。但更惡心的還不是這個,更惡心的是,明明有導航屬性可以訪問到另一張表,但是為了不跨repository,你只能通過unitOfWork.GetRepo來獲取一個repository的實例,然后就是大量的inner join。並且這個規則實在很難執行,於是你發現有時候你也在用導航屬性,然后你就會懷疑了,我定義的repository有何意義?
再加上很多人根本不知道UnitOfWork是什么意思,於是到處都有UnitOfWork,比如UI層都在用。可曾想,UnitofWork原來是用來包裹數據庫訪問的repository的,UI層怎能訪問數據庫呢?
3. 有BUG
這里說的BUG不是指運行會報錯,而是指架構的BUG。說白一點,就是這個架構會勾引你犯錯寫BUG。
最最明顯的一個BUG就是,IReposoitory<TEntity>下面有個void Update(TEntity entity)方法。當然也不是所有的EF+Reposoitory模式都這樣寫,但大多數是有的。
這個方法真的是害死人了。我相信被這個方法害過的人大有人在,我曾經也是其中之一。舉個簡單的例子,看能不能勾起你那憂傷的回憶。請看代碼:
1 public ActionResult Update(Product product) 2 { 3 _productRepository.Update(product); 4 _unitOfWork.SaveChanges(); 5 return View("UpdateSuccess"); 6 }
這段代碼看上去沒有問題,但BUG是,每次更新一個product,createdTime就被設置為當前時間,因為在Product構造函數里設置了createdTime為now。哎,真是悲劇!作為一個項目經理,你可能無數次忠告你的程序員注意這個BUG,但這個BUG還是不斷重現,因為過兩天Status字段又被重置了,再過幾天另一個字段的值又丟失了。
這個設計實際上跟Repository沒有任何關系,但很多大牛這樣寫了,害了很多人,所以在此專門提出。
結論
EF本身即是按照Repository模式設計的,我們完全沒有任何必要再自己去寫一套repository把EF的repository再包裹一層;EF本身的事務機制本是完美的,額外包裹一層repository之后,讓事務變得模糊。
我把我們項目的架構提取出來放到了github上面,大家可以看看:https://github.com/leotsai/mvcsolution。注意Data和Services層的代碼。
下面的代碼是其中一個service,是不是代碼簡潔合理?

1 using System; 2 using System.Linq; 3 using MvcSolution; 4 using MvcSolution.Data.Entities; 5 using MvcSolution.Data.Entities; 6 using MvcSolution.Infrastructure.Security; 7 8 namespace MVCSolution.Services.Users 9 { 10 public class UserService : ServiceBase<User>, IUserService 11 { 12 #region Implementation of IUserService 13 14 public User Get(string username) 15 { 16 using(var db = base.NewDB()) 17 { 18 return db.Users.Get(username); 19 } 20 } 21 22 public string[] GetRoles(string username) 23 { 24 using (var db = base.NewDB()) 25 { 26 return db.Roles.WhereByUsername(username).Select(x => x.Name).Distinct().ToArray(); 27 } 28 } 29 30 public bool CanLogin(string username, string password) 31 { 32 using (var db = base.NewDB()) 33 { 34 var user = db.Users.Get(username); 35 return user != null && user.IsDisabled == false 36 && user.Password == CryptoService.MD5Encrypt(password); 37 } 38 } 39 40 public void Register(User user) 41 { 42 using (var db = base.NewDB()) 43 { 44 if (db.Users.Get(user.Username) != null) 45 { 46 throw new Exception("username already registered."); 47 } 48 user.Password = CryptoService.MD5Encrypt(user.Password); 49 db.Users.Add(user); 50 db.SaveChanges(); 51 } 52 } 53 54 #endregion 55 } 56 }
關於如何更好的使用EF的架構,我認為應該有以下3點:
- 在service層要給訪問數據庫的代碼絕對的自由,就像sql一樣,導航屬性、跨表增刪改查隨便寫,而不是UnitOfWork.GetRepo;
- 在service層之后要終止對數據庫的訪問,也就是每一個service方法必須將DbContext dispose掉,不管這個service是做增刪改還是查詢;
- 在UI層可使用TransactionScope來包裹多個service,從而實現跨service的事務機制。
完。
MvcSolution框架 QQ討論群:539301714
如果遇到問題,可以進QQ群,群里的高手還有我會盡量盡快幫你解決問題。