Clean架构中领域不变量:应用层校验还是SQL原子执行?
Clean Architecture下的并发一致性与领域逻辑边界问题
问题背景
我正遵循Clean Architecture构建应用,当前流程为:
- 通过Repository执行SELECT获取数据;
- 在应用/用例层校验领域逻辑;
- 校验通过则通过Repository执行UPDATE/DELETE。
以删除计划参与者为例,流程是先查询计划,在应用层检查状态、权限等业务规则(如计划需处于OPEN状态、操作用户为计划主持人),再执行删除。但该设计存在并发一致性问题:查询数据可能非最新状态、SELECT与DELETE间数据被修改、竞态条件导致异常状态。
于是考虑将校验条件嵌入SQL,实现校验与修改原子化,示例SQL为:
DELETE FROM participants p USING plans pl WHERE p.plan_id = pl.id AND p.plan_id = ? AND p.user_id = ? AND pl.host_id = ? AND pl.status = 'OPEN' RETURNING p.*;
该方案可原子执行校验与修改、基于最新提交状态评估条件、提升并发安全性,但会将领域逻辑嵌入数据层,模糊领域层与数据层边界。
问题
- 从Clean Architecture角度,将领域条件直接嵌入SQL是否违规?是否违反其原则?
- 若需数据库层面保证强一致性以应对并发:
- 应用/领域层应保留哪些职责?
- 哪些规则或不变量需在SQL或数据库约束中执行?
- 如何划分领域逻辑与数据库保障的职责边界?
回答
问题1:从Clean Architecture角度,将领域条件直接嵌入SQL是否违规?是否违反其原则?
严格来说,这不算直接违反Clean Architecture的核心原则,但确实会模糊层间边界,需要结合业务场景权衡。
Clean Architecture的核心是依赖反转原则:内层(领域层、应用层)定义抽象,外层(数据层、UI层)依赖这些抽象实现。把领域规则写死在SQL里,相当于让数据层直接承载了领域逻辑,会带来几个问题:
- 领域规则分散在应用层和数据层,难以统一维护,修改规则时需要同时改动两处;
- 更换数据库或Repository实现时,需要同步修改SQL中的规则,违背了Repository抽象的解耦初衷;
- 单元测试时无法仅通过Mock Repository验证领域规则,必须依赖真实数据库环境,增加测试成本。
但Clean Architecture不是教条——如果并发一致性是业务的强需求,完全禁止这种做法反而会导致业务风险。关键是不要让SQL成为领域规则的唯一载体,必须保证领域层仍有对应的校验逻辑,SQL只是作为并发场景下的兜底保障。
问题2:若需数据库层面保证强一致性以应对并发
应用/领域层应保留的职责
- 核心领域规则的定义与校验:所有业务规则(比如“只有主持人能删除参与者”“计划处于OPEN状态才能操作”)必须在领域层用代码实现,作为业务逻辑的单一可信来源;
- 业务流程编排:负责调用Repository、处理业务异常(比如SQL执行后返回空结果,说明并发修改导致校验不通过,此时应用层需要返回“操作失败,数据已更新”的提示);
- 领域对象封装:保证领域对象的状态只能通过合法方法修改,避免外部直接操作数据,维护领域对象的完整性;
- 非并发场景的前置校验:在低并发或不需要强一致性的场景中,应用层的校验可以快速拦截非法请求,减少不必要的数据库交互。
需在SQL或数据库约束中执行的规则/不变量
- 并发场景下的原子性校验:即你示例中的“校验+修改”原子操作,确保高并发下只有符合最新数据状态的请求能执行成功;
- 数据底层硬约束:比如外键约束、唯一索引、字段枚举值约束(如
plan.status只能是OPEN/CLOSED/COMPLETED),这些是数据的底层一致性保障,避免非法数据写入; - 性能敏感的批量操作校验:如果需要批量修改/删除大量数据,在应用层逐个校验会导致性能瓶颈,此时可以把校验逻辑嵌入SQL,兼顾原子性和效率;
- 跨表一致性校验:涉及多个表的修改时,用SQL原子操作或数据库事务保证跨表数据的一致性,避免应用层多步操作带来的中间状态不一致。
领域逻辑与数据库保障的职责边界划分
可以遵循以下几个原则:
- 领域层是业务规则的单一可信来源:所有业务规则必须先在领域层用代码实现,SQL中的校验只是领域规则的“镜像实现”,仅用于并发场景的兜底;
- 数据库负责“数据一致性”,领域层负责“业务正确性”:数据库保证数据不会处于非法状态(如不存在的
plan_id、违反外键约束),领域层保证操作符合业务逻辑(如主持人权限、状态要求); - 仅当并发风险不可避免时,才将领域规则嵌入SQL:如果业务场景并发量低,或可以通过乐观锁(如版本号)在应用层解决,优先保留领域层的单一校验;
- Repository抽象隐藏SQL细节:即使SQL中包含校验逻辑,Repository的接口仍要保持抽象(比如
deleteParticipant(planId, userId, hostId)),内部实现用带校验的SQL,但外部调用者无需知晓SQL细节,符合依赖反转原则。
内容的提问来源于stack exchange,提问作者gonchan
相关产品推荐
相关产品推荐

