企业网站建设兴田德润怎么联系,家政网站建设方案,辽宁建设工程信息网登录不上去,做简单网站用什么软件概述在上一篇文章如何运用领域驱动设计 - 聚合中#xff0c;我们已经了解过领域驱动设计中一个很核心的对象-聚合。在现实场景中#xff0c;我们往往需要将聚合持久化到某个地方#xff0c;或者是从某个地方创建出聚合。此时就会使得领域对象与我们的基础架构产生紧… 概述在上一篇文章如何运用领域驱动设计 - 聚合中我们已经了解过领域驱动设计中一个很核心的对象-聚合。在现实场景中我们往往需要将聚合持久化到某个地方或者是从某个地方创建出聚合。此时就会使得领域对象与我们的基础架构产生紧密的耦合那么我们应该怎么隔绝这一层耦合关系使它们自身的职责界限更加清晰呢是的这就要用到我们今天要讲的内容 - 存储库。在很多地方我们喜欢叫它为仓储特别是在现有的AspNetCore应用中大量的应用都在引入Repository这种东西。那么究竟什么是存储库呢我们现在的使用方式是正确的吗它在领域驱动设计中又扮演着怎样的角色呢本文将从不同的角度来带大家重新认识一下“存储库”这个概念并且给出相应的代码片段本教程的代码片段都使用的是C#,后期的实战项目也是基于 DotNet Core 平台。直接看东西“少啰嗦直接看东西”。是的在本次的文章中居然居然居然 附带了Github的代码。本次代码其实是演示工作单元的实现但是它确实又结合了存储库的一些内容所以就在这里提供给大家参考。GitHub 地址点击直达哟:https://github.com/uoyoCsharp/MiCake.Uow.Easy这是一个工作单元的超简易版本您可以在github中看到它的描述和简介这里我就不再重复了。下一次的文章会对工作单元的实现进行解析和优化可能它就不属于 《如何运用领域驱动设计》 系列的正传系列了算个番外吧 (▽)。所以为了您不错过这一部分可以点击博客园右上角的关注有了动态之后就能够第一时间收到啦哦对了在Github代码中您可能会看到一个叫做MiCake米蛋糕的东西它是我们一步一步实现的DDD组件它会让您的 aspnet core 应用更轻松的融合DDD的思想并且它包含了我们该系列博文中所提到的所有战略组件以及它们之间的约束和处理。被广泛使用的仓储是的说存储库模式您可能还不能一下想到这是个什么东西但是一说到仓储您可能就会有一种豁然开朗的感觉“哦就是这个东西呀”。回顾一下您现有的AspNet Core项目是否已经引入了一个叫做Repository的对象并且它为您提供了与数据基础架构交互的方法。仿佛从某一天开始以往我们使用的BLL,DLL这种东西就逐渐开始消失了替换它们的是一个叫做Repository的东西。特别是从传统的AspNet演化为AspNetCore的阶段大量的应用都开始使用仓储了即使您在使用类似于EF这样的ORM框架。仓储是反模式吗关于存储厍模式存在非常多的误解和混淆许多人认为它是多余的仪式以及不必要的抽象它隐藏了底层持久化框架的能力。特别是当您正在使用类似于Entity FrameWork Core这样的ORM框架的时候您是否发现明明EFCore直接就可以实现的东西为什么我又在它的基础上套了一层而且这一层中我并没有执行任何逻辑只是简单的调用DbContextEF中的数据上下文这种东西。那为什么我不能直接调用DbContext呢是的这样的疑问相信不止很多同学都遇到了。所以在微软EF Core 3.x的官方教程中提到了这样的一句话该内容位于 ASP.NET Core 官方教程 - 数据访问 - 高级教程 中。那么我们真的不需要存储库这种东西吗答案是否定的至少在实践领域驱动设计的应用中。还记得在上一篇文章 如何运用领域驱动设计 - 聚合 中我们不止一次的提到了仓储这个概念因为它是为聚合而服务的而随着领域的深入使得领域模型越来越复杂的时候存储库将慢慢变成模型的扩展它将描述您每一个用例检索聚合的意图。思考一下您现有的应用中是否包含了一个全能的ORM框架比如EF那您引入仓储的原因是什么呢什么是存储库好吧这次的开篇太长了终于回到了正题什么是存储库 原著《领域驱动设计:软件核心复杂性应对之道》 中对存储库的有关解释为每种需要全局访问的对象类型创建一个对象这个对象就相当于该类型的所有对象在内存中的一个集合的“替身”。通过一个众所周知的接口来提供访问。提供添加和删除对象的方法用这些方法来封装在数据存储中实际插入或删除数据的操作。提供根据具体标准来挑选对象的方法并返回属性值满足查询标准的对象或对象集合所返回的对象是完全实例化的从而将实际的存储和查询技术封装起来。只为那些确实需要直接访问的Aggregate提供Repository。让客户始终聚焦于型而将所有对象存储和访问操作交给Repository来完成。国际惯例让我们来看看这一段话大致讲了什么。Repository提供了一个增删改查的操作它抽象了数据访问的部分。是的这个理解是很正确的因为这是存储库很重要的特性。所以有很多同学就开始疯狂的使用存储库了在项目中大量的引入Repository而嵌套于ORM之上。但是 我们忽略了上面的其它几点“确实需要直接访问的Aggregate提供Repository” “提供根据具体标准来挑选对象” 。 注意这很重要下文将一一为大家解释。如何运用存储库存储库是为聚合提供操作这一点是非常关键的存储库是为聚合而服务的。有关于聚合的部分可以查看上一篇文章 如何运用领域驱动设计 - 聚合。为什么呢它一定要为聚合服务 它不能为实体服务吗 因为聚合是一个整体在上一文中我们已经说过了当凝练出一个聚合根的时候就证明外界只能通过聚合根来访问聚合内的实体所以我们没有理由在任何一个地方需要穿透聚合根去访问实体这是错误并且没有意义的。那么很自然的就可以衍生出我们什么时候需要使用存储库单独来提取实体呢好像确实没有。不过有的同学会说了我在做**报表的时候我就确实需要只访问某个实体呀那么请思考两个点1、该实体是否需要提升为聚合根。 2、如果是广泛查询的报表可能并不需要通过仓储来获取对象需要专门的查询框架来完成。因此我们建立出来的仓储的接口可能是这个样子的复制代码public interface IRepositoryTAggregateRootwhere TAggregateRoot : class, IAggregateRoot
{
}此处使用了C#的接口泛型约束将仓储的服务者约束为了一个聚合根。该代码在上文介绍的 MiCake 中您也可以看到。存储库对外提供哪些方法到目前为止我们已经知道一个存储库至少应该包含根据ID来对聚合的增删改查方法可能有一些时候我们只需要查不需要删。但是就一个通用的存储库来说它能具有这些方法是毫无疑问的。所以我们的仓储接口可以增加一些通用方法复制代码public interface IRepositoryTAggregateRoot where TAggregateRoot : class, IAggregateRoot
{TAggregateRoot Find(TKey Id);void Add(TAggregateRoot aggregateRoot);void Update(TAggregateRoot aggregateRoot);void Delete(TAggregateRoot aggregateRoot);
}存储库是一个明确的约定虽然存储库提供了基础的提取方法但是在许多场景下我们可能更需要根据某种条件来从数据库中读取对应的模型并将其转换为领域聚合对象。比如在之前的一篇文章 如何运用领域驱动设计 - 领域服务 中就有一个地方出现了使用存储库的情况我们需要根据当前的位置来查找附近的饭店:复制代码var nearbyRestaurants restaurantRepository.GetNearbyRestaurant(currentAddress);采用了类似于这样的写法。该存储库对外提供了一个GetNearbyRestaurant的方法出来外界的应用服务就可以通过该方法来获取对应的结果。这是一个很好的方法签名我们通过传入一个当前位置就能够获取到附近的饭店。通过阅读存储库提供出来的方法就能理解领域中的检索意图从侧面也反应了领域的某些用例。但是现在有部分的同学热爱另外一种写法通过Lambda作为方法参数传递给下层的ORM框架来进行查询。该方法签名类似于这样复制代码IQueryableTEntity FindMatch(params ExpressionFuncTEntity, object[] propertySelectors);这样做的好处是所有的存储库都可以复用这个接口以后所有的查询都可以通过使用该方来来完成而不需要再去单独写各种Find方法。通过返回一个IQueryable对象甚至可以将业务查询逻辑直接放到应用层这样想怎么操作就怎么操作。请注意这非常的危险 您可能会问了“我平时所接触的框架或者仓储不都是这样写的吗可以实现我任何的业务查询爽歪歪。” 但是这样写正在逐渐丧失存储库原有的作用。回到开篇提到的一个问题假如使用了EF这样的ORM框架为什么还需要嵌套一层仓储呢 而现在您可能正在这样做开放且灵活的约定再加上延迟的IQueryable对象让仓储层完全丧失了原有的作用它反而成了负担为什么不直接使用DbContext对象呢 为了仓储而使用仓储为了看上去像DDD而DDD那不是自己骗自己吗所以请尽量避免在您的存储库中去写这种灵活而没有任何明确检索意图的方法接口它可能确实会使您减少代码书写量但随着项目的复杂和领域对象的逐渐增多它会使您的应用层越来越迷惑。所以存储库中所提供的应该是具有明确约定的方法。这里我摘抄了 领域驱动设计模式、原理与实践 中的一段话我觉得它的描述非常好存储库不是一个对象。它是一个程序边界以及一个明确的约定在其上命名方法时它需要的工作量与领域模型中的对象所需的工作量一样多。你的存储库约定应该是特定的以及能够揭示意图并对领域专家具有意义。具有领域意图的东西我们都应该领域层而类似于数据库的访问实现这类基础架构应该放在基础设施层。所以可以看出我们抽象出来的仓储接口是应该放在领域层的而仓储的实现可以放在基础设施层 。这个问题有很多小伙伴可能迷惑了很久我上次看到一位同学将仓储接口放在了应用层因为它认为和领域无关认为仓储只是一个提供增删改查的东西。而这也是因为忽略了仓储也是领域行为的一部分的结果。审计追踪在前面讲值对象的文章中有一位园友问了我一个问题有一点是类似于CreateDateCreateUser这种审计信息我们许多时候都会依附在领域对象身上那么是不是应该通过领域服务来做处理呢其实不然它们虽然对我们有参考意义其实并没有在捕获领域需求时捕获出来。往往这类审计信息都是我们按照以往的开发经验所提炼出来的所以它们对领域对象的影响很小。那么我们又很需要去操作它们比如持久化一个聚合根的时候为它附带上创建时间这样便于我们去追踪它的一些记录。而此时就可以依赖我们的存储库来完成了当聚合根在领域服务或者领域用例中已经完成了操作时将它传递给存储库持久化之前就可以让存储库为它加上审计信息。汇总存储库有时还可以拥有对集合汇总的功能比如上面我们提到了饭店的一个仓储可能我们在系统中想得到我系统中到底有多少个饭店或者在某个区域有多少个饭店。这种汇总的功能您也可以交给存储库来完成这也完美的符合“存储库”中“库”的含义。但还是请注意这些汇总的方法依然得拥有一个明确的约定格式不要因为是汇总就将存储库写的开放而过于灵活。有时候您可能需要形成一个报表该报表它包含了各个领域对象的汇总情况。在此时该汇总的职责可能并不属于存储库了它需要您使用另外的方式来完成该内容可以看下面的小节。不要使用过多特性干扰您的领域对象在持久化的过程中现在的主流方式我们都会依赖于类似于EF Core这样的ORM框架来完成。当我们需要将领域对象转换为数据库的数据对象可以理解为表吧时可能有时候就需要表明什么是主键什么具有约束等情况。如果您正在使用EF Core对于 Data annotations 您可能再熟悉不过了它提供了通过特性来标记的写法完成映射关系复制代码public class CustomerWithoutNullableReferenceTypes
{public int Id { get; set; }[Required] // Data annotations needed to configure as requiredpublic string FirstName { get; set; }[Required]public string LastName { get; set; } // Data annotations needed to configure as requiredpublic string MiddleName { get; set; } // Optional by convention
}该代码摘自 EF Core 教程 - 必需和可选属性这种写法很诱人因为只需要简单的在属性上增加一个特性就完成了配置。但是这些特性对领域对象其实是没有必要的它可能还会干扰您的阅读。因为我们在构建领域对象的时候不应该考虑数据持久层面的问题而构建出来的领域对象也应该保持干净。在EFCore中为我们提供了Fluent API的方式来配置模型该方式可以很好的让领域对象保持干净。假如您没有使用EFCore另外的ORM框架也一定会为您提供类似于这样的配置方法。不要为了显示而使用存储库很多场景我们可能需要提供一个丰富的界面或者一个完整的报表。比如在一个界面上显示了某个聚合中的一个实体的信息又或者在报表中提供了各个实体和值对象的汇总和特定信息。在这个情况下仓储可能就显得有点隆重了我必须要通过A、B、C……仓储获取所有聚合A,B,C然后再来处理汇总信息。要么就是将存储库的规则打破直接查询利用EF Core查询出IQueryable集合对象然后一顿输出猛如虎来达到效果。记住不要为了使用DDD而让您的开发变得复杂而不顺手在这个时候我们甚至可以不使用存储库我们可以利用另外的框架来直接查询数据库也或者是使用ADO.NET运用原生Sql来达到查询的效果。还有一种方法是将查询单独划分为应用系统的一个分支将修改命令单独划分为另外一个分支来操作领域对象。这是DDD的另外一种模式可能您已经听过它的英文简写了CQRS。该模式的内容会在后期的文章中为大家介绍MiCake后期也会增加对CQRS的支持。工作单元在持久化的过程中我们必须保证一个聚合的所有的部分一同保持成功或者一个用例的多个聚合同时保存成功在分布式中可能只能追求最终一致性。所以我们必须得保证存储库是有事务的而事务的管理是由工作单元来提供的。这也是为什么存储库每次都和工作单元这一概念一同出现。下面引用了微软AspNet中的一张图方便您理解工作单元UnitOfWork该图片选取自 微软 AspNet 教程 - 实现存储库和工作单元模式本章附带了关于工作单元和仓储接口的演示代码关于工作单元的部分会在下篇文章为大家介绍。持久化中的困难关于持久化的问题已经是一个老生常谈的话题了在一篇关于值对象的博文中就已经说明了这个问题。如何将领域对象如何通过ORM来持久化到数据库在回答这个问题之前我们得先理解一下什么是领域模型和数据模型领域模型是问题域的抽象富含行为和语言数据模式是一种包含指定时间领域模型状态的存储结构ORM可以将特定的对象C#的类映射到数据模型。数据模型和领域模型无关存储库的作用就是保持这两个模型的独立并且不让它们变得模糊不清。也就是说我们在设计领域模型时应该仅仅关心领域中的对象千万不要让框架比如ORM来驱动你的设计。关于这一点给了我一点灵感既然我们只关心领域对象那在持久化的时候能不能单独建立一个持久化对象专门供ORM去映射到数据库而仓储负责了聚合创建和保存的过程在这个过程中让仓储自动去完成领域对象到持久化对象的转换就行了。关于这个实现方法准备在下下一起番外系列中为大家介绍可能MiCake也会默认支持该方法来完成领域对象的持久化任务。当然因为是番外的系列所以为了您不错过这一部分可以点击博客园右上角的关注。 好吧我又把上面的话不要脸的又复制了一遍 (ง •_•)ง总结本次我们介绍了有关领域驱动设计中“存储库”的内容我们知道了什么是存储库以及如何去使用一个存储库。由于存储库属于一个很基础的概念所以在该章节中我们没有使用旅行记账的案例来为大家介绍。而更多的是希望大家能够理解使用存储库的场景和规范毕竟现在存储库模式是很常用的一个模式如果只知其然而不知其所以然的去使用存储库模式不仅体验不到它的益处反而会让代码变得越来越复杂。