コントローラーの肥大化、防ぎませんか?
こんにちは、編集長Kです。 いつも開発お疲れ様です。
APIを作っていると、こんな悩みはありませんか?
「データの取得と更新が混ざってコードが読みづらい」
「コントローラーが長すぎて、どこを直せばいいか分からない」
これは開発者なら誰もが通る道ですね。
そんな悩みを解決するのが「CQRS」という設計手法です。
日本語では「コマンド・クエリ責務分離」と呼びます。
難しそうに聞こえるかもですが、概念はシンプルです。
「データを書き込む処理」と「読み込む処理」を分ける。
たったこれだけです。
この記事で解説する手順の全体像は以下の通りです。
- CQRSの基本概念の理解
- 手順①:書き込み(Command)クラスの作成
- 手順②:読み込み(Query)クラスの作成
- 手順③:コントローラーでの呼び出しとAPI Resource化
前提条件として、以下の環境を想定しています。
- Laravel環境(Sanctum等の認証済み)が構築されている
- 基本的なMVCパターンの理解がある
- API Resourceを利用したJSONレスポンスの知識がある
まずはCQRSのイメージを掴もう
CQRSの最大のメリットは、コードの「責務」が明確になることです。
更新は更新のルールで、取得は取得のルールで書けます。
これにより、N+1問題の対策やパフォーマンス最適化もやりやすくなります。
図解すると、以下のようなイメージですね。

手順①:書き込み(Command)クラスを作る
それでは実践です。 まずはデータを「更新・作成」するCommandクラスを作ります。
例として、トレード履歴(tn_trades)の登録処理を書きましょう。
まずは、下記を実行してファイルを用意し、コードを記述してください。
namespace App\UseCases\TradeNote;
use App\Models\TradeNote\Trade;
use Illuminate\Support\Facades\DB;
class CreateTradeCommand
{
public function execute(array $data): Trade
{
return DB::transaction(function () use ($data) {
// ここで書き込みのビジネスロジックに集中します
return Trade::create([
'user_id' => auth()->id(),
'pair' => $data['pair'],
'entry_date' => $data['entry_date'], // UTCで保存を前提
]);
});
}
}これでOKです。
Controllerに書いていたDB保存処理を、まるっと移動できました。
トランザクションもここで管理すれば安全ですね。
手順②:読み込み(Query)クラスを作る
次はデータを「取得」するQueryクラスです。
フロントエンドに返すための、最適なデータ取得に特化させます。
続いて、下記です。
namespace App\UseCases\TradeNote;
use App\Models\TradeNote\Trade;
use Illuminate\Database\Eloquent\Collection;
class GetTradeListQuery
{
public function execute(int $userId): Collection
{
// N+1問題を回避するためのEager Loading(with)を徹底
return Trade::with(['user', 'details'])
->where('user_id', $userId)
->orderBy('entry_date', 'desc')
->get();
}
}こちらもバッチリですね。
データの「取得」だけを考えるので、クエリの最適化に集中できます。
手順③:コントローラーで呼び出す
最後に、これらをControllerで呼び出しましょう。
フロントへのレスポンスは、必ずAPI Resourceを経由させます。
まずは、下記を実行してください。
namespace App\Http\Controllers\TradeNote;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\UseCases\TradeNote\CreateTradeCommand;
use App\UseCases\TradeNote\GetTradeListQuery;
use App\Http\Resources\TradeNote\TradeResource;
class TradeController extends Controller
{
public function index(Request $request, GetTradeListQuery $query)
{
$trades = $query->execute($request->user()->id);
// Modelをそのまま返さず、必ずResourceを経由する
return TradeResource::collection($trades);
}
public function store(Request $request, CreateTradeCommand $command)
{
$validated = $request->validate([
'pair' => 'required|string',
'entry_date' => 'required|date',
]);
$trade = $command->execute($validated);
return new TradeResource($trade);
}
}とてもスッキリしましたね!
Controllerのコードが短くなり、何をしているかが一目でわかります。
Modelを直接返さず、Resourceを通すことで型安全も保証されます。
よくあるエラーと解決策(編集長Kの経験則)
ここで、現場でよくある落とし穴をシェアします。
① シンプルすぎる処理までCQRSにしてしまう
マスタデータの単なる一覧取得など、ロジックが皆無なものまで分けると逆効果です。
「ファイルが増えすぎて辛い」という状態になります。
最初は「複雑な処理」や「トランザクションが絡む更新」から導入しましょう。
② 非同期処理(キュー)でのコンテキスト喪失
Commandの中でJobをディスパッチする際、ヘッダー情報(AppSource等)に依存しているとエラーになります。
キューワーカー内ではHTTPリクエストが存在しないからです。
必要な情報は必ずController等の同期処理中に取得し、明示的に渡してくださいね。
まとめ:責務を分けて保守性を爆上げしよう
お疲れ様でした。
今回はLaravelでのCQRSの実践方法を解説しました。
「書くこと」と「読むこと」のルールを分ける。
これだけで、コードの可読性と保守性は劇的に向上します。
フロント側のSWRでのキャッシュ戦略とも非常に相性が良いですよ。
まずは、今一番「肥大化して読みづらい」と感じるAPIを1つ見つけてください。
そして、その処理をCommandとQueryに切り分けるリファクタリングに挑戦してみましょう。
それでは、また次回の記事でお会いしましょう!

コメント