数据库

位置:IT落伍者 >> 数据库 >> 浏览文章

领域驱动设计的编码:数据聚焦型开发的技巧 - 第 3 部分


发布日期:2019年10月28日
 
领域驱动设计的编码:数据聚焦型开发的技巧 - 第 3 部分

下载代码示例

这是我这一系列讲座的最后一部分面向那些侧重数据的开发者向他们讲述域驱动的设计 (DDD) 所使用的一些更具挑战性的编码概念 作为使用 Entity Framework (EF) 的 Microsoft NET Framework 开发者并且在很长时间内从事数据优先(甚至数据库优先)的开发工作我曾极其痛苦地试图了解如何将我的技能与一些 DDD 实现技术相结合 即使我并没有在项目中使用完整的 DDD 实现(从客户端交互直到代码)我仍从多种 DDD 工具中获益匪浅

在最后一讲中我将介绍 DDD 编码的两个重要技术模式以及如何将其应用到我所使用的对象关系映射 (ORM) 工具 EF 中 在之前的讲座中我讲述了一对一关系 在这里我将论述 DDD 所侧重的单向关系以及它们如何影响您的应用程序 这种选择会导致困难的决策认识到您最好不使用 EF 执行的一些奇妙的关系 同时我还将讨论一下在聚合根与存储库之间平衡任务的重要性
从根开始生成单向关系

从我开始使用 EF 生成模型时双向关系已成为一种标准我不假思索地就使用这种关系 实现双向导航的功能是有意义的 在拥有订单和客户的情况下能够查看客户的订单是非常好的功能而且对于某个订单能够访问其客户数据也是非常便利的 无需多想我在订单及其明细项目之间也生成了双向关系 订单与明细项目之间的关系确实会有用 但是如果您停下来稍微思考一下您拥有明细项目且需要追溯到其订单的情况非常少见 我能想像到的这种情况之一是您在针对产品进行报告希望对通常哪些产品会一起订购进行分析或者分析中涉及到客户或发运数据 在这些情况下您可能需要从产品导航到包含该产品的明细项目然后回到订单 不过我仅在报告场景中看到这种情况而这种情况下我不太需要处理侧重于 DDD 的对象

如果我只需要从订单导航到明细项目什么方法可以最有效地描述我的模型中的此类关系?

如我所述DDD 侧重于单向关系 Eric Evans 的建议是尽可能地限制关系非常重要以及了解域可能会发现自然的方向偏离管理复杂的关系特别是依赖于 Entity Framework 来维持关联时绝对会导致许多混乱情况 我已经撰写了大量关于数据点的专栏专门解释了 Entity Framework 中的关联 不论消除何种程度的复杂性都有可能会带来好处

考虑一下我在这一系列中对于 DDD 使用过的简单销售模型在从订单到其明细项目的方向中确实出现了偏差 我无法想像不从订单开始创建删除或编辑明细项目的情况

如果您回顾一下我以前在该系列中生成的 Order 聚合订单并不控制明细项目 例如需要使用 Order 类的 CreateLineItem 方法来添加新的明细项目

public void CreateLineItem(Product product int quantity)
  {
   var item = new LineItem
   {
    OrderQty = quantity
    ProductId = productProductId
    UnitPrice = productListPrice
    UnitPriceDiscount = CustomerDiscount + PromoDiscount
   };
   LineItemsAdd(item);
  }
      

LineItem 类型具有 OrderId 属性但没有 Order 属性 这意味着可以设置 OrderId 的值但不能从 LineItem 导航到实际的 Order 实例

在这种情况下按照 Evans 的话说施加了遍历方向实际上我确保了能够从 Order 遍历到 LineItem但反方向则不行

这种方法有其含义不仅在模型中而且还在数据层内 我使用 Entity Framework 作为 ORM 工具它只需通过 Order 类的 LineItems 属性便足以很好地理解此关系 由于我碰巧遵循了 EF 的约定它能够理解 LineItemOrderId 是我的返回到 Order 类的外键属性 如果我为 OrderId 使用了其他名称对于 Entity Framework 来说这个过程就要复杂得多

但是在这一情形中我可以向现有订单添加新的 LineItem如下所示

orderCreateLineItem(aProductInstance );
  var repo = new SimpleOrderRepository();
  repoAddAndUpdateLineItemsForExistingOrder(order);
  repoSave();
      

order 变量现在表示带有已有订单和单个新 LineItem 的图形 已有订单来自数据库并且 OrderId 中已经有值但新的 LineItem 只有 OrderId 属性具有默认值该值为

我的存储库方法接受该订单图形将其添加到我的 EF 上下文中然后应用正确的状态如图 中所示

将状态应用到订单图形

public void AddAndUpdateLineItemsForExistingOrder(Order order)
  {
  _contextOrdersAdd(order);
  _contextEntry(order)State = EntityStateUnchanged;
  foreach (var item in orderLineItems)
  {
   // Existing items from database have an Id & are being modified not added
   if (itemLineItemId > )
   {
    _contextEntry(item)State = EntityStateModified;
   }
  }
  }
      

如果您不熟悉 EF 行为这里加以说明Add 方法会导致上下文开始跟蹤图形中的所有内容(订单和单个明细项目) 同时使用 Added 状态标记图形中的每个对象 但是由于此方法侧重于使用已有订单我知道该 Order 不是新的因此该方法通过将 Order 实例设置为 Unchanged 来修复其状态 它还检查任何已有 LineItems 并将其状态设置为 Modified从而在数据库中更新它们而不是作为新项插入 在更为具体的应用程序中我倾向使用模式以更确定地了解每个对象的状态不过在这个例子中我不希望过多涉及其他细节 (在 Rowan Miller 的博客上可以看到此模式的一个早期版本网址为 bitly/cLoo我们合着的书籍《Programming Entity Framework:DbContext》[OReilly Media ] 中提供了更新过的例子)

由于所有这些操作在上下文跟蹤对象时完成Entity Framework 还会神奇地在我的新 LineItem 实例中修复 OrderId 的值 因此在我调用 Save 时LineItem 知道了 OrderId 值为
不再使用神奇的 EF 关系管理 — 对于更新

出现这种好运气是因为我的 LineItem 类型碰巧遵循了 EF 的外键名约定 如果我将它命名为 OrderId 之外的名称例如 OrderFK则必须对类型进行一些更改(例如引入不需要的 Order 导航属性)然后指定 EF 映射 这就不如人意了因为您增加了复杂性而只是为了满足 ORM 有时候这种情况可能是必要的但如果不必要我希望能够避免

更简单的方法就是不再使用 EF 关系中奇妙的依赖关系而是控制代码中外键的设置

第一步是告知 EF 忽略此关系否则它将继续查找外键

下面是我在 DbContextOnModelBuilder 方法覆盖中使用的代码这样 EF 就不会关注该关系

modelBuilderEntity<Order>()Ignore(o => oLineItems);
      

现在我将自行控制关系 这意味着重构因此我将构造函数添加到需要 OrderId 和其他值的 LineItem 中这使得 LineItem 更像是 DDD 实体我非常满意 我还必须修改 Order 中的 CreateLineItem 方法以便使用该构造函数而不是对象初始值

显示了存储库方法的更新版本

存储库方法

public void UpdateLineItemsForExistingOrder(Order order)
  {
   foreach (var item in orderLineItems)
   {
    if (itemLineItemId > )
    {
     _contextEntry(item)State = EntityStateModified;
    }
    else
    {
     _contextEntry(item)State = EntityStateAdded;
     itemSetOrderIdentity(orderOrderId);
    }
   }
  }
      

请注意我不用再添加订单图形然后将订单的状态修复为 Unchanged 实际上由于 EF 不了解关系如果我调用了 contextOrdersAdd(order)它会添加 order 实例但不会像以前一样添加相关明细项目

相反我迭代图形的明细项目不仅将现有明细项目的状态设置为 Modified还将新明细项目的状态设置为 Added 我使用的 DbContextEntry 语法完成两项任务 在设置状态之前它会检查以了解上下文是否已意识到(或者跟蹤)该特定实体 如果没有则它在内部连接实体 现在它可以响应代码设置状态属性的情况 因此在该行代码中我连接并设置 LineItem 的状态

我的代码现在也遵循将 EF 用于 DDD 的另一个忠告不要依赖于 EF 来管理关系 EF 执行许多奇妙的功能在许多情形下大有裨益 多年来我很高兴地受益于其中 但是对于 DDD 聚合您实际上是希望在自己的模型中管理这些关系而不是依赖于数据层来为您执行必要的操作

由于我在为键(例如 OrderOrderId)使用整数并依赖于我的数据库来为这些键提供值时陷入困境我需要在存储库中为新聚合(例如带有明细项目的订单)进行一些额外的工作 我需要紧密地控制持久性这样才能使用旧式的插入图形模式插入订单获取数据库生成的新 OrderId 值将该值应用到新的明细项目然后将它们保存到数据库 这是必需的因为我已经中断了通常使用 EF 来完成这些奇妙操作的关系 您可以在下载的示例中查看我如何在存储库中实现这一点

经过了几年我终于准备好停止依赖数据库来创建我的标识符开始为我的键值使用可以在应用程序中生成和分配的 GUID 这使得我能够进一步将我的域与数据库分隔开
保持神奇的 EF 关系管理 — 对于查询

在我的模型中放弃 EF 关系后对于在上一情形中执行更新确实非常有益 但是我并不希望放弃 EF 的所有关系功能 从数据库查询时加载相关数据是我希望留用的功能之一 不论是预先加载延迟加载还是显式加载我乐于享受 EF 无需表示和执行附加查询就能获得相关数据的优点

这就是分离所关注概念的延伸观点发挥作用的地方 在遵循 DDD 设计规则的过程中相似的类采用不同的表示形式很常见 例如您可能使用设计用于客户管理上下文中的 Customer 类来完成此操作与之相对的是仅仅用于填充选取列表的 Customer 类该选取列表中只需要客户的姓名和标识符

还可以采用不同的 DbContext 定义 在检索数据的情形中您可能需要能意识到 Order 与 LineItems 之间关系的上下文这样可以从数据库中预先加载订单及其明细项目 但是在执行我前面所进行的更新时您可能需要显式忽略该关系的上下文这样可以更加精确地控制域

对于您可能采用软件解决的特定复杂问题子集这种情况的一个极端观点是称为命令查询职责分离 (CQRS) 的模式 CQRS 引导您考虑将数据检索(读取)和数据存储(写入)视为单独的系统需要不同的模型和体系结构 在一个小示例中我重点强调了对数据检索操作和数据存储操作采用不同的关系理解的优点这可以让您了解 CQRS 所能帮助您实现的功能 您可以从 CQRS Journey 这个非常好的资源了解 CQRS 的更多信息网址为 msd/library/jj
数据访问在存储库中进行而不是聚合根

现在我希望回顾一下并解决最后一个问题这个问题在我开始关注单向关系时困扰了我许久 (这并不是说再也没有关于 DDD 的问题而是说这是我在这一系列中的最后一个主题)对于我们数据库优先的思维方式这是一个关于单向关系的常见问题(使用 DDD)进行数据访问的确切位置在哪里?

EF 最初发布时唯一使用数据库的方法是对现有数据库实施反向工程 因此如前所述我习惯于每个关系都是双向的 如果数据库中的 Customers 和 Orders 表具有描述一对多关系的主键/外键约束那么我在模型中就会看到这种一对多关系 客户具有指向订单集合的导航属性 订单具有指向 Customer 实例的导航属性

在发展到模型优先和代码优先(可以描述模型和生成数据库)的过程中我继续使用该模式在关系的两端定义导航属性 EF 很好用映射更简单编码也更自然

因此在 DDD 中当我发现使用的 Order 聚合根能够意识到 CustomerId 甚至可能意识到完整的 Customer 类型但是却无法从 Order 导航回 Customer 时我非常沮丧 我首先提出的问题是如果我要查找某个客户的所有订单应该怎么办?我始终认为我应该能够这么做而且我习惯于依赖具有双向导航的访问

如果逻辑是从我的订单聚合根开始我该怎么解决这个问题? 最初我也错误地认为要通过聚合根来完成所有操作但这无济于事

实际的解决方案让我觉得自己愚不可及 在这里我分享了自己的愚蠢想法以防有人跟我一样误入歧途 能够帮助我解决问题的既不是聚合根也不是 Order 不过在侧重于 Order 的存储库中(也是我用于执行查询和持久性的存储库)我的问题的答案显而易见

public List<Order>GetOrdersForCustomer(Customer customer)
   {
    return _contextOrders
       Where(o => oCustomerId == customerId)
     ToList();
   }
      

该方法返回 Order 聚合根的列表 当然如果我在 DDD 的工作范围中创建此项并且我知道必须在特定上下文中使用它而不是以防万一那么我会不辞辛苦地将该方法放到存储库中有可能我会在报告应用程序或者类似应用程序中需要它但是在针对生成销售订单设计的上下文中则无必要
我的问题刚刚开始

在过去的几年中我对 DDD 有所了解此系列中我介绍的主题是我在数据层中使用 Entity Framework 时在理解或弄清如何实现的过程中遇到的最困难的问题 其中一些挫折源自我多年来思考软件的角度我习惯于从我的数据库工作方式来考虑问题 转变了这个角度之后豁然开朗因为这让我将重点放在眼前的问题上也就是我所设计软件的域问题 与此同时我确实需要找到良好的平衡因为在添加到我的解决方案中时可能会出现数据层问题

在使用 Entity Framework 将我的类直接映射回数据库时我重点思考的是其工作原理不过考虑到域逻辑与数据库之间可能存在另一层(或更多层)也很重要 例如您可能会有一个域逻辑与之交互的服务 在这种情况下从您的域逻辑中映射时数据层的重要性很低(或者根本不重要)这种问题现在由服务来处理

有很多方法可用于实现软件解决方案 即使我没有实现完整的端到端 DDD 方法(这需要对此相当得精通)我的整个工作仍然从通过 DDD 学习到的知识和技术中获益颇多

               

上一篇:sql server dba 面试笔试问题

下一篇:我是这么利用数据——1篮子鸡蛋