在多数据库CQRS架构中,数据库操作应放在命令还是仓储中?
针对你提出的三种方案,逐一分析优劣,并给出更合理的实践方式:
方案1:仓储内同时操作两个数据库
CommunityRepository{ Add(Community community){ Database1.Insert(community); Database2.Insert(community); } }
这种方式的核心问题是违背了仓储的单一职责。仓储的本质是封装聚合根到主数据存储的持久化逻辑,而搜索库(比如Elasticsearch这类专门的检索存储)和主库通常是不同类型的存储,把两者的插入逻辑揉进仓储,会让仓储承担不属于它的职责,后续修改搜索库的同步规则(比如字段映射调整、异步同步)时,必须改动仓储代码,违反开闭原则。仅适合极简单、无未来扩展需求的小型项目,不推荐长期使用。
方案2:命令Handler直接操作两个数据库
CommunityCommands{ Handler(AddCommunityCommand community){ db1.Insert(community); db2.Insert(community); } }
这种方式的问题是业务逻辑与数据访问严重耦合。命令Handler的核心职责是处理业务规则(比如参数校验、状态转换),而非直接操作数据库。直接在Handler里写数据库操作,会导致代码难以测试(需要模拟两个数据库),且如果多个命令都需要同步搜索库,会出现大量重复代码,维护成本极高,完全不推荐。
方案3:主仓储+Handler直接操作第二个数据库
CommunityCommands{ Handler(AddCommunityCommand community){ communityRepository.Add(community); db2.Insert(community); } }
相比方案2,这种方式把主库的持久化逻辑交给了仓储,算是小进步,但依然存在问题:Handler里还是直接耦合了搜索库的操作,同样会导致重复代码和测试复杂度提升,仅适合临时快速开发的场景,不适合长期维护的项目。
更优实践
根据CQRS的设计理念,推荐两种更合理的实现方式:
1. 强一致性场景(必须两个库写入成功才返回)
封装专门的搜索同步服务,将搜索库的操作从Handler和仓储中剥离:
// 仓储仅负责主库持久化 CommunityRepository{ Add(Community community){ Database1.Insert(community); } } // 专门处理搜索库同步的服务 CommunitySearchSyncService{ SyncToSearch(Community community){ Database2.Insert(community); } } // 命令Handler专注业务逻辑与服务协调 CommunityCommands{ Handler(AddCommunityCommand command){ // 业务规则校验 var community = MapToCommunity(command); // 主库写入 communityRepository.Add(community); // 同步搜索库 communitySearchSyncService.SyncToSearch(community); } }
这种方式既保持了仓储的单一职责,又让Handler避免了直接操作数据库,代码的可维护性和可测试性大幅提升。
2. 最终一致性场景(推荐,CQRS常用模式)
通过领域事件+异步处理实现搜索库同步,彻底解耦主库与搜索库的操作:
// 仓储写入主库后发布领域事件 CommunityRepository{ Add(Community community){ Database1.Insert(community); // 发布聚合根新增事件 DomainEventPublisher.Publish(new CommunityAddedEvent(community)); } } // 事件处理器异步同步搜索库 CommunityAddedEventHandler{ Handle(CommunityAddedEvent event){ communitySearchSyncService.SyncToSearch(event.Community); } } // 命令Handler仅处理核心业务 CommunityCommands{ Handler(AddCommunityCommand command){ var community = MapToCommunity(command); // 业务校验与主库写入 communityRepository.Add(community); } }
这种方式的优势在于:命令Handler无需关心搜索库的存在,专注于业务逻辑;异步处理避免了同步写入带来的性能损耗;即使搜索库暂时不可用,也可以通过重试机制保证最终一致性,系统的容错性更强。
内容的提问来源于stack exchange,提问作者Augusto Will

