こんにちは、かつコーチです。
前回はFactoryでテストデータを効率よく作る方法を紹介しました。
これまでの記事では Article::where(...) のようにEloquentモデルを経由してデータを取得してきましたが、実はLaravelにはモデルを介さずに直接クエリを組み立てる Query Builder という仕組みもあります。
今回はこのQuery Builderの基本と、Eloquentとの使い分けを解説します。
Query Builderとは?
Eloquentとの違い
Query Builderとは、DB ファサードを使って、SQLに近い感覚でクエリを組み立てる機能です。
Eloquentは「モデルというオブジェクトを介してデータを扱う」仕組みでしたが、Query Builderは「テーブルに対して直接クエリを発行する」というシンプルな仕組みになっています。
| Eloquent | Query 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() と似た感覚で使えますが、モデルのイベント(created や updated などのフック)は発火しない点には注意が必要です。
どうしても複雑な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)で解決する