0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Laravelの with() では並び替えできなかった話と、join() に切り替えた理由

0
Posted at

一覧ページで、よくある要望をもらった。

「表示しているカラム全部に、昇順・降順の並び替え(ソート)機能をつけてほしい」

最初は
「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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?