CQRS (Command Query Responsibility Segregation)
CQRS(コマンド・クエリ責務分離)は、データの読み取り(Query)と書き込み(Command)のモデルを分離するアーキテクチャパターンです。
概要
従来のCRUDモデルでは、読み取りと書き込みに同じデータモデルを使用することが一般的でした。しかし、複雑なドメインや高負荷なシステムでは、読み取りと書き込みで求められる要件が大きく異なる場合があります。
- Command (書き込み): データの作成、更新、削除を行います。副作用があり、通常は値を返しません(またはIDのみなど最小限)。
- Query (読み取り): データの取得を行います。副作用がなく、データを返します。
メリット
- スケーラビリティ: 読み取りと書き込みの負荷が異なる場合、それぞれを独立してスケールさせることができます。
- パフォーマンスの最適化: 読み取り専用データベース(リードレプリカ)や、読み取りに最適化されたマテリアライズドビューを使用できます。
- 複雑性の分離: 複雑なビジネスロジック(書き込み)と単純なデータ取得(読み取り)を分離することで、コードの保守性が向上します。
- セキュリティ: 読み取りと書き込みで異なるセキュリティポリシーを適用しやすくなります。
.NETにおける実装 (MediatR)
.NETでは、MediatR ライブラリを使用してCQRSパターンを実装するのが一般的です。MediatRはインプロセスのMediatorパターン実装であり、コントローラーとビジネスロジックを疎結合にします。
セットアップ
NuGetパッケージをインストールします。
dotnet add package MediatR
実装例
1. Command (書き込み)
ユーザーを作成するコマンドの例です。
// CreateUserCommand.cs
using MediatR;
public class CreateUserCommand : IRequest<int>
{
public string Name { get; set; }
public string Email { get; set; }
}
コマンドハンドラーの実装です。
// CreateUserCommandHandler.cs
using MediatR;
public class CreateUserCommandHandler : IRequestHandler<CreateUserCommand, int>
{
private readonly IUserRepository _repository;
public CreateUserCommandHandler(IUserRepository repository)
{
_repository = repository;
}
public async Task<int> Handle(CreateUserCommand request, CancellationToken cancellationToken)
{
// バリデーションやビジネスロジック
var user = new User { Name = request.Name, Email = request.Email };
await _repository.AddAsync(user);
return user.Id;
}
}
2. Query (読み取り)
ユーザーを取得するクエリの例です。
// GetUserByIdQuery.cs
using MediatR;
public class GetUserByIdQuery : IRequest<UserDto>
{
public int Id { get; set; }
public GetUserByIdQuery(int id)
{
Id = id;
}
}
クエリハンドラーの実装です。
// GetUserByIdQueryHandler.cs
using MediatR;
public class GetUserByIdQueryHandler : IRequestHandler<GetUserByIdQuery, UserDto>
{
private readonly IUserRepository _repository;
public GetUserByIdQueryHandler(IUserRepository repository)
{
_repository = repository;
}
public async Task<UserDto> Handle(GetUserByIdQuery request, CancellationToken cancellationToken)
{
// 読み取り専用の最適化されたクエリを実行可能
var user = await _repository.GetByIdAsync(request.Id);
if (user == null) return null;
return new UserDto
{
Id = user.Id,
Name = user.Name,
Email = user.Email
};
}
}
3. Controllerでの利用
コントローラーはMediatRにリクエストを投げるだけで、具体的な処理を知る必要がありません。
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
private readonly IMediator _mediator;
public UsersController(IMediator mediator)
{
_mediator = mediator;
}
[HttpPost]
public async Task<IActionResult> Create(CreateUserCommand command)
{
var userId = await _mediator.Send(command);
return CreatedAtAction(nameof(GetById), new { id = userId }, null);
}
[HttpGet("{id}")]
public async Task<IActionResult> GetById(int id)
{
var query = new GetUserByIdQuery(id);
var user = await _mediator.Send(query);
if (user == null)
return NotFound();
return Ok(user);
}
}
注意点
- 複雑性の増加: 単純なCRUDアプリケーションにCQRSを導入すると、クラス数が増え、かえって複雑になる可能性があります。
- 結果整合性: 読み取りと書き込みで異なるデータベースを使用する場合、データが同期されるまでの遅延(結果整合性)を考慮する必要があります。
まとめ
CQRSは強力なパターンですが、すべてのプロジェクトに適しているわけではありません。ドメインの複雑さやパフォーマンス要件を考慮して、適用の是非を判断する必要があります。