この内容は古いバージョンです。最新バージョンを表示するには、戻るボタンを押してください。
バージョン:8
ページ更新者:爽健
更新日時:2026-06-11 07:12:00

タイトル: SQLとクエリビルダー
SEOタイトル: Laravel Query Builder 完全ガイド(DB ファサード / join / トランザクション /

この記事の要点
  • Query Builder は DB::table("users")->where(...)->get() のように生 SQL をラップして安全に書ける
  • パラメータは自動バインドされ SQL インジェクションを防止。生 SQL を書くなら whereRaw("col = ?", [$x])
  • join / groupBy / having / unionAll も全てメソッドチェーンで表現可能
  • トランザクションは DB::transaction(function () { ... })。例外で自動ロールバック
  • Eloquent との使い分け: 単純な集計・複雑な join は Query Builder、ドメインモデルを介す処理は Eloquent

Query Builder の基本

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

// 単一 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

// 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]);

トランザクション

// クロージャ版(例外で自動ロールバック・推奨)
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 インジェクション対策

Query Builder はすべてのパラメータを自動バインドします。whereRaw でも第 2 引数の配列にすればバインドされます:

// ✅ 自動バインド
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 BuilderSQL を直に書く方が見通し良い
大量バルク INSERTQuery Builder + insert()Eloquent はモデル生成のオーバヘッドあり
リレーション込みの取得Eloquent + with()N+1 回避が容易
外部 DB の参照Query Builderモデル不要

発行された SQL を確認

// 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() は主キーで進めるので安全。

Q: Query Builder で N+1 は起きる?
A: Query Builder はそもそもリレーションを自動取得しないので発生しません。Eloquent でのみ with() による Eager Loading が必要です。