DDD:含大量关联的聚合根如何维护单任务进行中不变量?
这个问题确实很典型——既要守住核心领域规则,又不能让聚合根因为关联数据过多而变得笨重。我分享几个在实际项目中验证过的思路,你可以根据业务场景选最合适的:
方案一:领域服务 + 数据库唯一约束双重保障
既然不想把所有Task塞进Project聚合根,那可以把“同一时间仅一个任务进行中”的规则检查交给领域服务,同时用数据库约束做最后一道防线:
- 领域服务层面:创建一个
TaskStatusService,提供markTaskAsInProgress(ProjectId projectId, TaskId taskId)方法。方法内部先通过TaskRepository查询该项目下是否存在状态为in_progress的任务,如果有就抛出ConcurrentTaskInProgressException这类领域异常;如果没有,再调用Task的markAsInProgress()方法(让Task自己维护状态变更的逻辑),最后持久化。 - 数据库层面:给Task表添加部分唯一索引(比如PostgreSQL的
CREATE UNIQUE INDEX idx_project_in_progress ON tasks (project_id) WHERE status = 'in_progress';)。这样即使并发场景下领域服务的检查没拦住,数据库也会直接拒绝违反约束的操作,彻底杜绝规则被破坏的可能。
这个方案的优势是聚合根保持轻量化,同时规则的保障足够可靠,适合任务量极大、不需要Project聚合根感知所有任务的场景。
方案二:Project聚合根管控规则,但不持有全量Task列表
让Project聚合根只负责“管控状态变更权限”,但不维护所有Task的引用,而是通过Repository完成数据查询和操作:
- 在Project聚合根中添加
startTask(TaskId taskId, TaskRepository taskRepo)方法。方法内部通过taskRepo查询当前项目下的in_progress任务数量,如果大于0就抛出领域异常;如果没有,就获取目标Task实例,调用其start()方法,最后由Repository持久化变更。 - 注意这个操作必须放在同一个事务中,确保“查询-检查-更新”的原子性,避免并发请求绕过检查。
这种方式既保留了聚合根对领域规则的管控权,又不会因为任务数量增长导致聚合根膨胀,适合需要Project聚合根参与状态变更逻辑的场景。
方案三:Project聚合根仅维护“当前进行中任务ID”
把Project聚合根的状态简化,只存储当前处于in_progress状态的任务ID(比如currentInProgressTaskId: Optional<TaskId>),而不是所有Task的列表:
- 当要启动一个新任务时,调用Project的
startNewTask(TaskId newTaskId)方法:首先检查currentInProgressTaskId是否不为空,如果不为空则抛出异常;然后将currentInProgressTaskId更新为newTaskId,同时通过TaskRepository找到对应Task并标记为in_progress。 - 当任务完成/暂停时,调用Project的
completeCurrentTask()方法,清空currentInProgressTaskId,同时更新对应Task的状态。
这个方案的好处是聚合根非常轻量,领域不变量直接封装在聚合根内部,逻辑清晰。而且Project的状态变化可以方便地发布领域事件(比如TaskStarted、TaskCompleted),适合需要基于Project状态触发其他业务逻辑的场景。
额外注意:并发场景的处理
不管选哪种方案,都要考虑并发竞争的问题:比如两个请求同时尝试启动同一项目下的不同任务。除了数据库约束,还可以给Task或Project添加乐观锁版本号,或者在关键操作时使用悲观锁,确保状态变更的原子性。
内容的提问来源于stack exchange,提问作者Tarlen

