DDD:在单个事务中创建具有共享生命周期的多个聚合
DDD: creating multiple aggregates with a shared life-cycle in a single transaction
据我所知,我知道每个事务只能修改一个聚合的一般规则,主要是为了并发性和事务一致性问题。
我有一个用例,我想在单个事务中创建多个聚合:RestaurantManager
、Restaurant
和 Menu
。它们看起来像一个单一的聚合体,因为它们的生命周期一起开始和结束:在域内创建没有 Restaurant
的 RestaurantManager
没有意义,反之亦然; Restaurant
和 Menu
也是如此。此外,如果Restaurant
或RestaurantManager
被删除(未注册),则应将它们一起删除。
但是,我将它们拆分为单独的聚合,因为一旦创建,它们就会单独更新,维护自己的不变量,我不想将它们全部加载到内存中只是为了更新一个 属性 例如 Restaurant
。
唯一将它们联系在一起的是它们的生命周期。
我的问题是,这是否代表可以违反每个事务只能在单个聚合上运行的“规则”。
我还想知道我是否应该通过让每个聚合根持有它所依赖的聚合根的标识符来在域模型中强制执行它们的共享生命周期,即让 Restaurant
要求MenuId
作为构造函数参数,对于 Menu
和 RestaurantId
也是如此,因此两者都不能在没有另一个的情况下创建。然而,这仍然不会强制应用程序服务将它们保存在一起,因为它可以在内存中创建它们,然后只保存 Menu
,例如。
"only a single aggregate should be modified per transaction"
创建时的争用并不重要。您可以在单个事务中创建 多个 AR 而不会出现问题,因为唯一可能发生冲突的其他操作是另一个重复的创建过程。
避免在单个事务中涉及多个 AR 的另一个原因是模块之间的耦合,但您始终可以使用同步调度的域事件来保持松耦合。
至于删除,让它最终保持一致可能问题不大。 Restaurant
关闭而 RestaurantManager
仍保持短时间注册真的很重要吗?
你问这个问题说明你的系统不是分布式的?如果您的系统是 运行 单个数据库服务器并由少数人使用,则最终一致性可能会使您实际上不需要的可伸缩性变得更加复杂。
从简单开始并根据需要进行重构,但跨越 AR 边界并不是应该始终如一地完成的事情,否则你的边界显然是错误的。
此外,如果您想传达 RestaurantManager
不能凭空产生并错误地与无效的 RestaurantId
相关联,您可能需要查看您的通用语言以获取指导。
例如
“A RestaurantManager
已注册给定 Restaurant
”:不确定它是否真的符合您的 UL,但这只是为了示例。
RestaurantManager manager = restaurant.registerManager(...);
这显然会增加耦合并可能影响性能,但它与 UL 保持一致并且更难以滥用模型。另请注意,对于单个数据库,您可以强制执行参照完整性,以处理这些无趣的参照约束。
恕我直言,您的要求是 DDD 中的一个非常正常的用例。始终有多个聚合协同工作以支持应用程序,并且它们在其生命周期中相互关联。但建模概念仍然适用。让我尝试借助一些 DDD 规则来解释您的模型的外观:
聚合是事务边界
聚合确保在任何时候都不会破坏业务不变性。这意味着如果您将多个聚合串在一起作为一个事务的一部分,则必须将它们全部加载到内存中以进行验证。
当您的应用程序 data-rich 并将数据存储在数据库集群中时,这尤其是一个问题 - 分区,分布式(想想 Mongo 或 Elasticsearch)。作为单个事务的一部分,您将遇到从可能不同的集群加载数据的问题。
聚合已全部加载
聚合及其关联的数据对象被完整加载到内存中。这意味着交易中不必要的对象(例如餐厅下个月的时间表)可能会加载到内存中。就其本身而言,这不是问题。但是当多个聚合在一起时,需要考虑加载到内存中的数据量。
聚合通过其唯一标识符相互引用
这个很简单,意味着每个聚合都通过标识符存储其引用的聚合,而不是将另一个聚合的数据包含在其中。
聚合之间的状态变化是通过域事件处理的
如果您希望一个聚合中的状态更改在其他聚合中具有 side-effects,您可以发布域事件,订阅者会在后台处理其他聚合中的更改。这就是您处理级联删除要求的方式。
通过遵循这些规则,您实质上是一次放大一个聚合并确保复杂性保持在较低水平。当您串起多个聚合时,虽然在第一天它是清晰易懂的,但最终,应用程序往往会变成一个大泥球,因为依赖性和不变量开始相互交叉。
正如@plalx 所指出的,当创建 交易聚合时,争用并不重要,因为它们尚不存在,因此无法参与在争论中。
至于在域中强制执行多个聚合的相互生命周期,我认为这是应用层(即应用服务或用例)的责任。
也许我的想法更接近于 Clean 或 Hexagonal 架构,但我认为尝试将每一个业务规则都推入“领域模型”是不可能的,甚至是不明智的。对我来说,域模型的重点是将问题域划分为小块(聚合),这些小块封装了一起变化的共同业务 data/operations,但是应用层有责任正确使用这些聚合以实现业务的最终目标(即整个应用程序),包括聚合之间的中介操作和控制它们的生命周期。
因此,我认为这东西属于应用程序服务。也就是说,在每个用例中经常更新多个聚合可能是域边界不正确的标志。
据我所知,我知道每个事务只能修改一个聚合的一般规则,主要是为了并发性和事务一致性问题。
我有一个用例,我想在单个事务中创建多个聚合:RestaurantManager
、Restaurant
和 Menu
。它们看起来像一个单一的聚合体,因为它们的生命周期一起开始和结束:在域内创建没有 Restaurant
的 RestaurantManager
没有意义,反之亦然; Restaurant
和 Menu
也是如此。此外,如果Restaurant
或RestaurantManager
被删除(未注册),则应将它们一起删除。
但是,我将它们拆分为单独的聚合,因为一旦创建,它们就会单独更新,维护自己的不变量,我不想将它们全部加载到内存中只是为了更新一个 属性 例如 Restaurant
。
唯一将它们联系在一起的是它们的生命周期。
我的问题是,这是否代表可以违反每个事务只能在单个聚合上运行的“规则”。
我还想知道我是否应该通过让每个聚合根持有它所依赖的聚合根的标识符来在域模型中强制执行它们的共享生命周期,即让 Restaurant
要求MenuId
作为构造函数参数,对于 Menu
和 RestaurantId
也是如此,因此两者都不能在没有另一个的情况下创建。然而,这仍然不会强制应用程序服务将它们保存在一起,因为它可以在内存中创建它们,然后只保存 Menu
,例如。
"only a single aggregate should be modified per transaction"
创建时的争用并不重要。您可以在单个事务中创建 多个 AR 而不会出现问题,因为唯一可能发生冲突的其他操作是另一个重复的创建过程。
避免在单个事务中涉及多个 AR 的另一个原因是模块之间的耦合,但您始终可以使用同步调度的域事件来保持松耦合。
至于删除,让它最终保持一致可能问题不大。 Restaurant
关闭而 RestaurantManager
仍保持短时间注册真的很重要吗?
你问这个问题说明你的系统不是分布式的?如果您的系统是 运行 单个数据库服务器并由少数人使用,则最终一致性可能会使您实际上不需要的可伸缩性变得更加复杂。
从简单开始并根据需要进行重构,但跨越 AR 边界并不是应该始终如一地完成的事情,否则你的边界显然是错误的。
此外,如果您想传达 RestaurantManager
不能凭空产生并错误地与无效的 RestaurantId
相关联,您可能需要查看您的通用语言以获取指导。
例如
“A RestaurantManager
已注册给定 Restaurant
”:不确定它是否真的符合您的 UL,但这只是为了示例。
RestaurantManager manager = restaurant.registerManager(...);
这显然会增加耦合并可能影响性能,但它与 UL 保持一致并且更难以滥用模型。另请注意,对于单个数据库,您可以强制执行参照完整性,以处理这些无趣的参照约束。
恕我直言,您的要求是 DDD 中的一个非常正常的用例。始终有多个聚合协同工作以支持应用程序,并且它们在其生命周期中相互关联。但建模概念仍然适用。让我尝试借助一些 DDD 规则来解释您的模型的外观:
聚合是事务边界
聚合确保在任何时候都不会破坏业务不变性。这意味着如果您将多个聚合串在一起作为一个事务的一部分,则必须将它们全部加载到内存中以进行验证。
当您的应用程序 data-rich 并将数据存储在数据库集群中时,这尤其是一个问题 - 分区,分布式(想想 Mongo 或 Elasticsearch)。作为单个事务的一部分,您将遇到从可能不同的集群加载数据的问题。
聚合已全部加载
聚合及其关联的数据对象被完整加载到内存中。这意味着交易中不必要的对象(例如餐厅下个月的时间表)可能会加载到内存中。就其本身而言,这不是问题。但是当多个聚合在一起时,需要考虑加载到内存中的数据量。
聚合通过其唯一标识符相互引用
这个很简单,意味着每个聚合都通过标识符存储其引用的聚合,而不是将另一个聚合的数据包含在其中。
聚合之间的状态变化是通过域事件处理的
如果您希望一个聚合中的状态更改在其他聚合中具有 side-effects,您可以发布域事件,订阅者会在后台处理其他聚合中的更改。这就是您处理级联删除要求的方式。
通过遵循这些规则,您实质上是一次放大一个聚合并确保复杂性保持在较低水平。当您串起多个聚合时,虽然在第一天它是清晰易懂的,但最终,应用程序往往会变成一个大泥球,因为依赖性和不变量开始相互交叉。
正如@plalx 所指出的,当创建 交易聚合时,争用并不重要,因为它们尚不存在,因此无法参与在争论中。
至于在域中强制执行多个聚合的相互生命周期,我认为这是应用层(即应用服务或用例)的责任。
也许我的想法更接近于 Clean 或 Hexagonal 架构,但我认为尝试将每一个业务规则都推入“领域模型”是不可能的,甚至是不明智的。对我来说,域模型的重点是将问题域划分为小块(聚合),这些小块封装了一起变化的共同业务 data/operations,但是应用层有责任正确使用这些聚合以实现业务的最终目标(即整个应用程序),包括聚合之间的中介操作和控制它们的生命周期。
因此,我认为这东西属于应用程序服务。也就是说,在每个用例中经常更新多个聚合可能是域边界不正确的标志。