🧺

Laravelで知ってると差がつくEloquentとQuery Builder(+おまけで現場Tipsも)

に公開

「LaravelのEloquentとQuery Builderの違いって、なんとなくわかってるつもりだったけど説明できない」
そんな人、意外と多いと思います。私もそうでした。

同じように
wheregetも動くし、なんかgroupByとかsortも使えるし」
「でもsave()できたりできなかったりするんだよな」
というモヤモヤを感じたことがあるなら、この記事はたぶんちょうどいい。
んなもん知ってるわ!という方は、Switchでハデス2にハマりましょう。おすすめです。

Eloquent、Query Builder、Model、Collection。
全部Laravelでは“当たり前に使う”けど、それぞれの階層を理解するとコードの意味が一気にクリアになります。

・・・最近一緒に仕事をしている人がとても大人なLaravelコードを書いていたので悔しくて一生懸命調べましたw


EloquentとQuery BuilderとModelの関係

ざっくり言うと、三者の関係はこう。

Eloquent Model(Userなど)
  └── Query Builderを内部的に利用
        └── SQLを生成してDBに投げる

つまり:

  • Model:テーブルを表すクラス(Userモデルなど)
  • Eloquent:Modelを通してDBとやりとりするORM(Object Relational Mapper)
  • Query Builder:SQLをオブジェクト的に組み立てるための仕組み

例を見てみよう。

// Query Builder
DB::table('users')->where('id', 1)->first();

// Eloquent ORM
User::where('id', 1)->first();

// Model経由
User::find(1);

どれも「ユーザーを1件取る」けど、
返ってくる中身が違う。

呼び出し方 戻り値の型 備考
DB::table() stdClass 生のオブジェクト。save()できない
User::where() User(Eloquentモデル) save()できる/イベントも効く
User::find() User(Eloquentモデル) 主キー検索のショートカット

Query Builderは“素のSQL”に近く、
Eloquentは“ORM(オブジェクトマッピング)”層にいる。
同じように見えても扱う世界が違うわけです。


Query Builderでしかできないこと

Eloquentが便利なのは間違いないけど、
Query BuilderにはBuilderでしかできないことが結構あります。

複雑なJOIN・集計・サブクエリ

DB::table('users as u')
  ->join('orders as o', 'u.id', '=', 'o.user_id')
  ->select('u.name', DB::raw('COUNT(o.id) as orders'))
  ->groupBy('u.id')
  ->having('orders', '>', 3)
  ->get();

こういう集計系や複雑な結合は、
Eloquentでも書けなくはないけどBuilderの方が明快で高速。

一括更新・大量INSERT

DB::table('users')->insert([
  ['name' => 'Taro'],
  ['name' => 'Jiro'],
]);

Eloquent::create()save()だと1件ずつINSERTするので遅い。
バルク処理ならBuilder一択。

生SQLそのまま投げたいとき

DB::select('SELECT * FROM users WHERE created_at > NOW() - INTERVAL 1 DAY');

こういうやむを得ないケースも、Query Builder側の領域。


Eloquentでしかできないこと

逆に、Eloquentの特権もたくさんある。

モデルイベント

protected static function booted()
{
    static::creating(function ($user) {
        $user->uuid = Str::uuid();
    });
}

creating, created, updating …などのイベントはBuilderにはない。
“DB変更のライフサイクルフック”が使えるのがEloquentの強み。

リレーション(hasMany / belongsTo など)

$user = User::find(1);
$posts = $user->posts;

Query BuilderだとJOINを自分で書く必要があるけど、
Eloquentならモデルの関係をそのままオブジェクトで扱える。

アクセサ / ミューテータ

public function getFullNameAttribute()
{
    return "{$this->first_name} {$this->last_name}";
}

$user->full_name で自動的に結合される。
実データを“見せ方”として加工できるのはEloquentならでは。


CollectionはSQLを発行しない

Eloquentでget()した後にgroupBy()sort()しているコード、見たことないですか?

User::select()->get()->sort();

これ、SQLを発行していません
get()でDBアクセスが終わっていて、
sort()PHPのメモリ上でソートしているだけです。

大量件数だとこれがボトルネックになります。
ソートやグルーピングはDB側でやる方が圧倒的に速い。

// DBでソート
User::orderBy('created_at', 'desc')->get();

Collectionは便利だけど、
「SQLを出してるのか、メモリで動いてるのか」を意識しておきたい。


よくある混乱ポイント

first()get()の違い

  • first() → モデル1件 or null
  • get() → Collection(複数件)

get()->first()は“2回フェッチしてる”ようなもので無駄です。
1件ならfirst()でいい。


pluck()の引数2つ

User::pluck('name', 'id');

pluck(value, key) の順。
つまり [id => name] の連想配列的Collectionになります。
※ Eloquentの場合は「Collection」、Query Builderの場合は「配列」になるので注意!


creatingidはまだ取れない

creatingイベントはINSERT前に発火するので、
$model->id はまだ未確定です。
使いたいなら created イベントへ。


tap() は外の変数も使える

$count = 0;

User::query()
  ->tap(function ($q) use (&$count) {
      logger($q->toSql());
      $count++;
  })
  ->get();

useでキャプチャすれば普通に使える。
tap()は「処理を挟んで同じインスタンスを返す」ための小技。


おまけ:現場で差がつくTips集

SQLを確認する

User::where('status', 'active')->toSql();

toSql()は実行せずにSQL文字列を返す。
ログ出力にも便利。


条件のグルーピング

User::where(function($q) {
    $q->where('active', true)
      ->orWhere('role', 'admin');
});

複雑なORを安全に書くならこれ。
地味だけどめっちゃ重要。


遅延ロードを検知する

Model::preventLazyLoading(! app()->isProduction());

N+1問題の温床を開発中に潰せる。
Eloquentの挙動を“見える化”しておくのはおすすめ。


hydrate()でDB::select()の結果をModel化する

$rows = DB::select('SELECT * FROM users WHERE active = 1');
$users = User::hydrate(array_map(fn($r) => (array) $r, $rows));

これでsave()toArray()も使える。
JOIN結果にエイリアスを付けておけば安全に動く。


🧰 モデルイベントをもっと活用する

Eloquentの強みの一つが「イベントをフックできる」こと。
creatingcreatedupdating などは、DB操作の直前・直後に呼ばれます。

代表的なイベント一覧

イベント名 タイミング よく使う用途
creating INSERT前 UUID発行、デフォルト値セット、バリデーション
created INSERT後 関連レコードの作成、通知発火、ログ出力
updating UPDATE前 値の整形、変更検知
updated UPDATE後 更新履歴の保存、外部API呼び出し
deleting DELETE前 関連データの削除確認
deleted DELETE後 監査ログ、論理削除の補助

例1:creating で UUID を自動発行

protected static function booted()
{
    static::creating(function ($model) {
        if (! $model->uuid) {
            $model->uuid = (string) Str::uuid();
        }
    });
}

creating は INSERT 前に呼ばれるので、
「DBに入る前に属性を整える」処理を書くのに向いています。


例2:created で 関連レコードを自動生成

protected static function booted()
{
    static::created(function ($user) {
        Profile::create([
            'user_id' => $user->id,
            'nickname' => $user->name,
        ]);
    });
}

created は INSERT 後なので、自動採番の id が確定しています。
「登録後に付随レコードを作る」や「通知を飛ばす」系はこっち。


例3:updating / updated で変更検知

protected static function booted()
{
    static::updating(function ($user) {
        if ($user->isDirty('email')) {
            Log::info('Email changed', ['user_id' => $user->id]);
        }
    });
}

isDirty() は変更があったカラムだけを検出できる便利メソッド。
更新トリガーで監査や履歴を残すときに使える。


🧩 tap() をちゃんと理解する

Laravelの tap() は、
「処理の途中に副作用を挟みつつ、値そのものはそのまま返す」関数。
これがシンプルだけど非常に便利。

パターン1:グローバル関数 tap()

$user = tap(User::find(1), function ($u) {
    $u->last_accessed_at = now();
    $u->save();
});

tap() は第1引数の値をコールバックに渡し、
その値自体を戻します。
副作用(ここでは更新)を行いながら変数を返すことができる。

return tap($user)->notify(new WelcomeMail);

↑ “メソッドチェーンを切らずに処理を挟む” のに使える。


パターン2:Builderの tap() メソッド

User::query()
    ->tap(function ($query) {
        logger($query->toSql());
    })
    ->where('active', 1)
    ->get();

これは クエリビルダに対して副作用を差し込む 用。
tap() の中ではSQLがまだ実行されていないので、
toSql()getBindings() のような「組み立て中の確認」ができる。


パターン3:変数の初期化やトランザクションの補助にも

$user = tap(new User(['name' => 'Taro']), fn($u) => $u->save());

あるいはトランザクション内で:

DB::transaction(function () {
    tap(Order::create([...]), function ($order) {
        $order->items()->createMany([...]);
    });
});

“結果を戻しながら何かする” というLaravelらしい文法糖。
書いてるうちにクセになります。


まとめ

  • Query Builderは“SQL職人”向け。
    → JOIN・集計・バルク更新・速度優先。
  • Eloquentは“オブジェクト指向的にDBを扱いたい人”向け。
    → モデルイベント・アクセサ・関連・キャストが強い。
  • Collectionは“取得後のデータ整形”。
    → SQLは発行されない。

この3つを混同せず、
「今はどの層で動いているのか」を意識できると、
Laravelがぐっと読みやすく、チューニングしやすくなります。

oneframe

Discussion