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模式的优势(按好坏顺序,最好的优先)
- 隔离数据库代码:存储库模式的一大优势是您知道全部数据库访问代码的位置。此外,您一般将存储库拆分为多个部分,例如目录仓储,订单处理仓储等,这使得查找具备错误或须要性能调整的特定查询的代码变得容易。
这绝对是一大优势。
- 聚合:域驱动设计(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样式的实体类包含容许更改根实体及其聚合的访问方法。