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

DDD领域驱动设计中Category聚合根自关联关系该如何正确实现?

DDD中Category聚合根自关联实现方案

现有实现的核心问题

你的当前代码存在几个明显的设计和逻辑错误,是导致校验失效的核心原因:

  • 方法名与逻辑不匹配:你命名的AddParentCategory方法实际逻辑是给当前分类添加子分类,和方法语义完全相反。而现有校验逻辑仅判断当前分类的父分类名称和要新增的分类名称是否重复,完全没有校验当前分类下的子分类重名问题。
  • 缺少子分类导航属性:仅定义了父分类导航ParentCategory,没有维护当前分类的子分类集合,根本无法遍历校验子分类重名。
  • 聚合规则越界:Category作为独立聚合根,不能在聚合内部直接加载所有关联子分类做校验,否则会破坏聚合边界,还会引发性能问题。
  • 缺失必要持久化字段:你重构后的代码删掉了初始代码里的ParentId、RootId字段,ORM映射和持久化都会出问题。

修正后的实现方案

第一步:调整Category聚合根设计

public class Category : AggregateRoot
{
    // 内部维护子分类集合,禁止外部直接修改
    private readonly List<Category> _children = new();
    // 对外仅暴露只读的子分类视图
    public IReadOnlyCollection<Category> Children => _children.AsReadOnly();
    public Name Name { get; private set; }
    // 根分类的ParentId为null
    public int? ParentId { get; private set; }
    public int RootId { get; private set; }
    public virtual Category ParentCategory { get; private set; }

    private Category() : base() { }

    // 根分类创建入口
    public static Result<Category> CreateRoot(string name)
    {
        var nameResult = Name.Create(name);
        if (nameResult.IsFailed) 
            return Result.Fail<Category>(nameResult.Errors);
        
        var category = new Category
        {
            Name = nameResult.Value,
            ParentId = null
        };
        // 根分类的RootId等于自身Id
        category.RootId = category.Id;
        return Result.Ok(category);
    }

    // 子分类创建入口
    public static Result<Category> CreateChild(Category parent, string name)
    {
        var nameResult = Name.Create(name);
        if (nameResult.IsFailed) 
            return Result.Fail<Category>(nameResult.Errors);
        
        return Result.Ok(new Category
        {
            Name = nameResult.Value,
            ParentId = parent.Id,
            RootId = parent.RootId,
            ParentCategory = parent
        });
    }

    // 内部方法:修改父分类,仅允许领域服务调用
    internal void ChangeParent(Category newParent)
    {
        ParentId = newParent.Id;
        RootId = newParent.RootId;
        ParentCategory = newParent;
    }

    // 内部方法:添加子分类
    internal void AddChild(Category child)
    {
        _children.Add(child);
    }
}

第二步:实现子分类重名校验

因为跨聚合的校验不能放在聚合根内部,所以我们通过领域服务处理创建+校验逻辑,由仓储层做查询判断,不需要加载所有子分类到内存:

public class CategoryDomainService
{
    private readonly ICategoryRepository _categoryRepository;

    public CategoryDomainService(ICategoryRepository categoryRepository)
    {
        _categoryRepository = categoryRepository;
    }

    public async Task<Result<Category>> CreateChildCategoryAsync(int parentId, string categoryName, CancellationToken cancellationToken)
    {
        // 校验父分类是否存在
        var parentCategory = await _categoryRepository.GetByIdAsync(parentId, cancellationToken);
        if (parentCategory == null)
            return Result.Fail<Category>("父分类不存在");

        // 仓储层直接查询该父分类下是否存在同名子分类,性能更高
        var existsSameName = await _categoryRepository.ExistsChildWithNameAsync(parentId, categoryName, cancellationToken);
        if (existsSameName)
            return Result.Fail<Category>($"当前分类下已存在名为{categoryName}的子分类");

        // 创建子分类并关联关系
        var childResult = Category.CreateChild(parentCategory, categoryName);
        if (childResult.IsFailed)
            return childResult;

        parentCategory.AddChild(childResult.Value);
        await _categoryRepository.AddAsync(childResult.Value, cancellationToken);
        
        return Result.Ok(childResult.Value);
    }
}

关键设计说明

  • 聚合边界控制:不要把整个分类树放到一个聚合里,否则修改任意分类都会锁整个树,并发性能极差。每个Category作为独立聚合根,通过ParentId、RootId做关联,跨聚合逻辑全部放到领域服务处理。
  • 校验逻辑拆分:聚合内部仅校验单实体本身的规则(比如名称格式是否合法),跨实体的规则(比如同父级下名称唯一)放到领域服务+仓储查询实现,符合DDD规范同时性能更高。
  • 如果你当前项目的分类总数非常少(比如不超过1000条),也可以把整个分类树作为单个聚合根CategoryTree管理,这样可以直接在聚合内部完成重名校验,不需要走领域服务,可根据实际业务场景选择。

内容的提问来源于stack exchange,提问作者MrJahanbin

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.28 10:15:03