◀ 8.

【Laravel】where句の入れ子(ネスト)

この記事の要点
  • Laravel のクエリビルダで WHERE 句を括弧でくくる(ネストする)には、where() にクロージャを渡す
  • ->where(function ($q) { ... }) の中に書いた条件が ( ) でまとめられる
  • SQL では AND が OR より優先されるため、OR を含む条件は必ずネストしないと意図しない結果になる
  • 外側の変数をクロージャ内で使うときは use ($keyword) か アロー関数 fn を使う
  • 生成された SQL は toSql() や toRawSql() で確認できる

やりたいこと: 括弧付きの WHERE を書く

「colA か colB が val1 で、かつ colC が val2」という条件を、素の SQL で書くと次のようになります。

SELECT * FROM article
WHERE (colA = 'val1' OR colB = 'val1') AND colC = 'val2'

これをクエリビルダ(または Eloquent)で書くと次のとおりです。

Article::where(function ($query) {
        $query->where('colA', 'val1')
            ->orWhere('colB', 'val1');
    })
    ->where('colC', 'val2')
    ->get();

where() の引数にクロージャ(無名関数)を渡すと、その中で組み立てた条件全体が括弧でくくられます。DB::table('article') で始めるクエリビルダでも書き方は同じです。

なぜネストが必要なのか(AND と OR の優先順位)

SQL では AND の方が OR より先に評価されます。ネストせずに次のように書くと、意図と違う SQL になります。

// 間違い: 括弧が付かない
Article::where('colA', 'val1')
    ->orWhere('colB', 'val1')
    ->where('colC', 'val2')
    ->get();
-- 生成される SQL
WHERE colA = 'val1' OR colB = 'val1' AND colC = 'val2'
-- 実際の解釈
WHERE colA = 'val1' OR (colB = 'val1' AND colC = 'val2')

この場合、colA が val1 の行は colC の値に関係なくすべて取得されてしまいます。エラーにはならず「件数が多い」「削除済みのデータまで出る」といった形で表面化するため、気づきにくいバグになります。orWhere を使うときは、まずネストが必要かを考えるのが安全です。

よく使うパターン

キーワード検索(外側の変数を使う)

クロージャの中から外側の変数を参照するには use で渡します。PHP 7.4 以降ならアロー関数でも書けます。

$keyword = $request->input('q');

// use で渡す
Article::where('status', 'published')
    ->where(function ($query) use ($keyword) {
        $query->where('title', 'like', "%{$keyword}%")
            ->orWhere('body', 'like', "%{$keyword}%");
    })
    ->get();

// アロー関数(外側の変数を自動で参照できる)
Article::where('status', 'published')
    ->where(fn ($q) => $q->where('title', 'like', "%{$keyword}%")
                         ->orWhere('body', 'like', "%{$keyword}%"))
    ->get();
WHERE status = 'published' AND (title LIKE '%...%' OR body LIKE '%...%')

OR の中に AND のグループを作る

orWhere() にもクロージャを渡せます。

Article::where('category_id', 1)
    ->orWhere(function ($query) {
        $query->where('category_id', 2)
            ->where('is_featured', true);
    })
    ->get();
WHERE category_id = 1 OR (category_id = 2 AND is_featured = 1)

2 段以上の入れ子

クロージャの中でさらにクロージャを渡せば、何段でもネストできます。

Article::where(function ($q) {
        $q->where('colA', 'val1')
          ->orWhere(function ($q2) {
              $q2->where('colB', 'val1')
                 ->where('colD', '>', 10);
          });
    })
    ->where('colC', 'val2')
    ->get();
WHERE (colA = 'val1' OR (colB = 'val1' AND colD > 10)) AND colC = 'val2'

条件があるときだけネストを追加する

検索フォームのように入力の有無で条件を変える場合は when() と組み合わせます。

Article::query()
    ->when($keyword, function ($query, $keyword) {
        $query->where(function ($q) use ($keyword) {
            $q->where('title', 'like', "%{$keyword}%")
              ->orWhere('body', 'like', "%{$keyword}%");
        });
    })
    ->where('colC', 'val2')
    ->get();

Laravel 11 などの新しいバージョンでは、「複数カラムのどれかが条件に一致」を whereAny(['title', 'body'], 'like', "%{$keyword}%") と短く書けるメソッドもあります。使っているバージョンの公式ドキュメントで提供有無を確認してください。

生成される SQL の確認方法

意図どおり括弧が付いているかは、get() の代わりに次のメソッドで確認できます。

// プレースホルダ (?) 付きの SQL
$sql = Article::where(...)->where('colC', 'val2')->toSql();

// 値を埋め込んだ SQL(Laravel 10 系後半以降)
$sql = Article::where(...)->where('colC', 'val2')->toRawSql();

// その場で出力して処理を止める
Article::where(...)->where('colC', 'val2')->dd();

開発中は Laravel Debugbar や Telescope を入れておくと、実行されたすべての SQL を画面で確認できて便利です。

落とし穴

  • ローカルスコープ内の orWhere: 自作のスコープ(scopeXxx)の中で orWhere を使う場合も、スコープの中身をクロージャでくくっておかないと、呼び出し側の条件と混ざります
  • like 検索の値: ユーザー入力の % や _ はワイルドカードとして解釈されます。完全一致させたい場合はエスケープが必要です
  • インデックス: OR 条件や前方一致でない LIKE はインデックスが効きにくく、データが多いと遅くなります。EXPLAIN で実行計画を確認しましょう

関連

Post Share
子ページ

子ページはありません

同階層のページ
  1. SELECT
  2. INSERT
  3. UPDATE
  4. DELETE
  5. order by句のキャスト
  6. count / max / average (集計)
  7. 配列を条件にする方法
  8. where句の入れ子(ネスト)