LaravelでCQRSを実践!コマンドとクエリを分離する設計手法

Laravel
スポンサーリンク
OANDA証券 MT4

コントローラーの肥大化、防ぎませんか?

こんにちは、編集長Kです。 いつも開発お疲れ様です。

APIを作っていると、こんな悩みはありませんか?

「データの取得と更新が混ざってコードが読みづらい」

「コントローラーが長すぎて、どこを直せばいいか分からない」

これは開発者なら誰もが通る道ですね。

そんな悩みを解決するのが「CQRS」という設計手法です。

日本語では「コマンド・クエリ責務分離」と呼びます。

難しそうに聞こえるかもですが、概念はシンプルです。

「データを書き込む処理」と「読み込む処理」を分ける。

たったこれだけです。

この記事で解説する手順の全体像は以下の通りです。

  • CQRSの基本概念の理解
  • 手順①:書き込み(Command)クラスの作成
  • 手順②:読み込み(Query)クラスの作成
  • 手順③:コントローラーでの呼び出しとAPI Resource化

前提条件として、以下の環境を想定しています。

  • Laravel環境(Sanctum等の認証済み)が構築されている
  • 基本的なMVCパターンの理解がある
  • API Resourceを利用したJSONレスポンスの知識がある

まずはCQRSのイメージを掴もう

CQRSの最大のメリットは、コードの「責務」が明確になることです。

更新は更新のルールで、取得は取得のルールで書けます。

これにより、N+1問題の対策やパフォーマンス最適化もやりやすくなります。

図解すると、以下のようなイメージですね。

CQRS(コマンド・クエリ責務分離)の基本アーキテクチャ図

手順①:書き込み(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に切り分けるリファクタリングに挑戦してみましょう。

それでは、また次回の記事でお会いしましょう!

この記事を書いた人
Trade Agency編集長K

■ 投資歴: 10年以上
■ スタイル: Fintokei(プロップファーム)× 株式投資
■ 職業: ITコンサルタント、アプリ開発

【詳細】
USD/JPYの裁量トレードでプロップファーム攻略に挑むエンジニアトレーダー。
Fintokeiで稼いだ利益を、堅実なバリュー・グロース株へ投資して資産を最大化する「資金循環スタイル」を実践中。

本業のWeb開発スキルを活かし、FXに役立つツール(Lot計算機など)を自作・無料公開しています。

Trade Agency編集長Kをフォローする
Laravelシステム開発
スポンサーリンク
Trade Agency編集長Kをフォローする

コメント

タイトルとURLをコピーしました