モデルをDBから取得するときや、oneToManyのリレーションを並び替えるときに、複数の条件で並び替えたい場合があります。1つ目の条件では値が同じで並び順が決定できず、2つ目の条件で優先順位を決定したい、というケースです。
結論から言うと、方法は2つです。`orderBy()` を必要な数だけ連続でつなぐか、`orderByRaw()` でORDER BY句を直接書きます。どちらもEloquentモデル・クエリビルダー・リレーションのいずれでも同じように使えます。以下でそれぞれの書き方と、内部でどう動いているか、ハマりやすいポイントまで解説します。
複数条件での並び替え方法1 orderByを連続
単純にorderByを複数つなげることで、複数の並び替え条件となります。
Profile::orderBy('ranked_at', 'desc')
->orderBy('rank', 'asc')
->get();
上記の例だと `ORDER BY `ranked_at` DESC, `rank` ASC` というクエリになります。書いた順番がそのままORDER BYの優先順位になるため、まず並び替えたい条件を先に書くのがポイントです。
ここで使っている `orderBy()` は、実はEloquentの `IlluminateDatabaseEloquentBuilder` 自身が持つメソッドではありません。`Builder` クラスのdocコメントには `@mixin IlluminateDatabaseQueryBuilder` と書かれており、`ForwardsCalls` トレイトを使ってクエリビルダー(`IlluminateDatabaseQueryBuilder`)へ処理を転送しています。つまり `Model::orderBy(…)` も内部的にはクエリビルダーの `orderBy` を呼んでいるだけなので、EloquentでもDBファサードのクエリビルダーでも挙動は同じです。
// リレーションでも同じ書き方が使える
$user->posts()
->orderBy('published_at', 'desc')
->orderBy('id', 'desc')
->get();
複数条件での並び替え方法2 orderByRaw
orderByRawを使えば自由にORDER BY句を記述することができます。もしFIELD関数などを使用したい場合はこちらで書いても良いでしょう。
Profile::orderByRaw('ranked_at desc, rank asc')
->get();
`orderByRaw` はカラム名やDESC/ASCをまとめて1つの文字列で渡せるため、`orderBy` を連続で書くより短くまとまります。特定の値の順に並べたい場合は、MySQLの `FIELD()` 関数と組み合わせると便利です。
// 指定したstatusの並び順で取得する
Profile::orderByRaw('FIELD(status, ?, ?, ?)', ['active', 'pending', 'banned'])
->get();
このように第2引数へバインド値を渡せば、変数を安全に埋め込めます。ユーザー入力を文字列連結でORDER BY句に埋め込むとSQLインジェクションの原因になるため、可変値は必ずバインドで渡してください。
複数条件の並び替えでハマりやすい落とし穴
複数条件のorderByは書き方自体は簡単ですが、意図と違う結果になりやすいポイントがいくつかあります。
- 条件の順番=優先順位:orderByを書いた順にORDER BYへ追加されるため、順番を入れ替えると結果が変わります。「同点のときの二次ソート」を後ろに書くのが基本です。
- NULLの並び順:DESCで並べるとNULLが先頭・末尾のどちらに来るかはDBによって異なります。狙った位置に固定したい場合は `orderByRaw(‘ranked_at IS NULL, ranked_at desc’)` のように制御します。
- orderByRawは検証されない:`orderByRaw` に渡した文字列はそのままSQLに展開されます。カラム名を動的に組み立てる場合はホワイトリストで検証し、可変値はバインドを使いましょう。
- 大量データのソート:`model:prune` のようなバッチ処理でチャンク取得しながら並び替える場合は、ソートに使うカラムへインデックスを張っておかないと遅くなります。
複数条件の並び替えは、まず「同じ値だったときにどのカラムで決着をつけたいか」を整理してから、その優先順位どおりに `orderBy` を並べるか `orderByRaw` にまとめると、意図した順序を確実に再現できます。