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