【Laravel】Query Builderの基本:Eloquentを使わないクエリの書き方

laravelアイキャッチ Laravel

こんにちは、かつコーチです。

前回はFactoryでテストデータを効率よく作る方法を紹介しました。

これまでの記事では Article::where(...) のようにEloquentモデルを経由してデータを取得してきましたが、実はLaravelにはモデルを介さずに直接クエリを組み立てる Query Builder という仕組みもあります。

今回はこのQuery Builderの基本と、Eloquentとの使い分けを解説します。

Query Builderとは?

Eloquentとの違い

Query Builderとは、DB ファサードを使って、SQLに近い感覚でクエリを組み立てる機能です。

Eloquentは「モデルというオブジェクトを介してデータを扱う」仕組みでしたが、Query Builderは「テーブルに対して直接クエリを発行する」というシンプルな仕組みになっています。

EloquentQuery Builder
書き方Article::where('status', '公開')->get()DB::table('articles')->where('status', '公開')->get()
戻り値モデルのコレクション汎用オブジェクトのコレクション
モデルのメソッド使える(アクセサ・リレーション等)使えない
向いている場面通常のCRUD、リレーションを扱う処理集計処理、モデル化されていないテーブル操作

「モデルという仕組みを一枚挟むか、直接テーブルを叩くか」の違いだとイメージすると分かりやすいです。

なぜEloquentだけでなくQuery Builderも知っておくべきか

普段の開発ではEloquentを使う場面が大半です。

ただ、集計クエリやログテーブルの操作など、「モデルの機能はいらないから、とにかく軽くクエリを発行したい」という場面では、Query Builderの方がシンプルに書けることがあります。

実はEloquentも、内部的にはこのQuery Builderの上に構築されています。

つまりQuery Builderの書き方を理解しておくと、Eloquentの where()orderBy() がなぜそう動くのかも自然と理解できるようになります。

基本の書き方

DBファサードでテーブルを指定する

Query Builderは、DB::table('テーブル名') から始めます。

<?php

use Illuminate\Support\Facades\DB;

// 全件取得
$articles = DB::table('articles')->get();

// 条件を指定して取得
$articles = DB::table('articles')
    ->where('status', '公開')
    ->get();

// 1件だけ取得
$article = DB::table('articles')->where('id', 1)->first();

where()get() の書き方はEloquentとほぼ同じなので、Eloquentに慣れていれば違和感なく読めると思います。

大きな違いは、戻り値が Article モデルのインスタンスではなく、stdClass という汎用オブジェクトになる点です。

絞り込み・並び替え・件数指定

実務でよく使う条件指定をまとめておきます。

<?php

use Illuminate\Support\Facades\DB;

$articles = DB::table('articles')
    ->where('status', '公開')
    ->where('published_at', '<=', now())
    ->orderBy('published_at', 'desc')
    ->limit(10)
    ->get();

Eloquentと同じように、where() をメソッドチェーンでつなげて条件を積み重ねていけます。

集計関数を使う

Query Builderが特に活躍するのが、件数や合計値を求める集計処理です。

<?php

use Illuminate\Support\Facades\DB;

$count = DB::table('articles')->where('status', '公開')->count();

$total = DB::table('orders')->sum('price');

$average = DB::table('reviews')->avg('rating');

$max = DB::table('products')->max('price');

count() / sum() / avg() / max() / min() は、集計結果だけを1件の値として返してくれます。

「件数だけ知りたいのに、Eloquentで全件取得してから count($collection) するのは無駄が多い」という場面で、これらのメソッドは特に効果を発揮します。

更新・削除・生SQL

insert・update・delete

データの登録・更新・削除も、Query Builderから直接行えます。

<?php

use Illuminate\Support\Facades\DB;

// 登録
DB::table('articles')->insert([
    'title' => '新しい記事',
    'status' => '下書き',
    'created_at' => now(),
    'updated_at' => now(),
]);

// 更新
DB::table('articles')
    ->where('id', 1)
    ->update(['status' => '公開']);

// 削除
DB::table('articles')->where('id', 1)->delete();

Eloquentの save()delete() と似た感覚で使えますが、モデルのイベント(createdupdated などのフック)は発火しない点には注意が必要です。

どうしても複雑なSQLを書きたいとき

集計や結合が複雑になりすぎて、Query Builderのメソッドチェーンでも表現しづらい場合は、生のSQLを直接実行することもできます。

<?php

use Illuminate\Support\Facades\DB;

$articles = DB::select(
    'SELECT * FROM articles WHERE status = ? ORDER BY published_at DESC LIMIT ?',
    ['公開', 10]
);

? のプレースホルダーに配列で値を渡す形式にしておけば、後述するSQLインジェクション対策も自動でされます。

つまずきやすいポイント:条件を文字列で組み立ててしまう

SQLインジェクションのリスク

以前の記事でも触れたSQLインジェクションの話は、Query Builderを直接使うときにも忘れてはいけません。

私が実際にレビューで指摘を受けたのが、検索キーワードを文字列連結でクエリに埋め込んでしまったケースです。

❌ Before:ユーザー入力を文字列連結でそのまま埋め込む

<?php

use Illuminate\Support\Facades\DB;

$keyword = request('keyword'); // ユーザーが入力した検索キーワード

$articles = DB::select(
    "SELECT * FROM articles WHERE title LIKE '%" . $keyword . "%'"
);

この書き方だと、$keyword' OR '1'='1 のような悪意ある文字列を入れられると、意図しないデータまで取得されてしまう危険があります。

✅ After:プレースホルダーを使って値を安全に渡す

<?php

use Illuminate\Support\Facades\DB;

$keyword = request('keyword');

$articles = DB::table('articles')
    ->where('title', 'like', '%' . $keyword . '%')
    ->get();

// 生SQLを使う場合もプレースホルダーを使う
$articles = DB::select(
    'SELECT * FROM articles WHERE title LIKE ?',
    ['%' . $keyword . '%']
);

Query Builderのメソッド(where など)を使う場合は、内部で自動的にプレースホルダー化されるため安全です。

どうしても生SQLを書く場合も、値を直接文字列に埋め込まず、必ず ? とパラメータの配列で渡すようにしましょう。

応用:Eloquentのクエリの中でQuery Builderの記法を使う

Eloquentから素のクエリに近い操作をする

Eloquentを使っている途中でも、toBase() を挟むとQuery Builderに近い形で結果を扱えます。

<?php

use App\Models\Article;

// Eloquentモデルではなく、軽量なstdClassの配列として取得する
$articles = Article::where('status', '公開')
    ->toBase()
    ->get();

toBase() を使うと、Eloquentのモデル化(アクセサの適用やイベント発火など)をスキップして、より軽量な形で結果を取得できます。

大量データを集計するだけで、モデルとしての機能が不要な場面では、パフォーマンス改善の選択肢として覚えておくと役立ちます。

まとめ

この記事のポイント

  • Query Builderは DB::table() からテーブルに直接クエリを発行する仕組み
  • Eloquentも内部的にはQuery Builderの上に成り立っている
  • count / sum / avg などの集計処理はQuery Builderの方がシンプルに書ける
  • 生SQLを書く場合も、文字列連結ではなくプレースホルダーで値を渡してSQLインジェクションを防ぐ
  • toBase() を使えば、Eloquentのクエリを軽量なQuery Builder形式で扱うこともできる

次に読むべき記事

Eloquentでリレーションを扱ううちに必ず出会う、パフォーマンスの落とし穴「N+1問題」を次は見ていきましょう。

→ 次の記事:N+1問題とは?eager loading(with)で解決する

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