DDD领域驱动设计结合CQRS模式下批量导入逻辑复用与性能优化问询
解决方案核心思路
你的核心问题根源是业务逻辑耦合在了Command Handler层,而Handler本身是用来处理请求协调的薄适配层,不是承载业务规则的地方。只要把业务逻辑下沉到领域层(聚合根/领域服务),就能同时满足复用性和性能要求,也完全符合CQRS不允许嵌套命令的规范。
步骤1:重构现有代码,把业务逻辑从Handler下沉到聚合根
你原来的Business Logic A/B/C全部是购物清单这个聚合根的固有业务规则,应该放到ShoppingList聚合根内部实现,Handler只负责参数传递、聚合读取、提交变更三个动作。
聚合根代码示例
public class ShoppingList : AggregateRoot { public Guid UserId { get; private set; } private List<ShoppingItem> _items = new(); public IReadOnlyCollection<ShoppingItem> Items => _items.AsReadOnly(); // 原CreateShoppingList的业务逻辑A收敛至此 public static ShoppingList Create(Guid userId) { if (userId == Guid.Empty) throw new ArgumentException("用户ID无效"); // 后续新增的创建类校验规则都可以加在这里 return new ShoppingList { UserId = userId }; } // 原AddItemToShoppingList的业务逻辑B收敛至此 public void AddItem(Guid productId, int quantity) { if (quantity <= 0) throw new ArgumentException("商品数量必须大于0"); var existingItem = _items.FirstOrDefault(x => x.ProductId == productId); if (existingItem != null) { // 已存在商品直接复用更新逻辑 UpdateItem(productId, existingItem.Quantity + quantity); return; } _items.Add(new ShoppingItem(productId, quantity)); } // 原UpdateItemOnShoppingList的业务逻辑C收敛至此 public void UpdateItem(Guid productId, int quantity) { if (quantity <= 0) throw new ArgumentException("商品数量必须大于0"); var existingItem = _items.FirstOrDefault(x => x.ProductId == productId) ?? throw new InvalidOperationException("商品不存在于购物清单"); existingItem.UpdateQuantity(quantity); } }
重构后的单操作Handler代码
public class ShoppingListCommandHandler : ICommandHandler<CreateShoppingListCommand>, ICommandHandler<AddItemToShoppingListCommand>, ICommandHandler<UpdateItemOnShoppingListCommand> { Task Handle(CreateShoppingListCommand command, CancellationToken ct) { var list = ShoppingList.Create(command.UserId); _shoppingListRepository.Add(list); _shoppingListRepository.SaveChangesAsync(ct); } Task Handle(AddItemToShoppingListCommand command, CancellationToken ct) { var list = await _shoppingListRepository.GetByUserIdAsync(command.UserId, ct); if (list == null) throw new InvalidOperationException("购物清单不存在"); list.AddItem(command.ProductId, command.Quantity); _shoppingListRepository.SaveChangesAsync(ct); } Task Handle(UpdateItemOnShoppingListCommand command, CancellationToken ct) { var list = await _shoppingListRepository.GetByUserIdAsync(command.UserId, ct); if (list == null) throw new InvalidOperationException("购物清单不存在"); list.UpdateItem(command.ProductId, command.Quantity); _shoppingListRepository.SaveChangesAsync(ct); } }
步骤2:重写导入逻辑,批量预加载数据+复用领域层逻辑
导入逻辑不需要自定义业务规则,也不需要发送单个Command,只需要批量预加载所有需要的数据到内存,直接调用聚合根的方法处理,最后一次性提交即可。
导入Handler代码示例
Task Handle(ImportShoppingListCommand command, CancellationToken ct) { // 1. 基础文件格式校验、解析行数据 var importRows = ParseAndValidateFile(command.FileContent); if (!importRows.Any()) return Task.CompletedTask; // 2. 批量校验用户合法性,仅1次DB查询 var userIds = importRows.Select(x => x.UserId).Distinct().ToList(); var validUserIds = await _userRepository.GetExistsUserIdsAsync(userIds, ct); var validRows = importRows.Where(x => validUserIds.Contains(x.UserId)).ToList(); // 3. 批量拉取所有有效用户的现有购物清单,仅1次DB查询 var existingLists = await _shoppingListRepository.GetListByUserIdsAsync(validUserIds, ct); var listDict = existingLists.ToDictionary(x => x.UserId); // 4. 内存聚合重复行:同一用户同一商品合并数量,避免重复处理 var groupedRows = validRows.GroupBy(x => new {x.UserId, x.ProductId}) .Select(g => new { g.Key.UserId, g.Key.ProductId, TotalQuantity = g.Sum(x => x.Quantity) }).ToList(); // 5. 遍历处理所有行,全部复用聚合根的业务逻辑 foreach (var row in groupedRows) { if (!listDict.TryGetValue(row.UserId, out var shoppingList)) { // 不存在购物清单则创建,复用创建逻辑 shoppingList = ShoppingList.Create(row.UserId); _shoppingListRepository.Add(shoppingList); listDict.Add(row.UserId, shoppingList); } // 添加商品,内部自动处理新增/更新逻辑,复用原有规则 shoppingList.AddItem(row.ProductId, row.TotalQuantity); } // 6. 一次性提交所有变更,仅1次DB写入 await _shoppingListRepository.SaveChangesAsync(ct); }
方案优势
- 逻辑唯一:所有业务规则全部收敛在领域层,单接口操作和批量导入共用同一套逻辑,后续规则修改只需要改动一处
- 性能达标:整个导入流程仅需要3次DB交互,没有循环IO开销,也不需要生成大量Command实例
- 符合规范:没有嵌套发送Command,完全符合你的CQRS约束
- 天然处理重复行:内存层先按用户+商品维度合并导入数据,避免重复操作的问题
扩展场景兼容
如果后续新增跨聚合的业务规则(比如校验每个家庭仅能有一个购物清单),只需要把这部分逻辑抽到公共的领域服务中,单操作Handler和导入Handler都调用同一个领域服务的校验方法即可,依然保持唯一的逻辑维护入口。
内容的提问来源于stack exchange,提问作者Johhny Bravo
相关产品推荐
相关产品推荐

