我有一个管理门票和客户的应用程序。客户拥有许多门票。如果客户被删除,其门票也将被删除。这是测试对象是否应该是聚合的测试之一。我选择它们都应该是不在聚合根下的实体。我将通常加载并显示跨越许多客户的票证列表。我使用SQL Server以关系格式保存数据。
我的架构如下:
我正忙着维护从门票到客户的引用。关系上,Ticket具有映射到Customers表的FK。当UI查看单个Ticket时,它会调用Facade.GetTicket(id)和Facade.GetCustomers()。我能够显示所有票证详细信息及其所属的客户名称。这一切都很好,花花公子。但是,许多不同客户拥有的大量门票如何呢?请记住,Ticket仅包含引用客户的Guid。我不想调用外观来获取我要在UI中列出的每张票的客户名称。我的Tickets是否对其上有一个名为CustomerInfo(CustomerId和CustomerName)的值对象有效?使用sql语句加入这些数据是否有效?如果我没有使用关系数据库怎么办?什么时候更改客户的名字?在系统中,Ticket实际上也持有对其他多个实体的引用。
似乎DDD业务逻辑与UI需要显示的内容存在脱节。
答案 0 :(得分:16)
如果客户被删除,其门票也将被删除。这是测试对象是否应该是聚合的测试之一。
我不希望您的域名专家说他们"删除客户"。客户可能被禁止,或者他们的帐户被暂停,门票可能会被取消或转移,但是这个词可以删除"删除"非常以CRUD为中心。理想情况下,你不应该真的很难删除数据(你可以将它存档到不同的数据存储,但也许?),恕我直言级联删除是一个危险的举动。让你的ORM处理。
定义聚合是关于定义"一致性边界",其中聚合的状态是"正确"根据领域专家为该聚合定义的不变量。这就是你定义聚合边界的方法。
我已经忙于维护从门票到客户的引用
要明确的是,域模型(其中实体和值对象被组合成聚合)和数据模型之间存在差异,数据模型实际上只是您所选OOP语言中数据库的代码表示。您的数据模型是您的表格,可能与您的域模型完全不同。您的数据模型可以在不存在实际引用的表之间建立关系。换句话说,您可以跨聚合边界使用外键引用。这只是为了在数据库中强制引用完整性。如果您使用ORM将这些表映射到类,则不会有#34;导航属性",只有ID值。
似乎DDD业务逻辑与UI需要显示的内容存在脱节。
现在我们开始讨论CQRS。 DDD是关于对问题域中的行为进行建模。域模型应该有很多行为(并且可能没有公共状态,我将会参与其中)。在调用域逻辑时,您应该在一个事务中加载聚合,调用所需的行为并保存结果。因此,您的存储库应如下所示:
public interface IRepository<TEntity>
{
TEntity Get(Guid id);
void Save(TEntity item);
}
实现将始终急切地加载所有内容,因为在您的ORM映射中,您不会包含对聚合之外的实体的引用。事实上,您现在可能已经认为文档数据库更适合存储聚合,而恕我直言,您是对的。
但你怎么查询?简单。写一个查询。如果您正在实施CQRS,那么您将拥有一个不会使用您的存储库的精简读取层,并且不会使用您精心设计的实体及其ORM映射。 只需撰写查询即可。如果您正在使用SQL Server,请考虑将超快Linq转换为SQL数据上下文,或使用Dapper或Simple.Data。甚至只是ADO.NET。关键是你的查询是关于从数据库中获取一些数据,而你不需要为此做出行为。
如果您正在使用文档数据库进行聚合持久性,那么CQRS模式的合理实现可能会使您可能使用从域行为生成的事件构建完全独立的数据库。选项&#34; Read Store&#34;是无止境的。以下是一些想法:
Bonus Chatter(活动采购)
假设您选择使用域中的事件来构建您在应用中使用精简读取层访问的读取存储(不要使用存储库或大量ORM框架。请使用Dapper或简单的东西),您如何确保始终拥有正确的事件以及正确的数据?那么,如果你真的没有自己存储聚合怎么办?如果您将聚合所发出的事件存储在事件存储中,并且每个聚合都有一个事件流,该怎么办?如果要加载聚合,可以加载它的事件并在内存中重放它们以构建聚合的最新状态。
我不再详细介绍,但是值得研究CQRS和事件采购,因为他们是DDD的好伙伴并且能够很好地一起玩。 :)