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

在Domain-Driven Design与CQRS项目中如何在领域层遍历并修改自引用聚合根分类树

如何在DDD领域层处理分类树的排序与层级校验?

问题描述

我在项目中采用Domain-Driven Design(DDD)与CQRS架构,使用自引用聚合根Category来建模层级结构,Category的字段如下:

Category
- CategoryId $id
- CategoryName $name
- ?CategoryId $parentId
- CategoryPosition $position

现在需要实现分类树排序功能,API控制器会接收如下结构的树形数据:

[
  {
    "id": "ffb03f1d-dade-4f45-80d5-6e28fea17e70",
    "children": [
      {
        "id": "bdb82eb6-59ea-4645-ae70-d03b5e01c4b6",
        "children": [
          {
            "id": "0ad9c916-eb64-4b0a-b2d0-6864b81b304f",
            "children": []
          }
        ]
      },
      {
        "id": "afa58caf-a0b4-4787-b784-ed3344af2b2b",
        "children": [
          {
            "id": "6f334ab9-47b3-4f7b-8f87-f9c36db8afe5",
            "children": []
          }
        ]
      }
    ]
  },
  {
    "id": "7d3b6110-c282-42b3-b649-efc9394fa5f0",
    "children": []
  }
]

目前我在基础设施层通过递归遍历新树,使用MoveCategory命令来修改每个分类的parentId和position。但每个命令只能修改单个Category,无法通过单一命令完成操作。同时我需要检查树深度不超过4层,这是一条领域规则。
我的问题是:如何在领域层中遍历该新树并进行修改操作?

解决方案

1. 先把树深度校验逻辑放到领域层

领域规则必须由领域层守护,不能放到基础设施层。你可以在Category所在的领域服务里实现递归校验的方法,确保传入的树形结构符合深度要求:

public class CategoryTreeDomainService
{
    public void ValidateTreeDepth(List<CategoryTreeDto> treeNodes, int currentDepth = 1)
    {
        if (currentDepth > 4)
        {
            throw new DomainException("分类树深度不能超过4层");
        }

        foreach (var node in treeNodes)
        {
            if (node.Children.Any())
            {
                ValidateTreeDepth(node.Children, currentDepth + 1);
            }
        }
    }
}

这里的CategoryTreeDto是你从API接收的树形数据映射到领域层的传输模型(不是聚合根,只是用来传递结构)。

2. 定义批量更新命令,统一处理整个树形结构

既然单个MoveCategory命令无法覆盖整个树的修改,建议你定义一个批量更新分类树的命令,比如UpdateCategoryTreeCommand,把整个树形结构作为命令参数。然后在命令处理程序里完成校验和递归更新:

// 批量更新命令定义
public class UpdateCategoryTreeCommand : ICommand
{
    public List<CategoryTreeDto> TreeNodes { get; set; }
}

// 命令处理程序
public class UpdateCategoryTreeCommandHandler : ICommandHandler<UpdateCategoryTreeCommand>
{
    private readonly ICategoryRepository _categoryRepo;
    private readonly CategoryTreeDomainService _treeService;

    public UpdateCategoryTreeCommandHandler(ICategoryRepository categoryRepo, CategoryTreeDomainService treeService)
    {
        _categoryRepo = categoryRepo;
        _treeService = treeService;
    }

    public async Task Handle(UpdateCategoryTreeCommand command, CancellationToken cancellationToken)
    {
        // 第一步:先校验树深度,不符合规则直接抛出领域异常
        _treeService.ValidateTreeDepth(command.TreeNodes);

        // 第二步:递归遍历树形结构,更新每个分类的父节点和排序位置
        await ProcessTreeNodes(command.TreeNodes, parentId: null);

        // 第三步:提交所有变更到数据库
        await _categoryRepo.UnitOfWork.SaveChangesAsync(cancellationToken);
    }

    private async Task ProcessTreeNodes(List<CategoryTreeDto> nodes, Guid? parentId)
    {
        for (int i = 0; i < nodes.Count; i++)
        {
            var node = nodes[i];
            // 从仓储获取对应的聚合根
            var category = await _categoryRepo.GetByIdAsync(node.Id);
            if (category == null)
            {
                throw new DomainException($"分类ID {node.Id} 不存在");
            }

            // 调用聚合根的方法修改父节点和位置,把领域行为封装在聚合根内
            category.UpdateParentAndPosition(parentId, i + 1); // 排序位置通常从1开始计数

            // 递归处理子节点
            if (node.Children.Any())
            {
                await ProcessTreeNodes(node.Children, node.Id);
            }
        }
    }
}

3. 在聚合根内封装修改行为,保证状态一致性

上面代码中的category.UpdateParentAndPosition方法,应该是Category聚合根内部的方法,用来封装修改parentId和position的逻辑,确保聚合根的状态始终合法:

public class Category : AggregateRoot<CategoryId>
{
    public CategoryName Name { get; private set; }
    public CategoryId? ParentId { get; private set; }
    public CategoryPosition Position { get; private set; }

    // 构造函数等其他代码省略...

    // 封装更新父节点和排序位置的行为
    public void UpdateParentAndPosition(CategoryId? newParentId, int newPosition)
    {
        // 可以在这里添加额外的领域规则校验,比如不能把自己设为父节点
        if (newParentId == Id)
        {
            throw new DomainException("分类不能设置自己作为父节点");
        }

        ParentId = newParentId;
        Position = new CategoryPosition(newPosition);
    }
}

4. 避免基础设施层直接操作领域对象

之前你在基础设施层直接递归修改的做法,会导致领域规则被绕过。正确的做法是:基础设施层只负责实现仓储接口,处理数据持久化逻辑;所有的业务规则校验、状态修改都由领域层(聚合根、领域服务、命令处理程序)来完成。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.27 15:27:48