假设您正在为论坛建模,并且您正在尽力使用DDD和CQRS(只是单独的阅读模型部分)。你有:
Category {
int id;
string name;
}
Post {
int id;
int categoryId;
string content;
}
每次创建新帖子时,都会引发域名事件PostCreated
。
现在,我们的观点想要预测每个类别的帖子数量。我的域名并不关心计数。我想我有两个选择:
PostCreated
并使用CategoryQueryHandler.incrimentCount(categoryId)
之类的内容递增计数。PostCreated
,并使用CategoryRepo.incrimentCount(categoryId)
之类的内容增加计数。同样的问题适用于所有其他计数,例如用户的帖子数量,帖子中的评论数量等。如果我不在任何地方使用这些计数,除了我的观点,我应该让我的查询处理程序采取照顾他们?
最后,如果我的某个域服务想要在类别中计算帖子数,我是否必须将count属性实现到类别域模型上,或者该服务只是使用读取模型查询来获取该计数或者替代存储库查询,例如CategoryRepo.getPostCount(categoryId)
。
答案 0 :(得分:7)
我的域名不关心计数。
这相当于说您没有任何需要或管理计数的不变量。这意味着没有计数有意义的聚合,因此计数不应该在您的域模型中。
按照您的建议将其实施为PostCreated事件的计数,或者通过对Post商店运行查询,或者......对您有用的任何事件。
如果我不在任何地方使用这些计数,除了我的观点,我应该让我的查询处理程序负责持久化吗?
那,或读取模型中的任何其他内容 - 但如果您的阅读模型支持select categoryId, count(*) from posts...
域名服务将希望拥有类别
中的帖子数
对于域名服务来说,这是一件非常奇怪的事情。域服务通常是无状态查询支持 - 通常它们由聚合使用以在命令处理期间回答一些问题。它们实际上并不强制执行任何业务不变量,它们只支持聚合这样做。
在两个级别上查询读取模型以便写入模型使用的计数没有意义。首先,读取模型中的数据是陈旧的 - 从查询中获得的任何答案都可以在您完成查询的那一刻和您尝试提交当前事务的那一刻之间发生变化。其次,一旦你确定过时的数据是有用的,没有特别的理由更喜欢在事务中观察到的陈旧数据之前的陈旧数据。也就是说,如果数据无论如何都是陈旧的,你也可以将它作为命令参数传递给聚合,而不是将其隐藏在域服务中。
OTOH,如果你的域需要它 - 如果存在一些约束计数的业务不变量,或者使用计数约束其他东西的那个 - 那么需要在某些聚合控制计数状态。
修改的
考虑两个并发运行的事务。在事务A中,聚合id:1运行需要对象计数的命令,但聚合不控制该计数。在事务B中,正在创建聚合id:2,这会更改计数。
简单的情况是,这两笔交易是通过运气发生在连续的区块中发生的
A: beginTransaction
A: aggregate(id:1).validate(repository.readCount())
A: repository.save(aggregate(id:1))
A: commit
// aggregate(id:1) is currently valid
B: beginTransaction
B: aggregate(id:2) = aggregate.new
B: repository.save(aggregate(id:2))
B: commit
// Is aggregate(id:1) still in a valid state?
我表示,如果aggregate(id:1)仍然处于有效状态,那么它的有效性不依赖于repository.readCount()的时效性 - 使用事务开始之前的计数本来也一样好。
如果aggregate(id:1)未处于有效状态,则其有效性取决于其自身边界之外的数据,这意味着域模型是错误的。
在更复杂的情况下,这两个事务可以同时运行,这意味着我们可能会看到在计数读取和聚合保存(id:1)之间发生聚合(id:2)的保存,像这样
A: beginTransaction
A: aggregate(id:1).validate(repository.readCount())
// aggregate(id:1) is valid
B: beginTransaction
B: aggregate(id:2) = aggregate.new
B: repository.save(aggregate(id:2))
B: commit
A: repository.save(aggregate(id:1))
A: commit
考虑为什么使用控制状态的单个聚合可以解决问题的原因可能也很有用。让我们改变这个例子,以便我们有一个包含两个实体的聚合....
A: beginTransaction
A: aggregate(version:0).entity(id:1).validate(aggregate(version:0).readCount())
// entity(id:1) is valid
B: beginTransaction
B: entity(id:2) = entity.new
B: aggregate(version:0).add(entity(id:2))
B: repository.save(aggregate(version:0))
B: commit
A: repository.save(aggregate(version:0))
A: commit
// throws VersionConflictException
编辑
提交(或保存,如果您愿意)可以抛出的概念是一个重要的概念。它强调该模型是与记录系统分开的实体。在简单的情况下,该模型可防止无效写入,并且记录系统可防止写入冲突。
实用的答案可能是让这种区别变得模糊。尝试对计数应用约束是设置验证的示例。除非集合的表示位于聚合边界内,否则域模型将遇到问题。但关系数据库往往擅长集合 - 如果您的记录系统恰好是关系存储,您可以通过使用数据库约束/触发器来维护集合的完整性。
如何处理此类问题应基于对特定故障的业务影响的理解。缓解而不是预防可能更合适。
答案 1 :(得分:0)
当涉及到我认为如果你真的需要将计数保存到数据库时必须考虑的事情数量。
在我看来,在大多数情况下,你不需要保存计数,除非他们的计算非常昂贵。所以我没有CategoryQueryHandler.incrementCount
或CategoryRepo.incrementCount
。
我只有一个PostService.getPostCount(categoryId)
来运行像
SELECT COUNT(*)
FROM Post
WHERE CategoryId=categoryId
然后在PostCreated
事件触发时调用它。