为什么DDD要求一个聚合只能有一个Repository?
早期我开始在公司技术部落地DDD时我在订单主表写了一个Repository然后又给订单明细表写了一个Repository。我当时是认为明细表有自己的增删改查需求单独一个Repository用起来更方便。但是这个做法却让聚合根逐渐失去对整个聚合的控制一致性边界也会随之被打破。日常开发中碰到的单据几乎都是一张主表加一张明细表的结构。订货单、配货单、收货单、出入库单、盘点单全是这个模式。主表存单据的整体信息明细表存这条单据里每一行具体的条目。下面这篇内容就从这个最常见的表结构出发聊聊为什么一个聚合只能有一个仓储以及在代码里怎么用编译器来强制保证它。仓储面向的是聚合不是表DDD里的Repository和传统三层架构里的DAO不是一回事。DAO面向表一张表一个DAO。Repository面向聚合一个聚合一个Repository。一个聚合可能对应一张表也可能对应多张表。对于最常见的单据模型它通常对应一张主表和一张明细表。Repository的save方法调用方传进去的是一个聚合根对象Repository内部自己处理主表和明细表的写入顺序、事务管理、外键回填。调用方不知道底层是两张表还是十张表它只认聚合根。反过来getById也是一样的。Repository从主表查出数据再从明细表批量查出关联数据按主表id分组后组装成一个完整的聚合根对象返回。调用方拿到的就是一个明细列表已经有数据的聚合根不需要自己再去做任何额外的查询或拼装。Repository负责聚合的持久化。调用方只看领域对象数据库怎么存、怎么查都封装在Repository里面。如果给明细单独建一个Repository调用方就可以绕过聚合根直接操作明细数据聚合根也就无法再统一维护整个聚合的一致性。一致性边界是根源为什么主表和明细表要放在一个聚合里不是因为它们在数据库里有外键关系而是因为在业务上它们经常也同时修改的。拿一个最常见的场景来说对单据执行某个状态变更操作时(从待发货扭转到已返回)主表的status字段要更新明细表里的数量字段也要同步填入。这两件事必须在同一个事务里完成否则主表和明细的数据就会出现不一致。主表和明细表是一荣俱荣一损俱损的关系这个一致性边界就是聚合存在的根本理由。一个Repository保存的不是几张表而是一个一致性边界。既然是一个整体对它的持久化操作就应该由一个Repository来统一管理。save的时候主表和明细一起存update的时候主表和明细的变更一起同步到数据库包括新增的明细、修改的明细、以及被聚合根移除的明细。这个职责只能由同一个Repository负责。给明细单独建Repository就意味着主表和明细的写入可以由不同的调用方在不同的时间点触发一致性边界就被打破了。子实体的写操作必须经过聚合根在DDD里子实体不是聚合根没有独立的生命周期。子实体可以被单独查询但不能绕过聚合根独立修改。实际业务中更多的操作是围绕整张单据展开的「给我查一下这张单据的所有明细」「把这张单据里某个条目的数量改成50」。大多数业务操作都是围绕整张单据完成的而不是某一条明细。体现在代码上对明细的增删改都由聚合根提供方法。添加明细通过聚合根的addItem修改某条明细通过聚合根提供的方法删除明细通过聚合根的removeItem。外部代码不能绕过聚合根直接操作子实体。聚合根在操作子实体的时候还可以加上业务校验。比如removeItem在删除前先判断单据是不是待处理状态已提交的不允许删除明细。这个校验涉及两个信息单据自身的status和明细列表都在聚合根的控制范围内。如果允许外部直接操作明细这类校验逻辑就会散落到每个调用点维护起来很痛苦。子实体的持久化跟着聚合根一起走。调用repository.save(order)时Repository实现里把主表和明细一起存。调用repository.update(order)时Repository把主表和明细的变更一起同步到数据库。明细不需要自己的DomainRepository。读操作是例外上面讨论的都是写模型。写操作必须严格走聚合根这一点没有商量余地。对于读模型DDD没有要求一定经过聚合根读操作可以放宽。聚合根的Repository实现里可以提供按条件查询明细的方法比如按单据id查明细列表、按某个字段查某条明细。这属于读取侧的便利接口不违反聚合的写操作约束。读操作不改变数据状态没有一致性风险。管理后台的多条件分页查询场景更极端可能需要跨聚合JOIN。CQRS的实践是写操作严格走领域模型读操作优先走Repository直接查不得已时允许跨聚合SQL。读操作的放宽不会破坏聚合的完整性因为数据只被读取不被修改。小结一个聚合一个仓储这条原则不是DDD的教条而是从一致性边界自然推导出来的设计决策。数据必须同时改的东西放在一个聚合里一个聚合的持久化由一个Repository统一管理子实体的写操作必须经过聚合根。