You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.19 10:24:42