EF Core已經出2.1版,開始考慮使用據傳性能調優已經接近C++的.Net Core寫新項目。想要拋棄以前使用asp.net那種sql腳本的碼代碼方式。同時找了一些開源的項目,比如ABP,SimpleCommerce。
其中ABP項目大而全,封裝了很多模式,但文檔更多是描述如何使用,如果自己不去看代碼很容易不知所雲。ABP項目基於Ioc(castle windsor)的動態代理特性實現了及其靈活的模塊化方案,可以在運行過程中加載項目並初始化。同時ABP封裝了自身的UnitofWork方式,結合了IoC框架太多特性(castle windsor)。比如使用了該框架動態代理的實現,在業務執行之前插入UnitofWork相關邏輯。
而SimpleCommerce則利用了AutoFac以及asp.netcore的特性實現了模塊化。對於倉儲模式涉及的比較少。對於項目解耦可以說是一個簡單的示例。
那么究竟要怎么開始EFCore項目?近期看到一篇,比較實用簡單。
不,倉儲或者說unit-of-work模式(簡稱 Rep/UoW)不再使用於EF Core。EF Core 已經實現了Rep/UoW模式,因此在ef core之上再抽象一層Rep/UoW模式,並無幫助。
比較明智的選擇是直接使用EF Core,這樣你可以使用EF Core 的全部功能,以實現高性能的數據庫訪問。
本文的目的:
本文關注一下幾點:
- 人們如何評價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。
場景設定
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模式的優點(按好壞順序,最好的優先)
- 隔離數據庫代碼:存儲庫模式的一大優點是您知道所有數據庫訪問代碼的位置。此外,您通常將存儲庫拆分為多個部分,例如目錄倉儲,訂單處理倉儲等,這使得查找具有錯誤或需要性能調整的特定查詢的代碼變得容易。
這絕對是一大優點。
- 聚合:域驅動設計(DDD)是一種設計系統的方法,它建議您有一個根實體,並將其他關聯實體聚合於它。我在“Entity Framework Core in Action”一書中使用的示例是一個書實體,其中包含評論實體的集合。那些評論只有在連接到書本的時候才有意義。因此DDD建議你只能通過書實體來修改評論。Rep/UoW模式通過提供向Book Repository添加/刪除評論的方法來實現此目的。
- 隱藏復雜的T-SQL命令:有時你需要繞過智能的EF Core的直接使用T-SQL。這種類型的訪問應該從較高層隱藏,但很容易找到以幫助維護/重構。應該指出,Rob Conery的文章 命令/查詢 對象也可以處理這個問題。
- 易於模擬/測試:模擬單個倉儲很容易,這使得單元測試代碼更容易訪問數據庫。這種情況在幾年前就已經存在,但是現在還有其他解決這個問題的方法,我將在后面介紹。
Rep / UoW模式的缺點(按好壞順序,最壞的優先)
前三項都是關於性能。我並不是說你寫不出高效的Rep/UoW 模式,但他的確很難,我看到過很多種實現都帶有性能問題(包括微軟的舊Rep/UoW實現)
這是我在Rep / UoW模式中發現的缺點列表:
- 性能 - 排序/過濾:在微軟的舊(2013)Rep/UoW 實現中,有個GetStudents方法,返回 IEnumerable<Student>。這意味着任何過濾或排序都將在軟件中完成,這是低效的。
- 性能 - 延遲加載:存儲庫通常返回一種類型的IEnumerable / IQueryable結果,例如Microsoft示例中的Student實體類。假設您想顯示學生所擁有的關系中的信息,例如他們的地址,要怎么辦?在這種情況下,倉儲中最簡單的方法是使用延遲加載來讀取學生的地址實體。問題是延遲加載會導致數據庫為其加載的每個延遲加載數據作一次查詢,這比將所有數據庫訪問組合到一個數據庫查詢中要慢。
- 性能 - 更新:許多Rep / UoW實現嘗試隱藏EF Core,並且這樣做不會充分利用其所有功能。例如,Rep / UoW將使用EF Core 的 Update方法更新實體,該方法保存實體中的每個屬性。然而,使用EF Core的內置更改跟蹤功能,它只會更新已更改的屬性。
- 過於通用: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樣式的實體類包含允許更改根實體及其聚合的訪問方法。