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
。这意味着任何过滤或排序都将在软件中完成,这是低效的。 - 性能 - 延迟加载:存储库通常返回一种类型的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
因为IQueryable
1 public IQueryableSortFilterPage 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
处理 创建,更新和删除 数据库访问的方法
查询对象处理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(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_reviews; 4 public IEnumerable 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样式的实体类包含允许更改根实体及其聚合的访问方法。