倉儲模式還適用於EF Core嗎?

EF Core已經出2.1版,開始考慮使用據傳性能調優已經接近C++的.Net Core寫新項目。想要拋棄之前使用asp.net那種sql腳本的碼代碼方式。同時找了一些開源的項目,好比ABP,SimpleCommerce。前端

其中ABP項目大而全,封裝了不少模式,但文檔更可能是描述如何使用,若是本身不去看代碼很容易不知所云。ABP項目基於Ioc(castle windsor)的動態代理特性實現了及其靈活的模塊化方案,能夠在運行過程當中加載項目並初始化。同時ABP封裝了自身的UnitofWork方式,結合了IoC框架太多特性(castle windsor)。好比使用了該框架動態代理的實現,在業務執行以前插入UnitofWork相關邏輯。git

而SimpleCommerce則利用了AutoFac以及asp.netcore的特性實現了模塊化。對於倉儲模式涉及的比較少。對於項目解耦能夠說是一個簡單的示例。程序員

那麼究竟要怎麼開始EFCore項目?近期看到一篇,比較實用簡單。github

 

不,倉儲或者說unit-of-work模式(簡稱 Rep/UoW)再也不使用於EF Core。EF Core 已經實現了Rep/UoW模式,所以在ef core之上再抽象一層Rep/UoW模式,並沒有幫助。web

比較明智的選擇是直接使用EF Core,這樣你可使用EF Core 的所有功能,以實現高性能的數據庫訪問。sql

本文的目的:數據庫

本文關注一下幾點:網絡

  • 人們如何評價EF的Rep / UoW模式。
  • 在EF的基礎上使用Rep / UoW模式的利弊。
  • 使用EF Core代碼替換Rep / UoW模式的三種方法。
  • 如何使您的EF Core數據庫訪問代碼易於查找和重構。
  • 關於對EF Core 代碼的單元測試。

我將假設你熟悉C#代碼和Entity Framework6(EF6.x)或者Entity Framework Core。本文主要探討EF Core ,但大部分也都適用於EF6.x。架構

場景設定mvc

  2013年我開始建設一個關於醫療保健的大型web應用。使用了剛剛面世的ASP.NET MVC4 and EF 5,它支持可以處理地理數據的SQL Spatial types。

當時流行的數據庫訪問模式是Rep/UoW模式----具體能夠查看微軟2013年寫的文章,關於使用EFCore 和Rep / UoW模式進行數據庫訪問。

隨着時間的推移,我在2017年末與一家初創公司簽定了合同,以幫助解決EF6.x應用程序的性能問題。性能問題的主要部分緣由是延遲加載,這是由於應用程序使用Rep / UoW模式所需。

  事實證實,幫助啓動項目的程序員使用了Rep/UoW模式。在與精通技術的公司創始人交談時,他說他發現應用程序中的Rep / UoW部分很是不透明且難以使用。

 

人們如何評價EF的Rep / UoW模式。

在做爲我對當前Spatial Modeller™設計的評論的一部分進行研究時,我發現了一些博客文章,這些文章爲放棄存儲庫提供了使人信服的理由。這類最有說服力和深思熟慮的帖子是「構建於UnitofWork的Repositories不是一個好主意」。Rob Conery的主要觀點是,Rep / UoW只是複製實體框架(EF)DbContext給你的東西,因此爲何要將完美框架隱藏在一個沒有增長任何價值的外觀背後。

另外一篇博文「爲何EF使存儲庫模式過期」,文中 Isaac Abraham 指出,repository 並無使測試更加容易,而這是他本該實現的。對於EF Core來講更是如此。

他們的觀點對嗎?

對於repository/unit-of-work優缺點,個人的觀點

我將經可能不偏不倚地從新審視 Rep/UoW 模式。下面是個人觀點:

Rep / UoW模式的優勢(按好壞順序,最好的優先)

  1.  隔離數據庫代碼:存儲庫模式的一大優勢是您知道全部數據庫訪問代碼的位置。此外,您一般將存儲庫拆分爲多個部分,例如目錄倉儲,訂單處理倉儲等,這使得查找具備錯誤或須要性能調整的特定查詢的代碼變得容易。
    這絕對是一大優勢。
  2. 聚合:域驅動設計(DDD)是一種設計系統的方法,它建議您有一個根實體,並將其餘關聯實體聚合於它。我在「Entity Framework Core in Action」一書中使用的示例是一個書實體,其中包含評論實體的集合。那些評論只有在鏈接到書本的時候纔有意義。所以DDD建議你只能經過書實體來修改評論。Rep/UoW模式經過提供向Book Repository添加/刪除評論的方法來實現此目的。
  3. 隱藏複雜的T-SQL命令:有時你須要繞過智能的EF Core的直接使用T-SQL。這種類型的訪問應該從較高層隱藏,但很容易找到以幫助維護/重構。應該指出,Rob Conery的文章 命令/查詢 對象也能夠處理這個問題。
  4. 易於模擬/測試:模擬單個倉儲很容易,這使得單元測試代碼更容易訪問數據庫。這種狀況在幾年前就已經存在,可是如今還有其餘解決這個問題的方法,我將在後面介紹。
 
 
 您將注意到我沒有列出「用另外一個數據庫訪問庫替換EF Core」。這是Rep / UoW背後的思想之一,但我認爲這是一種誤解,由於a)很難替換數據庫訪問庫,b)你真的要在你的應用程序中交換這樣一個關鍵庫嗎?你確定不會在ASP.NET或React.js之上抽象一層,那麼爲何要如此對你的數據庫訪問庫呢?
 

Rep / UoW模式的缺點(按好壞順序,最壞的優先)

前三項都是關於性能。我並非說你寫不出高效的Rep/UoW 模式,但他的確很難,我看到過不少種實現都帶有性能問題(包括微軟的舊Rep/UoW實現)

這是我在Rep / UoW模式中發現的缺點列表:

  1. 性能 - 排序/過濾:在微軟的舊(2013)Rep/UoW 實現中,有個GetStudents方法,返回 IEnumerable<Student>。這意味着任何過濾或排序都將在軟件中完成,這是低效的。
  2. 性能 - 延遲加載:存儲庫一般返回一種類型的IEnumerable / IQueryable結果,例如Microsoft示例中的Student實體類。假設您想顯示學生所擁有的關係中的信息,例如他們的地址,要怎麼辦?在這種狀況下,倉儲中最簡單的方法是使用延遲加載來讀取學生的地址實體。問題是延遲加載會致使數據庫爲其加載的每一個延遲加載數據做一次查詢,這比將全部數據庫訪問組合到一個數據庫查詢中要慢。
  3. 性能 - 更新:許多Rep / UoW實現嘗試隱藏EF Core,而且這樣作不會充分利用其全部功能。例如,Rep / UoW將使用EF Core 的 Update方法更新實體,該方法保存實體中的每一個屬性。然而,使用EF Core的內置更改跟蹤功能,它只會更新已更改的屬性。
  4. 過於通用:Rep/UoW 模式吸引人的一個緣由來自這樣的觀點:能夠寫一個通用的倉儲,這樣你能夠用它實現子倉儲,好比目錄倉儲,訂單處理倉儲等等,這將會減小代碼量。可是個人經驗是:通用倉儲在初期的確會有用,但在你後期往每一個子倉儲添加愈來愈多的代碼時,將會變得愈來愈複雜。

總結壞的部分 - - Rep/UoW 模式 隱藏EF Core,這意味着您沒法使用EF Core的功能來生成簡單但高效的數據庫訪問代碼。

如何使用EF Core,但仍然受益於Rep / UoW模式的優勢

  在以前的「好的部分」部分中,我列出了 Rep/UoW 表現良好的隔離,聚合,隱藏和單元測試。在本節中,我將討論一些不一樣的軟件模式和實踐,當與良好的架構設計相結合時,在您直接使用EF Core時提供相同的隔離,聚合等功能。

  我將解釋每個,而後在分層軟件架構中將它們組合在一塊兒。

  查詢對象:一種隔離和隱藏數據庫讀取代碼的方法。

  數據庫訪問能夠分爲四種類型:建立,讀取,更新和刪除 - 稱爲CRUD。對我來講,讀取部分(在EF Core中稱爲查詢)一般是構建和性能調整最難的部分。許多應用程序依賴於良好,快速的查詢,例如,要購買的產品列表,要作的事情列表等等。人們提出的答案是查詢對象。

  我在2013年第一次遇到他們在Rob Conery的文章(前面提到過)中,他引用了命令/查詢對象。另外,吉米·博加德在2012年發佈了一個名爲「同意對存儲庫的查詢對象」的帖子。使用.NET的IQueryable類型和擴展方法,咱們能夠改進Rob和Jimmy的例子中的查詢對象模式。

  下面的清單給出了一個查詢對象的簡單示例,該對象能夠選擇整數列表的排序順序。

 1 public static class MyLinqExtension  2 {  3     public static IQueryable<int> MyOrder  4         (this IQueryable<int> queryable, bool ascending)  5  {  6         return ascending  7             ? queryable.OrderBy(num => num)  8             : queryable.OrderByDescending(num => num);  9  } 10 }

這是一個如何調用MyOrder查詢對象的示例

1 var numsQ = new[] { 1, 5, 4, 2, 3 }.AsQueryable(); 2  
3 var result = numsQ 4     .MyOrder(true) 5     .Where(x => x > 3) 6     .ToArray();

  MyOrder查詢對象起做用,由於IQueryable類型包含一個命令列表,這些命令在應用ToArray方法時執行。在個人簡單示例中,我沒有使用數據庫,但若是咱們使用應用程序的DbContext中的DbSet <T>屬性替換numsQ變量,那麼IQueryable <T>類型中的命令將轉換爲數據庫命令。

  由於IQueryable <T>類型直到最後才執行,因此能夠將多個查詢對象連接在一塊兒。讓我從個人書「Entity Framework Core in Action」中給出一個更復雜的數據庫查詢示例。下面的代碼中使用連接在一塊兒的四個查詢對象來選擇,排序,過濾和分頁某些書籍上的數據。您能夠在在線網站http://efcoreinaction.com/上看到這一點。

 1 public IQueryable<BookListDto> SortFilterPage  2  (SortFilterPageOptions options)  3 {  4     var booksQuery = _context.Books  5  .AsNoTracking()  6  .MapBookToDto()  7  .OrderBooksBy(options.OrderByOptions)  8  .FilterBooksBy(options.FilterBy,  9  options.FilterValue); 10  
11  options.SetupRestOfDto(booksQuery); 12  
13     return booksQuery.Page(options.PageNum-1, 14  options.PageSize); 15 }

  查詢對象提供比Rep / UoW模式更好的隔離,由於您能夠將複雜查詢拆分爲一系列能夠連接在一塊兒的查詢對象。這使得編寫/理解,重構和測試更容易。此外,若是您有一個須要原始SQL的查詢,您可使用EF Core的FromSql方法,該方法也返回IQueryable <T>。

  處理 建立,更新和刪除 數據庫訪問的方法

  查詢對象處理CRUD的讀取部分,可是建立,更新和刪除部分,您在哪裏寫入數據庫?我將向您展現運行CUD操做的兩種方法:直接使用EF Core命令、使用實體類中的DDD方法。咱們看一個很是簡單的更新示例:在個人圖書應用程序中添加評論(請參閱http://efcoreinaction.com/)。

  注意:若是您想嘗試添加評論,能夠這樣作:隨書有一個GitHub代碼庫:https://github.com/JonPSmith/EfCoreInAction.。要運行ASP.NET Core應用程序,而後a)克隆repo,選擇分支Chapter05(每章都有一個分支)並在本地運行應用程序。您將看到每本書旁邊都出現一個Admin按鈕,其中包含一些CUD命令。

 

選項1 - 直接使用EF Core命令

最明顯的方法是使用EF Core方法來更新數據庫。這是一種方法,能夠爲書籍添加新評論,並提供用戶提供的評論信息。注意:ReviewDto是一個類,用於保存用戶填寫審閱信息後返回的信息。

 1 public Book AddReviewToBook(ReviewDto dto)  2 {  3     var book = _context.Books  4         .Include(r => r.Reviews)  5         .Single(k => k.BookId == dto.BookId);  6     var newReview = new Review(dto.numStars, dto.comment, dto.voterName);  7  book.Reviews.Add(newReview);  8  _context.SaveChanges();  9     return book; 10 }

步驟是:

第3行到第5行:加載特定書籍,由評論輸入中的BookId定義,帶有評論列表

第6行到第7行:建立新評論並將其添加到圖書的評論列表中

第8行:調用SaveChanges方法,該方法更新數據庫。

注意:AddReviewToBook方法位於名爲AddReviewService的類中,該類存在於個人ServiceLayer中。此類被註冊爲服務,並具備一個構造函數,該構造函數接受應用程序的DbContext,它由依賴注入(DI)注入。注入的值存儲在私有字段_context中,AddReviewToBook方法可使用它來訪問數據庫。

  這會將新評論添加到數據庫中。它有效,但還有另外一種方法:可使用更多的DDD方法來構建它。

  選項2 - DDD樣式的實體類

  EF Core爲咱們提供了一個新的地方,能夠在實體類中添加更新代碼。EF Core有一個稱爲支持字段的功能,能夠構建DDD實體。經過支持字段,您能夠控制對任何關係的訪問。這在EF6.x中其實是不可能的。

  DDD提到聚合(前面提到過),而且全部聚合只能經過根實體中的方法進行更改,我將其稱爲訪問方法。在DDD術語中,評論是圖書實體的集合,所以咱們應該經過Book實體類中名爲AddReview的訪問方法添加評論。這會將上面的代碼更改成Book實體中的方法

1 public Book AddReviewToBook(ReviewDto dto) 2 { 3     var book = _context.Find<Book>(dto.BookId); 4  book.AddReview(dto.numStars, dto.comment, 5  dto.voterName, _context); 6  _context.SaveChanges(); 7     return book; 8 }

Book實體類中的AddReview訪問方法以下所示:

 1 public class Book  2 {  3     private HashSet<Review> _reviews;  4     public IEnumerable<Review> Reviews => _reviews?.ToList();  5     //...other properties left out  6  
 7     //...constructors left out
 8  
 9     public void AddReview(int numStars, string comment, 10         string voterName, DbContext context = null) 11  { 12         if (_reviews != null) 13  { 14             _reviews.Add(new Review(numStars, comment, voterName)); 15  } 16         else if (context == null) 17  { 18             throw new ArgumentNullException(nameof(context), 19                 "You must provide a context if the Reviews collection isn't valid."); 20  } 21         else if (context.Entry(this).IsKeySet) 22  { 23             context.Add(new Review(numStars, comment, voterName, BookId)); 24  } 25         else                                    
26  { 27             throw new InvalidOperationException("Could not add a new review."); 28  } 29  } 30     //... other access methods left out

   這種方法更復雜,由於它能夠處理兩種不一樣的狀況:一種是已經加載了評論,另外一種是沒有加載的。但它比原始案例更快,由於若是還沒有加載評論,它使用「經過外鍵建立關係」方法。

  由於訪問方法代碼在實體類中,因此若是須要它可能會更復雜,由於它將成爲您須要編寫的代碼(DRY)的惟一版本。在選項1中,您能夠在不一樣的地方重複相同的代碼,不管什麼時候您須要更新Book的評論集合。

  注意:我寫了一篇名爲「使用Entity Framework Core建立域驅動設計實體類」的文章,全部關於DDD樣式的實體類。這個主題有更詳細的介紹。我還更新了關於如何使用EF Core編寫業務邏輯以使用相同DDD樣式的實體類的文章。

爲何實體類中的方法不調用SaveChanges?在選項1中,單個方法包含全部部分:a)加載實體,b)更新實體,c)調用SaveChanges以更新數據庫。我能夠這樣作,由於我知道它是由網絡動做調用的,而這就是我想要作的。使用DDD實體方法,您沒法在實體方法中調用SaveChanges,由於您沒法肯定操做是否已完成。例如,若是您從備份中加載書籍,則可能須要建立書籍,添加做者,添加任何評論,而後調用SaveChanges以便將全部內容保存在一塊兒。

選項3:GenericServices庫

還有第三種方式。我注意到在我正在構建的ASP.NET應用程序中使用CRUD命令時有一個標準模式,在2014年,我創建了一個名爲GenericServices的庫,適用於EF6.x。在2018年,我爲EF Core構建了一個更全面的版本,名爲EfCore.GenericServices(請參閱EfCore.GenericServices上的這篇文章)。

這些庫並不真正實現存儲庫模式,而是充當實體類與前端所需的實際數據之間的適配器模式。我使用了原版EF6.x,GenericServices,它爲我節省了數月編寫枯燥的前端代碼。新的EfCore.GenericServices甚至更好,由於它可使用標準樣式的實體類和DDD樣式的實體類。

哪一個選項最好?

選項1(直接EF Core 代碼)具備最少的寫代碼,可是存在重複的可能性,由於應用程序的不一樣部分可能想要將CUD命令應用於實體。例如,當用戶經過更改內容時,您可能會經過ServiceLayer進行更新,但外部API可能不會經過ServiceLayer,所以您必須重複CUD代碼。

選項2(DDD樣式的實體類)將關鍵更新部分放在實體類中,所以代碼可供任何能夠獲取實體實例的人使用。事實上,由於DDD樣式的實體類「鎖定」對屬性和集合的訪問,若是他們想要更新Reviews集合,則每一個人均可以使用Book實體的AddReview訪問方法。因爲許多緣由,這是我想在將來的應用程序中使用的方法(請參閱個人文章,討論優缺點)。(輕微)降低是它須要一個單獨的加載/保存部分,這意味着更多的代碼。

選項3(EF6.x或EF Core GenericServices庫)是個人首選方法,特別是如今我已經構建了處理DDD樣式實體類的EfCore.GenericServices版本。正如您將在有關EfCore.GenericServices的文章中看到的,該庫大大減小了在Web /移動/桌面應用程序中編寫所需的代碼。固然,您仍然須要在業務邏輯中訪問數據庫,但這是另外一個故事

組織您的CRUD代碼

Rep/UoW模式的一個好處是它能夠將您的全部數據訪問代碼保存在一個地方。當直接交換使用EF Core時,您能夠將數據訪問代碼放在任何地方,但這使您或其餘團隊成員很難找到它。所以,我建議您明確規劃代碼的位置,並堅持下去。

圖顯示了分層或六邊形體系結構,僅顯示了三個組件(我遺漏了業務邏輯,在六邊形體系結構中,您將擁有更多組件)。顯示的三個組件是:

  • ASP.NET Core: 這是表示層,提供HTML頁面和/或Web API。這沒有數據庫訪問代碼,但依賴於ServiceLayer和BusinessLayers中的各類方法。
  • ServiceLayer: 它包含數據庫訪問代碼,包括查詢對象以及Create,Update和Delete方法。服務層使用適配器模式和命令模式來連接數據層和ASP.NET Core(表示)層。 (請參閱個人一篇關於服務層的文章)。
  • DataLayer: 它包含應用程序的DbContext和實體類。而後,DDD樣式的實體類包含容許更改根實體及其聚合的訪問方法。

 

 

 

 

 注意:前面提到的類庫GenericServices (EF6.x) 和 EfCore.GenericServices (EF Core) ,提供ServiceLayer的功能。在DataLayer和你的web/mobile/desktop應用之間起到 適配器命令模式
 
圖中我要指出的是,經過使用不一樣的程序集,一個簡單的命名標準(參見圖中粗體字Book)和文件夾,你能夠構建一個應用程序,你的數據庫代碼被隔離而且很容易找到。
 
 
 
EFCore 單元測試方法
 
 
  最後一部分來看看對使用EFCore的應用的單元測試。倉儲模式的有點之一就是在測試時可使用Mock替換倉儲。所以直接使用EFCore,就排除了Mock法(技術上你能夠Mock EFCore,但很難作好)。
  值得慶幸的是使用EFCore,如今你可使用In-Memory 數據庫模擬數據庫。In-memory數據庫建立更快,而且具備相同的初始狀態(好比 空),所以更容易寫單元測試。在個人文章: Using in-memory databases for unit testing EF Core applications,中你能夠找到如何實現的更多細節,包括一個名爲 EfCore.TestSupport 的nuget包,改包能夠提供使EFCore單元測試更便捷的方法。
 
 
  
 
結論
 
  我最後一個使用Rep/UoW模式的項目,已是2013年,從那之後再沒用過該模式。我嘗試了不少種方式,好比使用一個名爲 GenericServices 基於EF6.x的自定義類庫,如今更多的使用基於EFCore的標準查詢對象和DDD實體方法。他們編寫代碼很方便而且性能不錯,若是性能很差也很容易找到每一個數據性能問題所在。
  在我爲Manning出版社撰寫的書中,有一章,其中我演繹了一本「銷售」書籍的ASP.NET核心應用程序。該過程使用了查詢對象和DDD實體方法,並代表它能夠產生出色的數據庫訪問。(參閱個人文章「 一個EFCore性能調優的可用做示例」)
 
  我本身的工做遵循讀取的查詢對象和DDD樣式的實體類及其對CUD和業務邏輯的訪問方法。我確實須要在適當的應用程序中使用這些來真正知道它們是否有效,但它有前途。關注個人博客空間以得到更多的DDD方式的實體類框架,並受益於此,有時也可能會出一個新框架。
相關文章
相關標籤/搜索