DDD聚合间实体关联最佳实践咨询:跨聚合持有非根实体标识符
嘿,针对你提到的这个场景——一个聚合需要引用另一个聚合的非根实体标识符,再结合你现有的Product聚合(已经明确了Sku与Product的不变量约束:名称/部件编号同步更新、无重复Sku),下面是几个经过行业验证的最佳实践方案,既能守住DDD的聚合边界原则,又能满足业务需求:
1. 优先引用聚合根ID,通过根导航到非根实体
这是DDD最核心的原则之一:聚合根是外部访问聚合内部实体的唯一入口。除非业务上有极强的理由,否则其他聚合应该只持有目标聚合的根ID(比如productId),而不是非根实体的skuId。
举个例子:假设你有个OrderLine聚合需要关联Sku,那它应该存productId而非skuId。当需要获取Sku的信息时,通过Product聚合根来查询对应的Sku实例——这样所有对Sku的操作都必须经过Product根的校验,完美维护你设定的那些不变量(比如名称同步、无重复Sku)。
如果业务确实要求直接关联Sku,那一定要明确:这个引用是只读的,绝对不能通过这个ID直接修改Sku——所有修改必须回到Product聚合内部完成。
2. 给非根实体设计包含聚合根标识的全局唯一ID
如果必须持有非根实体的ID,那把这个ID设计成包含聚合根标识的格式,比如skuId可以是{productId}-{localSkuId}(比如PROD-001-SKU-001)。
这么做的好处很明显:
- 能快速定位到对应的聚合根,跨聚合查询时不会有歧义;
- 强化聚合边界,一眼就能看出这个Sku属于哪个Product,避免出现脱离根的孤立Sku引用。
3. 维护只读的非根实体投影(Projection)
如果另一个聚合需要频繁访问Sku的某些属性(而不只是ID),别直接去碰Product聚合的内部状态——可以通过领域事件同步一个只读的投影表(比如SkuProjection)。
比如,当Product聚合内部更新Sku的名称或部件编号时,发布SkuUpdated领域事件,然后由专门的投影服务监听这个事件,更新SkuProjection表。其他聚合需要查询Sku信息时,直接查这个投影表就行,完全不影响Product聚合的自治性。这样既保证了数据一致性,又不会破坏聚合边界。
4. 明确约定:外部对非根实体的引用是“弱关联”
一定要在团队内部明确规则:外部聚合持有的非根实体ID只是一个引用指针,不代表拥有任何修改权限。所有对该非根实体的修改操作,必须通过其所属的聚合根来执行,并且要在聚合内部的事务中保证不变量(比如你提到的Product和Sku名称同步更新)。
举个例子:假设你有个Inventory聚合持有skuId来记录库存数量。当需要修改Sku的名称时,Inventory聚合根本不需要参与这个事务——只有Product聚合内部在事务里完成Product和Sku的名称同步,然后通过事件通知Inventory投影更新对应的名称即可。
5. 绝对避免跨聚合的强事务操作
DDD非常不推荐跨聚合的分布式事务,因为这会彻底破坏聚合的自治性。如果你的业务场景中,另一个聚合的操作需要依赖Sku的状态,用最终一致性来实现,而不是强事务。
比如Order聚合创建订单时需要检查Sku库存,这时候先查Sku的投影数据确认库存充足,创建订单后发布OrderCreated事件,然后Inventory聚合监听事件再扣减库存。如果后续发现库存不足,通过补偿机制(比如取消订单、通知用户)来处理,而不是把Order和Product/Inventory聚合绑在一个事务里。
内容的提问来源于stack exchange,提问作者Steven

