概要
Laravelでデータベースの大量のデータを扱う場合、クエリービルダーでSQLを発行してDBMS側でデータを絞り込んだ方がフレームワークの負担が少なくなる。
一方でリレーションに基づいた記述は、可読性やメンテナンスの面で有利な面を持つ。
そこで、クエリービルダーとリレーションの組み合わせについて試してみた。
- モデルクラスの
all()
メソッドとwhere()
のチェーンによる絞り込みは、DBMSの機能を使っておらず非効率ではないか - モデルクラスで記述するクエリービルダーは、DBMS側でデータを絞り込んだ結果をフレームワークで扱うので効率的なようだ
- DBファサードで記述するクエリービルダーは、得られるデータがモデルのインスタンスではなく連想配列になるため、モデルで定義したリレーションが使えない
準備
クエリービルダーで使ったものと同じデータを準備する。
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) |
all()とwhere()の組み合わせ
以下の様なコントローラーのアクションでデータを絞り込んでビューに渡す。
1 2 3 4 5 6 7 8 |
public function query_relation() { $orders = Order::all()->where('customer_id', '=', 2); return view('queries.query_relation', [ 'title' => 'Query Test', 'orders' => $orders, ]); } |
ビュー側では$orders
を受け取って、リレーションに基づいて表示する。
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> |
ブラウザーでの表示は以下のとおり。
- 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
ただしこの場合の動作は、DBMSから全データをall()
で取得した後にcustomer_id
で絞り込んでいる。以下の様にtinkerで確認してみると、where
句を含むSQLは発行されていないようだ。
1 2 |
>>> Order::all()->where('customer_id', '=', 2)->toSql() BadMethodCallException with message 'Method Illuminate\Database\Eloquent\Collection::toSql does not exist.' |
実際、get()
なしで直接モデルのインスタンスが得られている。tinkerで確認してみると、$orders
はOrder
クラスのインスタンスを要素とするコレクションとなっている。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
>>> Order::all()->where('customer_id', '=', 2) => Illuminate\Database\Eloquent\Collection {#4341 all: [ 1 => App\Order {#4332 id: 2, customer_id: 2, ordered_at: "2020-06-05 00:00:00", item: "rubber sheet", }, 2 => App\Order {#4344 id: 3, customer_id: 2, ordered_at: "2020-06-06 00:00:00", item: "plastic plate", }, ], } |
モデルのメソッドによるクエリービルダー
コントローラーのアクションで、以下の様にOrder
モデルからクエリービルダーを構成する。
1 2 3 4 5 6 7 8 |
public function query_relation() { $orders = Order::where('customer_id', '=', 2)->get(); return view('queries.query_relation', [ 'title' => 'Query Test', 'orders' => $orders, ]); } |
ビューは上と同じ内容で、結果も同じように表示される。
tinkerで確認するとSQLが構成されて値もバインドされているので、DBMS側でデータが絞り込まれているようだ。
1 2 3 4 5 6 |
>>> Order::where('customer_id', '=', 2)->toSql() => "select * from `orders` where `customer_id` = ?" >>> Order::where('customer_id', '=', 2)->getBindings() => [ 2, ] |
get()
の結果得られるデータは上記のall()
~where()
と同じで、Orderクラスのインスタンスを要素とするコレクション。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
>>> Order::where('customer_id', '=', 2)->get() => Illuminate\Database\Eloquent\Collection {#4343 all: [ App\Order {#4327 id: 2, customer_id: 2, ordered_at: "2020-06-05 00:00:00", item: "rubber sheet", }, App\Order {#4312 id: 3, customer_id: 2, ordered_at: "2020-06-06 00:00:00", item: "plastic plate", }, ], } |
DBファサードによるクエリービルダー
クエリビルダ―をモデルクラスからではなくDBファサードから記述した場合。
1 2 3 4 5 6 7 8 |
public function query_relation() { $orders = DB::table('orders')->where('customer_id', '=', 2)->get(); return view('queries.query_relation', [ 'title' => 'Query Test', 'orders' => $orders, ]); } |
同じビューを使った場合、以下のようなエラーがブラウザーに表示される。
1 2 3 |
ErrorException Undefined property: stdClass::$customer (View: /home/vagrant/git/codecamp/laravel/laravel_tutorial/resources/views/queries/query_relation.blade.php) |
tinkerで確認すると、SQLは同じように発行されバインディングも同じ。
1 2 3 4 5 6 |
>>> DB::table('orders')->where('customer_id', '=', 2)->toSql() => "select * from `orders` where `customer_id` = ?" >>> DB::table('orders')->where('customer_id', '=', 2)->getBindings() => [ 2, ] |
ただし、get()
で得られるデータが上と異なる。全体は配列だが、要素がOrder
クラスのインスタンスではなく、Order
の属性をキーとする連想配列となっている。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
>>> DB::table('orders')->where('customer_id', '=', 2)->get() => Illuminate\Support\Collection {#4337 all: [ {#4351 +"id": 2, +"customer_id": 2, +"ordered_at": "2020-06-05 00:00:00", +"item": "rubber sheet", }, {#4324 +"id": 3, +"customer_id": 2, +"ordered_at": "2020-06-06 00:00:00", +"item": "plastic plate", }, ], } |
このため、$orders
の要素からリレーションを定義したはずのcustomer
を呼び出そうとしても、「定義されていない属性」としてエラーが発生する。
そこでビュー側でリレーションを用いず各要素を連想配列として取り出してみる。
1 2 3 4 5 6 7 |
@foreach ($orders as $order) <ol> @foreach ($order as $key => $value) <li>{{ $key }}: {{ $value }}</li> @endforeach </ol> @endforeach |
こうすると以下の様に$orders
の抽出結果が表示される。
- id: 2
- customer_id: 2
- ordered_at: 2020-06-05 00:00:00
- item: rubber sheet
- id: 3
- customer_id: 2
- ordered_at: 2020-06-06 00:00:00
- item: plastic plate
ただし各要素のorderに関係づけられたcustomer
はこのままでは得られないため、クエリーレベルで2次元配列として関連する全モデルの属性をカラムとして取り出す必要がありそうだ。