4

我有一个管理门票和客户的应用程序。一个客户拥有许多票。如果客户被删除,其票证也会被删除。这是查看对象是否应该是聚合的测试之一。我选择它们都应该是不在聚合根下的实体。我通常会加载并显示跨越许多客户的票证列表。我正在使用 SQL Server 以关系格式保存数据。

我的架构如下:

  1. ASP.NET MVC 应用程序。
  2. WCF 服务(门面)。此服务具有公共方法,例如 GetTicket、GetTickets、GetCustomer 和 GetCustomers。它直接为此使用存储库。它还有一个名为 Run 的公共方法。Run 接受一个命令,可以是 AssignTicket、ChangeTicketName 等。为了处理这些命令,Facade 被注入了服务、ITicketService 和 ICustomerService。这些服务上的方法用于执行来自外观的命令。这些服务使用 ITicketRepository 和 ICustomerRepository 来加载数据。
  3. 关系 SQL Server 数据库

我正忙于维护从门票到客户的参考资料。从关系上讲,Ticket 有一个映射到 Customers 表的 FK。当 UI 查看单个工单时,它会调用 Facade.GetTicket(id) 和 Facade.GetCustomers()。我能够显示所有票的详细信息以及它所属的客户姓名。这一切都很好,花花公子。但是,由许多不同客户拥有的大量门票呢?请记住,Ticket 仅包含一个引用客户的 Guid。我不想为要在 UI 中列出的每张票调用外观来获取客户的姓名。我的票上有一个名为 CustomerInfo( CustomerId 和 CustomerName ) 的值对象是否有效?使用sql语句加入这些数据是否有效?如果我不是 t 使用关系数据库?当客户的姓名被更改时怎么办?在系统中,Ticket 实际上也持有对多个其他实体的引用。

似乎 DDD 业务逻辑与 UI 需要显示的内容存在脱节。

4

1 回答 1

17

如果客户被删除,其票证也会被删除。这是查看对象是否应该是聚合的测试之一。

我不希望您的领域专家会说他们“删除客户”。客户可能会被禁止,或者他们的帐户被暂停,门票可能会被取消或转让,但“删除”这个词非常以 CRUD 为中心。理想情况下,您不应该真的硬删除数据(也许您可以将其归档到不同的数据存储中?),恕我直言,级联删除是一个危险的举动。让你的 ORM 来处理。

定义一个聚合是关于定义一个“一致性边界”,根据领域专家为该聚合定义的不变量,聚合的状态在该边界内是“正确的”。这就是您定义聚合边界的方式。

我正忙于维护从门票到客户的参考资料

请注意,域模型(实体和值对象被组装成聚合)与数据模型(实际上只是您选择的 OOP 语言中数据库的代码表示)之间存在差异。您的数据模型是您的表格,可能与您的领域模型完全不同。您的数据模型可以在不存在实际引用的表之间建立关系。换句话说,您可以有跨聚合边界的外键引用。这只是为了在数据库中强制执行参照完整性。如果您要使用 ORM 将这些表映射到类,您将没有“导航属性”,只有 ID 值。

似乎 DDD 业务逻辑与 UI 需要显示的内容存在脱节。

现在我们开始谈论 CQRS。DDD 是关于对问题域中的行为进行建模。域模型应该有很多行为(并且可能没有公共状态,我将继续讨论)。调用域逻辑时,您应该在一个事务中加载聚合、调用所需的行为并保存结果。因此,您的存储库应如下所示:

public interface IRepository<TEntity>
{
    TEntity Get(Guid id);
    void Save(TEntity item);
}

该实现将始终急切地加载所有内容,因为在您的 ORM 映射中,您不会包含对聚合之外的实体的引用。事实上,您现在可能已经认为文档数据库更适合存储聚合,恕我直言,您是对的。

但是你怎么查询呢?简单的。写一个查询。如果您正在实施 CQRS,您有一个使用您的存储库的精简读取层,并且使用您精心设计的实体及其 ORM 映射。只写一个查询。如果您使用的是 SQL Server,请考虑将超快速的 Linq to SQL 数据上下文组合在一起,或者使用 Dapper 或 Simple.Data。甚至只是 ADO.NET。关键是您的查询是关于从数据库中获取一些数据,而您不需要行为。

如果您将文档数据库用于聚合持久性,那么 CQRS 模式的合理实现可能会让您使用从域行为生成的事件构建一个完全独立的数据库。“读取存储”的选项是无穷无尽的。这里有一些想法:

  • 文档数据库的相同或其他实例,但具有预构建的视图模型。
  • SQL 数据库(您可以放弃第 3 范式并进行复制,因为您想优化读取性能而不是 OLTP 性能)。
  • 磁盘上预先编写的 HTML 文件?

Bonus Chatter(事件溯源)

假设您采用使用域中的事件来构建读取存储的方法,您可以在应用程序中使用精简读取层访问该读取存储(不要为存储库或大型 ORM 框架而烦恼。使用一点 Dapper 或其他简单的东西) ,您如何确保始终拥有包含正确数据的正确事件?好吧,如果您实际上从未存储聚合本身怎么办?如果您将聚合发出的事件存储在 Event Store 中,每个聚合都有一个事件流,该怎么办?当你想加载一个聚合时,你加载它的事件并在内存中重放它们以建立聚合的最新状态。

我不会更详细地介绍它,但值得研究 CQRS 和 Event Sourcing,因为它们是 DDD 的好伙伴,并且可以很好地配合使用。:)

于 2013-11-14T00:04:09.897 回答