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).
My domain doesn’t care about count.
这相当于说您没有任何需要或管理计数的不变量.这意味着没有计数有意义的聚合,因此计数不应该在您的域模型中.
如您所建议的那样,将其实现为PostCreated事件的计数,或者通过对Post商店运行查询,或者……对您有用的任何事件.
If I don’t use these counts anywhere except my views should I just have my query handlers take care of persisting them?
那个,或读取模型中的任何其他东西 – 但如果您的阅读模型支持类似选择categoryId,count(*)来自帖子,那么您甚至不需要那么多……
domain services will ever want to have a count of posts in category
对于域名服务来说,这是一件非常奇怪的事情.域服务通常是无状态查询支持 – 通常它们由聚合使用以在命令处理期间回答一些问题.它们实际上并不强制执行任何业务不变量,它们只支持聚合这样做.
在两个级别上查询读取模型以便写入模型使用的计数没有意义.首先,读取模型中的数据是陈旧的 – 从查询中获得的任何答案都可以在您完成查询的那一刻和您尝试提交当前事务的那一刻之间发生变化.其次,一旦你确定过时的数据是有用的,没有特别的理由更喜欢在事务中观察到的陈旧数据之前的陈旧数据.也就是说,如果数据无论如何都是陈旧的,您也可以将其作为命令参数传递给聚合,而不是将其隐藏在域服务中.
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:2)的保存发生在读取计数和聚合保存(id:1)之间,就像这样
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
编辑
提交(或保存,如果您愿意)可以抛出的概念是一个重要的概念.它强调该模型是与记录系统分开的实体.在简单的情况下,该模型可防止无效写入,并且记录系统可防止写入冲突.
实用的答案可能是允许这种区分模糊.尝试将约束应用于计数是设置验证的一个示例.除非集合的表示位于聚合边界内,否则域模型将遇到问题.但关系数据库往往擅长集合 – 如果您的记录系统恰好是关系存储,您可以通过使用数据库约束/触发器来维护集合的完整性.
> Greg Young on Set Validation and Eventual Consistency
如何解决这样的问题应该基于对特定故障的业务影响的理解.缓解而不是预防可能更合适.