一覧ページで、よくある要望をもらった。
「表示しているカラム全部に、昇順・降順の並び替え(ソート)機能をつけてほしい」
最初は
「JS でフロント側の配列をソートすれば良くない?」とも思った。
だが、ページネーションをしているため、結局は バックエンド(DB)でソートする必要があった。
そこで問題になったのがリレーションのソート。
一覧では works テーブルの値だけでなくリレーション先(branches テーブル)の値(仮)も表示している
この「リレーション先の値」で並び替えをしたかったのですが、
当初は with() でリレーションを読み込んでおり、
そのまま orderBy('branch.name') しようとしてエラーに。
後から振り返ると、with() がいつ動いているかを理解していなかったのが原因だったので、
自分用のメモとして整理しておきます。
まず整理:with() と join() の役割の違い
with()
役割:
リレーション先を「あとからまとめて取りに行く」(N+1 対策の eager load)
動き方
まずメインテーブル(今回なら works)を SELECT
その結果に含まれる branch_id を集める
2で集めた ID 一覧を使って、branches テーブルを 別クエリでまとめて取得
取得した Branch を、PHP側で $work->branch に紐づける
join()
役割:
テーブル同士を「SQLの中で結合」する
動き方
1つの SQL の中で、結合したテーブルのカラムを使って絞り込み・並び替えができる
だから
リレーション先の値で sort(orderBy)したい
with() は無理。
join()で、SQLレベルで結合する必要がある。
なぜ with() だとソートできないのか
例として「作業 works を、リレーションの branch.name でソートしたい」とする。
Work::with('branch')
->orderBy('branch.name') // ← これがうまく動かない
->get();
理由:
with('branch') のeager loradingは下記のように動く:
まず works テーブルに対して
select * from works order by branch.name ...
を実行しようとする。
そもそも branch.name というカラムは works テーブル側にはないのでエラー。
その後、branch_id を集めて
select * from branches where id in (...)
みたいなクエリで別 SQL として branch を取りに行く
つまり
orderBy が実行されるタイミングでは、リレーションはまだ結合されていない。
だから branch.name というカラムは SQL 上に存在しない。
join() を使うとどうなるか
同じことを join を使って書くと:
$works = Work::query()
->select('works.*')
->join('branches', 'branches.id', '=', 'works.branch_id')
->orderBy('branches.name') // ← 結合したテーブルのカラムでソートできる
->get();
この場合、
実際の SQL イメージ:
select works.*
from works
inner join branches on branches.id = works.branch_id
order by branches.name;
branches.name は SQLの中にちゃんと存在するカラムになる。
よって orderBy('branches.name') が普通に効く。
まとめ
with()の挙動を知ること。with()はメインのクエリの後に、その結果を用いて、リレーション先の実行している
Work::with('branch')
->get();
select * from `works`;
select * from `branches`
where `branches`.`id` in (1, 2, 5); //works.branch_idの一覧
その結果、PHP側では
$works = Work::with('branch')->get();
foreach ($works as $work) {
echo $work->branch->name;
}
を行っても、 works の数だけクエリが走ることがなくなる。
注意点
ちなみに、joinの後にforeachを回すときは、withでロードしていないと、N+1問題が発生する。
Eloquent はプロパティアクセス($work->branch)されたときにまず relations を見る。
なければリレーションメソッドを実行して取りに行く(これが N+1 の原因になるやつ)。
つまりは、join をして、並び替えを行い、 with() で eager loading をするのが、良いということになりそうだ。
参照
https://laravel.com/docs/12.x/eloquent-relationships?utm_source=chatgpt.com#eager-loading