当涉及不变量时,我们应该信任存储库吗?

Should we trust the repository when it comes to invariants?

在我正在构建的应用程序中,有很多场景需要 select 一组聚合来执行特定操作。例如,如果满足过期策略(只有一个),我可能必须将一堆 Reminder 聚合标记为过期。

我有一个 ReminderExpirationPolicy 域服务,它总是在发送提醒之前应用。该政策的作用类似于:

reminderRepository.findRemindersToExpire().forEach(function (reminder) {
    reminder.expire(clockService.currentDateTime());
});

过期策略当前是重复的,因为它作为 SQL 谓词存在于 SqlReminderRepository.findRemindersToExpire 方法和 Reminder.expire 聚合方法中。

这个问题的答案可能有强烈的意见(尽管肯定应该有利有弊 - 并且可能是一种广泛采用的做法),但我是否应该简单地相信 Reminder.expire 方法只会被调用为ReminderExpirationPolicy 过程的一部分,并相信存储库实现将 return 正确的过期提醒集,或者我是否也应该保护 Reminder 聚合本身内的不变量?

注意:我知道在单个事务中修改多个聚合是次优的并且会阻碍可扩展性,但在我的案例中这是最实用的解决方案。

should I simply trust that the Reminder.expire method will only get called as part of the ReminderExpirationPolicy process and trust that the repository implementation will return the correct set of reminders to expire or should I also protect the invariant within the Reminder aggregate itself?

简答:你落后了。您必须保护提醒聚合中的不变量;使用策略作为查询规范是可选的。

要认识到的关键是,在您的场景中,使用策略作为查询规范确实是可选的。消除持久性问题,你应该能够做到这一点

repo.getAll () { a -> a.expire(policy) ; }

聚合拒绝更改状态,因为这样做会违反业务不变性。

通常,这种区别之所以重要,是因为您可以通过查询存储库获得的任何数据都是陈旧的 - - 可能有另一个线程 运行ning 与你的线程重合,它在你的查询 运行 之后但在你的过期命令 运行s 之前更新聚合,并且如果那个巧合的工作是改变聚合以一种不再满足策略的方式,然后您的过期命令将稍后出现并威胁要违反不变量。

由于无论如何聚合都必须保护自己免受这种竞争条件的影响,因此检查查询中的策略是可选的。

当然,这仍然是个好主意 -- 在正常操作过程中,您不应该发送预期会失败的命令。

真正发生的事情,如果你稍微眯起眼睛,是expire命令和查询使用相同的策略,但是命令执行路径正在评估writeModel状态是否满足策略,查询正在评估readModel 状态是否满足策略。所以这并不是真正重复的逻辑 - 我们在每种情况下都使用不同的参数。

However, where my assumptions are different than yours is that from as far as I can see (assuming optimistic locking), even if the data become stale after aggregates are loaded and that I do not enforce the expiration rule within the aggregate, the operation would still fail because of a concurrency conflict.

是的,如果您确信处理命令的聚合版本与用于测试策略的版本相同,那么并发写入将为您提供保护。

另一个考虑因素是您正在失去封装的好处之一。如果策略检查发生在聚合中,那么可以保证每个可能使聚合过期的代码路径也必须评估策略。如果聚合依赖于调用者来检查策略("anemic domain" 模型的变体),则您无法获得该保证。