DDD中多对多关联的Parent与Child(均为AggregateRoot)如何建模实现双向访问?
DDD中Parent与Child多对多关系的代码建模方案
嘿,这个多对多聚合根的场景在DDD里确实需要仔细处理,既要保证聚合的边界完整性,又要满足双向访问的需求。我来给你梳理一套落地的建模方案,附代码示例:
核心原则:聚合根的边界约束
因为Parent和Child都是AggregateRoot,DDD要求每个聚合根的内部状态只能由自身维护,跨聚合的关联不能直接持有实体引用(否则会破坏聚合的封装性和一致性边界)。正确的做法是:
- 每个聚合根持有对方的ID值对象集合,而非实体引用
- 关联的建立/解除通过领域服务统一处理,保证双向关联的一致性
- 双向访问通过仓储根据ID集合查询对应的聚合根
代码实现(以Java为例)
1. 定义ID值对象
用值对象封装ID,提升表达力并统一校验逻辑:
// ParentId 值对象 public record ParentId(String value) { public ParentId { if (value == null || value.isBlank()) { throw new IllegalArgumentException("Parent ID cannot be empty"); } } } // ChildId 值对象 public record ChildId(String value) { public ChildId { if (value == null || value.isBlank()) { throw new IllegalArgumentException("Child ID cannot be empty"); } } }
2. 实现Parent聚合根
内部维护Child的ID集合,对外提供只读访问,通过内部方法修改集合:
public class Parent implements AggregateRoot<ParentId> { private final ParentId id; private String name; // 仅持有Child的ID,而非实体 private final Set<ChildId> childIds = new HashSet<>(); // 构造函数:强制初始化必要属性 public Parent(ParentId id, String name) { this.id = id; this.name = name; } // 内部方法:仅允许领域服务调用,添加Child关联 void addChildId(ChildId childId) { this.childIds.add(childId); } // 内部方法:移除Child关联 void removeChildId(ChildId childId) { this.childIds.remove(childId); } // 对外提供只读的ID集合,防止外部修改内部状态 public Set<ChildId> getChildIds() { return Collections.unmodifiableSet(childIds); } // 基础属性访问器 public ParentId getId() { return id; } public String getName() { return name; } public void setName(String name) { this.name = name; } }
3. 实现Child聚合根
和Parent结构对称,维护Parent的ID集合:
public class Child implements AggregateRoot<ChildId> { private final ChildId id; private String name; private final Set<ParentId> parentIds = new HashSet<>(); public Child(ChildId id, String name) { this.id = id; this.name = name; } void addParentId(ParentId parentId) { this.parentIds.add(parentId); } void removeParentId(ParentId parentId) { this.parentIds.remove(parentId); } public Set<ParentId> getParentIds() { return Collections.unmodifiableSet(parentIds); } // 基础属性访问器 public ChildId getId() { return id; } public String getName() { return name; } public void setName(String name) { this.name = name; } }
4. 领域服务:维护双向关联一致性
通过领域服务统一处理关联的建立与解除,保证两边的ID集合同时更新:
public class FamilyRelationshipService { private final ParentRepository parentRepository; private final ChildRepository childRepository; // 依赖注入仓储 public FamilyRelationshipService(ParentRepository parentRepository, ChildRepository childRepository) { this.parentRepository = parentRepository; this.childRepository = childRepository; } // 建立Parent与Child的关联 public void establishParentChildRelationship(ParentId parentId, ChildId childId) { Parent parent = parentRepository.findById(parentId) .orElseThrow(() -> new IllegalArgumentException("Parent not found: " + parentId.value())); Child child = childRepository.findById(childId) .orElseThrow(() -> new IllegalArgumentException("Child not found: " + childId.value())); // 调用聚合根内部方法更新关联 parent.addChildId(childId); child.addParentId(parentId); // 持久化两个聚合根 parentRepository.save(parent); childRepository.save(child); } // 解除Parent与Child的关联 public void terminateParentChildRelationship(ParentId parentId, ChildId childId) { Parent parent = parentRepository.findById(parentId) .orElseThrow(() -> new IllegalArgumentException("Parent not found: " + parentId.value())); Child child = childRepository.findById(childId) .orElseThrow(() -> new IllegalArgumentException("Child not found: " + childId.value())); parent.removeChildId(childId); child.removeParentId(parentId); parentRepository.save(parent); childRepository.save(child); } }
5. 仓储接口:支持批量查询
定义仓储接口,提供根据ID集合批量查询聚合根的方法:
public interface ParentRepository extends Repository<Parent, ParentId> { Optional<Parent> findById(ParentId id); void save(Parent parent); // 批量查询:根据多个ParentId获取对应的聚合根 List<Parent> findByIds(Set<ParentId> parentIds); } public interface ChildRepository extends Repository<Child, ChildId> { Optional<Child> findById(ChildId id); void save(Child child); List<Child> findByIds(Set<ChildId> childIds); }
6. 双向访问的实现
在应用服务层通过仓储根据ID集合查询对应的聚合根:
public class FamilyApplicationService { private final ParentRepository parentRepository; private final ChildRepository childRepository; public FamilyApplicationService(ParentRepository parentRepository, ChildRepository childRepository) { this.parentRepository = parentRepository; this.childRepository = childRepository; } // 获取某个Parent的所有Child public List<Child> getChildrenOfParent(ParentId parentId) { Parent parent = parentRepository.findById(parentId) .orElseThrow(() -> new IllegalArgumentException("Parent not found")); return childRepository.findByIds(parent.getChildIds()); } // 获取某个Child的所有Parent public List<Parent> getParentsOfChild(ChildId childId) { Child child = childRepository.findById(childId) .orElseThrow(() -> new IllegalArgumentException("Child not found")); return parentRepository.findByIds(child.getParentIds()); } }
关键注意事项
- 避免跨聚合持有实体引用:直接持有其他聚合根的实体会破坏聚合的边界,导致一致性维护困难,还可能引发持久化层面的懒加载、序列化问题。
- 聚合根状态仅内部修改:通过内部方法(如
addChildId)修改集合,对外只暴露只读视图,保证聚合根的封装性。 - 领域服务保证原子性:关联操作必须通过领域服务统一执行,确保两边的ID集合同时更新,避免出现单向关联的不一致情况。
内容的提问来源于stack exchange,提问作者Vivendi
相关产品推荐
相关产品推荐

