概要
複数テーブルが関連付けられているデータを子テーブルの条件で絞り込みたい場合。クエリービルダーのJOIN
句によってテーブルを結合すると結果は配列となるが、配列ではなくオブジェクトのままリレーションで操作したいとき。
whereHas()
メソッドを使うと、親のモデルでhasMany()
で定義された子モデルの操作が可能になる。
1 2 3 |
親モデル::whereHas('子モデル', function($query) { $query->where('子モデルの属性', '演算子', '値'); ])->get(); |
whereHas()
の使い方は以下のとおり。
- 第1引数で子モデル名を指定
- 第2引数にクエリーを引数としたコールバックを定義
- コールバックで引数のクエリーに対してクエリービルダーメソッドを適用
- 最後の
get()
を忘れがちなので注意
準備
クエリービルダーの確認で使ったテーブルで確認する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
mysql> SELECT * FROM customers; +----+-----------+ | id | name | +----+-----------+ | 1 | customer1 | | 2 | customer2 | | 3 | customer3 | +----+-----------+ 3 rows in set (0.00 sec) mysql> SELECT * FROM emails; +----+-------------+--------------------+ | id | customer_id | email | +----+-------------+--------------------+ | 1 | 1 | customer1@mail.com | | 2 | 2 | customer2@mail.com | +----+-------------+--------------------+ 2 rows in set (0.00 sec) mysql> SELECT * FROM orders; +----+-------------+---------------------+---------------+ | id | customer_id | ordered_at | item | +----+-------------+---------------------+---------------+ | 1 | 1 | 2020-06-05 00:00:00 | screw | | 2 | 2 | 2020-06-05 00:00:00 | rubber sheet | | 3 | 2 | 2020-06-06 00:00:00 | plastic plate | | 4 | 1 | 2020-06-07 00:00:00 | rubber sheet | | 5 | 3 | 2020-06-08 00:00:00 | wire | +----+-------------+---------------------+---------------+ 5 rows in set (0.00 sec) |
1つ先のリレーション
親が直接持つ子モデルの場合、以下の様に記述する。ここでは子モデルのcustomer
の名前が'customer2'
のデータを抽出している。
抽出結果は親モデルのコレクションとしてビューに渡している。
1 2 3 4 5 6 7 8 9 10 |
public function relation() { $orders = Order::whereHas('customer', function($query){ $query->where('name', '=', 'customer2'); })->get(); return view('queries.relation', [ 'title' => 'Query Test', 'orders' => $orders, ]); } |
モデルのコレクションを受け取ったビューでは、親モデルとしてこれらを扱い、必要に応じてリレーションによって子モデルの属性を取り出す。
email
を持たないcustomer
に対応するため、optional()
メソッドを使っている。
1 2 3 4 5 6 7 8 9 |
<ol> @foreach ($orders as $order) <li>{{ $order->ordered_at }}: {{ $order->customer->name }}</li> <ul> <li>e-mail: {{ optional($order->customer->email)->address }}</li> <li>item: {{ $order->item }}</li> </ul> @endforeach </ol> |
結果は以下のとおりで、customer.id=2
に相当するcustomer2
のデータが抽出されている。
- 2020-06-05 00:00:00: customer2
- e-mail: customer2@mail.com
- item: rubber sheet
- 2020-06-06 00:00:00: customer2
- e-mail: customer2@mail.com
- item: plastic plate
whereHas()
で生成されるSQLを確認しておく。where
句の対象としてサブクエリーが構成されている(見やすいように結果を改行している)。
1 2 3 4 5 |
>>> Order::whereHas('customer', function($query){ ... $query->where('name', '=', 'customer2'); ... })->toSql(); => "select * from `orders` where exists ( select * from `customers` where `orders`.`customer_id` = `customers`.`id` and `name` = ?)" |
バインドされる値も確認。
1 2 3 4 5 6 |
>>> Order::whereHas('customer', function($query){ ... $query->where('name', '=', 'customer2'); ... })->getBindings(); => [ "customer2", ] |
2つ先のリレーション
子モデルの子モデルの属性で絞り込む場合は、whereHas()
のコールバック内でwhereHas()
を入れ子で呼び出す。呼び出し部分だけを取り出すと以下のとおり。
コールバックの引数は、コールバック内ローカルスコープなので同じ名前でも構わない。
1 2 3 4 5 |
$orders = Order::whereHas('customer', function($q) { $q->whereHas('email', function($q) { $q->where('address', 'LIKE', 'customer2%'); }); })->get(); |
結果は以下のとおりで、メールアドレスが'customer2'
で始まるcustomer2
のみ取り出されている。
- 2020-06-05 00:00:00: customer2
- e-mail: customer2@mail.com
- item: rubber sheet
- 2020-06-06 00:00:00: customer2
- e-mail: customer2@mail.com
- item: plastic plate
生成されるSQLを確認。サブクエリーが入れ子で構成されている。
1 2 3 4 5 6 7 8 |
>>> $orders = Order::whereHas('customer', function($q) { ... $q->whereHas('email', function($q) { ... $q->where('address', 'LIKE', 'customer2%'); ... }); ... })->toSql(); => "select * from `orders` where exists ( select * from `customers` where `orders`.`customer_id` = `customers`.`id` and exists ( select * from `emails` where `customers`.`id` = `emails`.`customer_id` and `address` LIKE ?))" |
バインドされる値も確認。
1 2 3 4 5 6 7 8 |
>>> $orders = Order::whereHas('customer', function($q) { ... $q->whereHas('email', function($q) { ... $q->where('address', 'LIKE', 'customer2%'); ... }); ... })->getBindings(); => [ "customer2%", ] |
1件のコメント