
本文详解在 cqrs 架构下如何合理拆分读写职责:不强制要求按仓储(repository)粒度拆分,而应以命令/查询语义为核心,结合 command bus 与 query bus 实现真正松耦合、可扩展的读写分离。
本文详解在 cqrs 架构下如何合理拆分读写职责:不强制要求按仓储(repository)粒度拆分,而应以命令/查询语义为核心,结合 command bus 与 query bus 实现真正松耦合、可扩展的读写分离。
在 CQRS(Command Query Responsibility Segregation)架构中,“读写分离”的本质并非简单地将一个 UserRepository 拆成 UserReadRepository 和 UserWriteRepository 两个类——那只是表层的代码拆分,容易陷入“为 CQRS 而 CQRS”的过度设计陷阱。真正的 CQRS 关注的是职责分离:写操作(Command)负责状态变更与业务规则执行;读操作(Query)专注高效、灵活、可优化的数据获取,二者可使用完全不同的数据模型、存储引擎甚至技术栈。
因此,推荐采用更符合领域驱动设计(DDD)与现代 PHP 实践的方案:以服务为边界,通过 Command/Query Handler + 消息总线(Bus)实现逻辑隔离。以下为清晰落地步骤:
✅ 正确实践:用 Command/Query Handler 替代读写仓储拆分
首先,引入轻量级 CQRS 支持库(如 Ecotone Laravel):
composer require ecotone/laravel
接着定义语义明确的命令与查询对象(DTO):
// app/Queries/GetUserByEmailQuery.php
class GetUserByEmailQuery
{
public function __construct(public string $email) {}
}
// app/Commands/SaveUserCommand.php
class SaveUserCommand
{
public function __construct(
public string $id,
public string $name,
public string $email,
public ?string $avatar = null
) {}
}
然后编写对应的 Handler——可选择单服务聚合(简洁)或双服务分离(严格):
// app/Services/UserService.php —— 推荐初学者起步方式(职责清晰,维护友好)
#[Service]
class UserService
{
#[CommandHandler]
public function handleSaveUser(SaveUserCommand $command): void
{
$user = User::updateOrCreate(
['id' => $command->id],
['name' => $command->name, 'email' => $command->email, 'avatar' => $command->avatar]
);
// 可触发领域事件,如 UserCreated::from($user)
}
#[QueryHandler]
public function handleGetUserByEmail(GetUserByEmailQuery $query): ?User
{
return User::where('email', $query->email)->first();
}
}
? 注意:Ecotone 会自动注册这些 Handler 到对应的 CommandBus / QueryBus,无需手动绑定。
? 在控制器中调用(零侵入、高可测)
// app/Http/Controllers/UserController.php
class UserController extends Controller
{
public function __construct(
private CommandBus $commandBus,
private QueryBus $queryBus
) {}
public function store(Request $request)
{
$this->commandBus->send(new SaveUserCommand(
$request->input('id', Str::uuid()->toString()),
$request->input('name'),
$request->input('email'),
$request->input('avatar')
));
return response()->json(['message' => 'User saved']);
}
public function showByEmail(Request $request)
{
$user = $this->queryBus->send(new GetUserByEmailQuery($request->input('email')));
return $user
? response()->json($user->only('id', 'name', 'email'))
: response()->json(['error' => 'Not found'], 404);
}
}
⚠️ 关键注意事项与最佳实践
- 不要机械拆分 Repository:UserRepositoryReadInterface / UserRepositoryWriteInterface 在多数场景下是反模式——它混淆了「数据访问抽象」与「业务意图抽象」。CQRS 的核心是 操作语义(Command/Query),不是 数据访问方式。
- 读模型可独立演进:未来若需高性能查询,可将 handleGetUserByEmail 迁移至专用读库(如 Elasticsearch、Materialized View 或缓存层),而写模型保持不变。
- 事务边界由 Command Handler 控制:所有写操作应在 CommandHandler 内完成完整业务事务(含验证、领域事件发布、最终一致性处理)。
- Query 不应修改状态:确保 QueryHandler 中无 save()、delete() 等副作用操作,这是 CQRS 基本契约。
- 命名即契约:SaveUserCommand、GetUserByEmailQuery 等类名必须准确反映其不可变语义,利于团队协作与代码可读性。
总结来说,CQRS 不是“把接口一分为二”,而是构建一套围绕命令驱动变更、查询驱动展示的系统思维。从 CommandBus 和 QueryBus 入手,比过早纠结于仓储拆分更稳健、更贴近真实业务演进路径。











