Laravel Query BuilderはSQLの組立てを助けますが、すべての入力やループ内の取得が自動的に安全・高速になるわけではありません。値のバインド、識別子の許可、実行本数、更新処理の影響を分けて確認します。
最初に分けて確認する4点
- 値をプレースホルダーへ渡しているか。文字列連結したSQLの安全性をQuery Builderが補ってくれるとは考えない。
- 列名・テーブル名・並び順の指定は、実在する固定候補から選んでいるか。値のバインドと同じ防御ではない。
- 取得をループで繰り返していないか。実際のSQL本数と処理時間を別に測る。
- 更新・削除・トランザクションには業務上の条件とDB固有の制約があるか。例外時のrollbackだけで業務の正しさまで保証しない。
隔離MySQLでは、Query Builderだけでも親2行の後に子を逐次取得すると3本、一括のwhereIn取得なら2本になることを確認しました。これはfixtureの本数で、任意の実装が常にこの値になる保証ではありません。
公式: Query Builderの値/列名とupsert、DB接続と暗黙COMMIT。関連はページングとSQL観測。以下の元コードは、注意書きと対象DBを確認しながら参照してください。
| この記事の要点 |
|
Query Builder の基本
下のname順LIMIT10例は、同じ名前の行の順序まで確定しません。固定した結果境界が必要なら、id等の一意キーを決め手に追加します。first/valueは対象がない場合のnullも処理してください。
DB::table() から始めるメソッドチェーン形式で SQL を組み立てます:
use Illuminate\Support\Facades\DB;
// SELECT * FROM users WHERE active = 1
$users = DB::table('users')
->where('active', 1)
->get();
// SELECT id, name FROM users WHERE age >= 20 ORDER BY name LIMIT 10
$users = DB::table('users')
->select('id', 'name')
->where('age', '>=', 20)
->orderBy('name')
->limit(10)
->get();
// 単一行 / 単一カラム
$user = DB::table('users')->where('id', 1)->first();
$name = DB::table('users')->where('id', 1)->value('name');
$count = DB::table('users')->count();
WHERE 句のバリエーション
| メソッド | 生成される SQL |
|---|---|
where('age', '>', 20) | age > 20 |
whereIn('id', [1,2,3]) | id IN (1,2,3) |
whereBetween('age', [20,30]) | age BETWEEN 20 AND 30 |
whereNull('deleted_at') | deleted_at IS NULL |
whereExists(function ($q) {...}) | EXISTS (SELECT ...) |
orWhere('email', $x) | OR email = ? |
whereRaw('YEAR(created_at) = ?', [2026]) | 生 SQL(パラメータバインド) |
JOIN / GROUP BY / HAVING
// 内部結合 + 集計
$stats = DB::table('users')
->join('orders', 'users.id', '=', 'orders.user_id')
->select('users.name', DB::raw('SUM(orders.amount) AS total'))
->groupBy('users.id', 'users.name')
->having('total', '>', 10000)
->orderByDesc('total')
->get();
// 左外部結合
$users = DB::table('users')
->leftJoin('profiles', 'users.id', '=', 'profiles.user_id')
->select('users.*', 'profiles.bio')
->get();
// サブクエリ join
$latest = DB::table('logins')
->select('user_id', DB::raw('MAX(login_at) AS last'))
->groupBy('user_id');
$users = DB::table('users')
->joinSub($latest, 'l', fn($j) => $j->on('users.id', '=', 'l.user_id'))
->select('users.name', 'l.last')
->get();
selectRaw / whereExists / Union
// selectRaw で集計関数や式を直接書く
$rows = DB::table('orders')
->selectRaw('YEAR(created_at) AS year, SUM(amount) AS total')
->groupBy(DB::raw('YEAR(created_at)'))
->get();
// EXISTS サブクエリ
$users = DB::table('users')
->whereExists(function ($q) {
$q->select(DB::raw(1))
->from('orders')
->whereColumn('orders.user_id', 'users.id')
->where('orders.amount', '>', 10000);
})
->get();
// UNION
$a = DB::table('users')->where('type', 'admin');
$b = DB::table('users')->where('type', 'editor');
$all = $a->unionAll($b)->get();
INSERT / UPDATE / DELETE
次は書込・物理削除を含む保守用の例です。確認目的で実データへ実行しないでください。 Query BuilderはEloquentのモデルイベント/キャスト/SoftDeletes等を自動適用するものではありません。必要な権限・論理削除・制約を別途設計します。MySQL/MariaDBのupsertは第2引数だけで重複列を決定せず、実テーブルのPRIMARY/UNIQUE制約を使います。
// 単一 INSERT
DB::table('users')->insert([
'name' => 'Alice',
'email' => 'alice@example.com',
'created_at' => now(),
]);
// 一括 INSERT
DB::table('users')->insert([
['name' => 'Bob', 'email' => 'bob@example.com'],
['name' => 'Charlie', 'email' => 'charlie@example.com'],
]);
// INSERT して ID を取得
$id = DB::table('users')->insertGetId([
'name' => 'Dan',
'email' => 'dan@example.com',
]);
// UPDATE
$affected = DB::table('users')
->where('id', 1)
->update(['name' => 'Alice Smith']);
// UPSERT(PHP 8 + Laravel 8+)
DB::table('users')->upsert(
[['email' => 'a@x.com', 'name' => 'A'], ['email' => 'b@x.com', 'name' => 'B']],
['email'], // 重複検出キー
['name'] // 更新対象カラム
);
// DELETE
DB::table('users')->where('active', 0)->delete();
生 SQL を実行: DB::select / DB::statement
次のTRUNCATE・DDL・連結SQLは、実行を勧めない旧資料です。 TRUNCATEはデータを失わせ、MySQLの一部DDLは暗黙COMMITを起こします。transactionで囲めばすべて戻せるとは判断しません。SELECTも権限・負荷・返却列の検討が必要です。
// SELECT 系
$rows = DB::select('SELECT * FROM users WHERE active = ?', [1]);
// 戻り値が不要なステートメント (CREATE / ALTER / TRUNCATE)
DB::statement('TRUNCATE TABLE logs');
DB::statement('CREATE INDEX idx_email ON users(email)');
// DDL
DB::unprepared('CREATE TABLE backup_users AS SELECT * FROM users');
// プレースホルダ必須(SQL インジェクション対策)
// ❌ 危険
DB::select("SELECT * FROM users WHERE email = '{$email}'");
// ✅ 安全
DB::select('SELECT * FROM users WHERE email = ?', [$email]);
トランザクション
次の口座更新は例外時のrollbackを示す断片で、送金の完成実装ではありません。残高・対象行の存在・同時更新・二重実行等を保証せず、例外を飲み込んで正常終了すれば自動rollbackにはなりません。再試行される処理へ外部決済・メール送信を無条件に含めないでください。
// クロージャ版(例外で自動ロールバック・推奨)
DB::transaction(function () {
DB::table('accounts')->where('id', 1)->decrement('balance', 1000);
DB::table('accounts')->where('id', 2)->increment('balance', 1000);
DB::table('transfers')->insert([
'from' => 1, 'to' => 2, 'amount' => 1000,
]);
});
// 手動制御版
DB::beginTransaction();
try {
// ...
DB::commit();
} catch (\Throwable $e) {
DB::rollBack();
throw $e;
}
// デッドロック時のリトライ回数(第 2 引数)
DB::transaction(function () { /* ... */ }, 3);
SQL インジェクション対策
通常のwhere等の条件値と、テーブル/列名・SQL式は別です。識別子は値としてバインドできないため、並び替え列等はアプリ側の固定許可リストから選びます。whereRawは第2引数へ渡す値をバインドできますが、生SQL文字列自体に入力を連結してよいわけではありません:
// ✅ 自動バインド
DB::table('users')->where('email', $email)->get();
// → SELECT * FROM users WHERE email = ?
// バインド: [$email]
// ✅ whereRaw でもバインドできる
DB::table('users')->whereRaw('LOWER(email) = ?', [strtolower($email)])->get();
// ❌ 絶対やってはいけない(文字列連結)
DB::select("SELECT * FROM users WHERE email = '$email'");
Eloquent との使い分け
| 場面 | 推奨 | 理由 |
|---|---|---|
| 1 件取得 → 加工 → 保存 | Eloquent | モデルのビジネスロジックを通す |
| 複雑な集計レポート | Query Builder | SQL を直に書く方が見通し良い |
| 大量バルク INSERT | Query Builder + insert() | Eloquent はモデル生成のオーバヘッドあり |
| リレーション込みの取得 | Eloquent + with() | N+1 回避が容易 |
| 外部 DB の参照 | Query Builder | モデル不要 |
発行された SQL を確認
下のgetQueryLog/ddにはバインド値や個人情報が出る場合があります。公開画面や本番で試さず、専用の隔離データで確認してください。toSqlは実行結果ではなく1クエリの組立て結果です。
// toSql() で生 SQL(プレースホルダのまま)
$sql = DB::table('users')->where('id', 1)->toSql();
// → SELECT * FROM users WHERE id = ?
// バインドパラメータ
$bindings = DB::table('users')->where('id', 1)->getBindings();
// → [1]
// クエリログを取る(開発時のみ)
DB::enableQueryLog();
DB::table('users')->get();
dd(DB::getQueryLog());
FAQ
Q: get() と first() と value() の違い
A: get() はコレクション、first() は 1 行(オブジェクト or null)、value('col') は 1 カラムの値のみ。
Q: chunk / chunkById の違い
A: chunkのOFFSETとchunkByIdのキー条件を区別します。chunkByIdも処理中のキー/条件変更や同時更新を一律に安全にするものではありません。JOIN時の対象キー/別名やwhereの論理グループも確認し、固定データと変更を含む場合を別々に検証します。
Q: Query Builder で N+1 は起きる?
A: Query Builderでも、親一覧の後にループ内で子を1回ずつ取得すれば1+N本になります。Eloquentの自動取得に限った問題ではありません。whereInによる一括取得やJOIN/集計等を検討しますが、行数・重複・並び順・メモリと取得権限を前後で照合します。
- インストールと設定
- クイックスタート & チュートリアル(初心者向け)
- クイックスタート & チュートリアル(中級者向け)
- ルーティング
- Bladeテンプレート(ビュー/レイアウト)
- コントローラー
- マイグレーションとテーブル定義
- データベースの設定
- Eloquentモデル (ORM)
- SQLとクエリビルダー
- バリデーション
- .envファイルの設定値へのアクセス
- 動作環境による分岐処理
- configフォルダ配下の設定値へのアクセス
- assetヘルパーを利用したpublicフォルダへのアクセス
- storageフォルダへのアクセス
- アプリケーション名の変更
- メンテナンス
- ログイン画面(認証システム)の作成
- ログインの必須化
- ログインユーザー情報の取得
- ルートの認証化
- 本番サーバーへのデプロイ方法
- 多言語化
- csrf_field
- ファイルのダウンロード
- CSVのアップロードおよび読み込み(maatwebsite/excel)
- ページタイトルの設定
- コマンド一覧
- エラー一覧
- SQLの実行ログ出力方法
- キャッシュのクリア
- Selectの結果の最初もしくは最後に任意の値を追加する方法
- ajaxでPOST通信する際の注意点
- ソーシャルログインの実装
- セッション情報の確認
- ログイン、ユーザー登録、パスワードリセット後のリダイレクト先の変更方法
- redirectやreturn viewにメッセージを付与する方法
- クッキー(cookie)の設定と取得
- クラスの再読み込み
- csrfの有効時間を変更する方法
- ViewComposerを用いてviewに共通の値を付与する方法
- View::shareを用いて共通の値を各ビューに渡す方法
- ミドルウェアを用いた処理の共通化
- Middleware内でAuth::check()などを使用する方法
- Controller以外でリダイレクトする方法
- セッションの値の取得/保存/更新/削除
- $requestの値を変更する方法
- 常時SSL化
- ページング(ページネーション)をする方法
- vue.jsとの連携
- Vue.jsと連携するSPA実行環境構築
- .envの値をvue.jsで参照する方法
- vue.jsを本番環境にリリースする方法
- could not find driver(Windows, MySQL編)
人気ページ
- 1 Eclipseで「サーバーに追加または除去できるリソースがありません。」の原因と対処法
- 2 tomcat の起動 / 停止ログと catalina.log・catalina.out の違い
- 3 JavaScript で base URL を取得する方法|window.location.origin
- 4 YouTube Data API v3 エラー一覧|403・400・404 の原因と対処
- 5 Laravel エラー一覧|500/Blade/DB 接続/ルーティングの代表エラー
- 6 3Dグラフィックスのソフト一覧|Blender・Metasequoiaと用途別の選び方
- 7 Spring Frameworkのアノテーション一覧
- 8 【Spring】@Valueアノテーションとは
- 9 CATALINA_HOME の確認方法 (Linux / Mac)
- 10 【Spring】@Autowiredアノテーションとは
最近更新/作成されたページ
- Visual Studio「デザイナーを表示できません」エラーの原因と対処 2026-10-28 12:06:00
- SSH 認証に失敗しました。再試行してください の対処 2026-10-28 12:06:00
- SSH2 秘密鍵の読み込みに失敗しました の原因と対処 2026-10-28 12:06:00
- TeraTerm のよくあるエラーと対処法完全ガイド — Connection refused / 2026-10-28 12:06:00
- Visual Studio エラー一覧|コンパイルエラー・警告の確認方法と対処 2026-10-28 11:39:57
- Whitelabel Error Page の原因と消し方|Spring Boot のエラーページ 2026-10-28 11:39:57
- Eclipseで「サーバーに追加または除去できるリソースがありません。」の原因と対処法 2026-10-28 11:39:57
- SQL WHERE 句完全ガイド 2026-10-28 11:39:57
- SQL JOIN 完全ガイド (INNER/LEFT/RIGHT/FULL OUTER) 2026-10-28 11:39:57
- tomcat の起動 / 停止ログと catalina.log・catalina.out の違い 2026-10-28 10:24:23
- Tomcat の起動確認方法 5 つ|localhost:8080・ポート・プロセス・ログ 2026-10-28 10:24:23
- Oracle 19c COMMITの使い方: 暗黙コミット・自動確定・応答不明時の確認 2026-10-28 09:59:09
- Oracle 19c DELETE文|削除対象の確認・COMMIT前の判断・ROLLBACKの条件 2026-10-28 09:59:09
- Oracle 19c INSERT文|INSERT ALL・SELECT・RETURNINGとMERGEの使い分け 2026-10-28 09:59:09
- Oracle 19cのトランザクション制御|COMMIT・ROLLBACK・分離レベルと接続設定 2026-10-28 09:59:09