##はじめに
PFを作るときにリレーションのデータを取得する方法に苦しんだことと、よく聞くN+1問題について概念程度しか知らなかったので一度しっかり学んでおこうと思い調べまとめてみました。
##結論まとめ
$user->posts();はリレーションオブジェクトでクエリビルダが使える
$user->posts;は動的プロパティを呼び出しコレクションを返す
N+1問題はlazyloadが原因で起きるのでeagarloadしてやって解決する!
はい何のことかわからないので自分で調べた結果を自分なりに解釈してまとめてみました。
##動的プロパティとリレーションメソッド
UserテーブルとPostテーブルがあるとして、両テーブルは1対多の関係になっています。
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
public function posts()
{
return $this->hasMany('App\Post');
}
}
このような関係で以下のような2つの具体的な違いについてわからずなんとなくで過ごしてきました・・・。
$user->posts;
$user->posts();
結論から先に述べるとこの2つには決定的な違いがあります。
それはEloquantリレーションと動的プロパティという違いがあるということです。
###$user->postsは動的プロパティ
idが1のユーザーの全ての投稿を取得したいとします。
$user = User::find(1);
$posts = $user->posts; //select * from `posts` where `posts`.`user_id` = 1;
dd($posts)
で中身を見ていくと
Illuminate\Database\Eloquent\Collection {#1263 ▼
#items: array:3 [▼
0 => App\Post {#1266 ▶}
1 => App\Post {#1267 ▶}
2 => App\Post {#1268 ▶}
]
}
のようにPostモデルのインスタンスのコレクションが返ってきています.
$user->posts->title
とすればそのユーザーの持つ投稿のタイトルにアクセスできます。
つまり$user->posts
は動的プロパティを呼び出すということになります。
###動的プロパティって何ですかorz
なぜ動的プロパティと呼ばれるか見ていきます。
dd($user)
で$user
の中身を見ていきます。
App\User {#1260 ▼
//中略
#attributes: array:9 [▶]
//中略
#relations: array:1 [▼
"posts" => Illuminate\Database\Eloquent\Collection {#1263 ▼
#items: array:3 [▶]
}
]
//以下略
attributes
にはUserモデルの値が格納されていて、relations
にはkey名に定義したリレーションメソッド名
、valueにリレーションメソッドの制約にしたがって取得されたインスタンスのコレクション
にした連想配列が入っています。
HasManyであればコレクション、HasOneであればひとつのモデルインスタンスが入ります。
attributes
には固定の値、relations
にはリレーションによって値が変わるので動的プロパティと呼ばれるみたいです。
###$user->posts()はリレーションオブジェクトを返す
$user->posts()
の値を$posts
に代入して確認してみると
//$user->posts()を$postsに代入
$user = User::find(1);
$posts = $user->posts();
dd($posts);
dd($posts)
の内容は以下のように表示されます
Illuminate\Database\Eloquent\Relations\HasMany {#1259 ▼
#foreignKey: "posts.user_id"
#localKey: "id"
#query: Illuminate\Database\Eloquent\Builder {#1253 ▶}
#parent: App\User {#1260 ▶}
#related: App\Post {#1248 ▶}
}
これはリレーションオブジェクトといい、こういう関係でリレーションしていますよという内容が表示されているだけです。
なぜこのような内容を返すのかというと先程のUser.php
で
public function posts()
{
return $this->hasMany('App\Post');
}
と定義されていてUserモデルのpostsメソッドを呼び出したときはHasManyオブジェクトを返すようになっているのでリレーションオブジェクトが返されます。
リレーションメソッドはクエリビルダとして使用することができます。
しかし動的プロパティのようにカラム名をつけて値を取得することはできません。
リレーションメソッドのあとにget()やfirst()をつけてデータを取得できます。
get()で取得した場合はコレクションで返ります。
クエリビルダはLaravelをSQLの記法を書きやすくするものです。
公式ドキュメント クエリビルダ
https://readouble.com/laravel/5.7/ja/queries.html
$user = User::find(1);
$posts = $user->posts()->where('active', 1)->get();
このようにリレーションオブジェクトは使うことができます。
しかし、コレクションが返ってくる動的プロパティではクエリビルダをつなげることができませんので注意して下さい。
##N+1問題とは?
何かとよく聞くN+1問題。
例えば、ユーザーの情報を取得しそのユーザーが持つ投稿の情報を取得したいとして以下のようなコードを書きます
//全てのユーザ情報を取得
$users = User::all()
//それぞれのユーザーデータを取得する
foreach($users as $user) {
$user->posts;
}
するとこのようなクエリ文が発行されてしまいます。
select * from users;
select * from posts where user_id = 1;
select * from posts where user_id = 2;
select * from posts where user_id = 3;
.
.
.
select * from posts where user_id =
といったクエリ分はレコードの数だけ発行され続けるのでこれが10件程度であればよいのですが、100件はたまた1万件となってはデータを取得するために時間がかかりすぎてサーバーを圧迫してしまいます。
**全ての情報の数をN、そしてそれに加えて全ユーザー情報を取得するクエリ(+1)だけクエリを発行するのでN+1問題と呼ばれます。 **もし上記の例でposts
の数が25であるとすれば26回クエリが発行されるということになります。
//全てのユーザ情報を取得
$users = User::all() //select * from users;
//それぞれのユーザーデータを取得する
foreach($users as $user) {
$user->posts; //select * from posts where user_id = ~ レコードの数だけ繰り返される
}
なのでレコードの数+1回分のクエリが発行されてしまうのですね。
###ではなぜN+1問題が発生するのか
$user->posts
は動的プロパティです。
動的プロパティにはそのリレーション先のプロパティが参照されるまでロードされない
という**遅延ロード(またはlazyload)**と呼ばれる性質があります。
※lazy(レイジー):だらしない、怠惰。"The lazy song"ってBrunoMarsの曲にもありますね(余談)
遅延ロードは必要最低限のデータがロードされるのでリソースに優しいという点がありますがN+1問題で圧迫している以上解決する必要があります。そこで活用するのがeagerload
です。
※eager(イーガー) :熱心 餃子の王将でよく聞こえるワードではない
動的プロパティは「遅延ロード」されます。つまり実際にアクセスされた時にだけそのリレーションのデータはロードされます。そのため開発者は多くの場合にEagerローディングを使い、モデルをロードした後にアクセスするリレーションを前もってロードしておきます。Eagerロードはモデルのリレーションをロードするため実行されるSQLクエリを大幅に減らしてくれます。
##eagarloadするにはwithメソッドを使う!
プロパティとしてEloquentリレーションにアクセスする場合、そのリレーションデータは「遅延ロード」されます。つまり、そのリレーションデータへ最初にアクセスするまで、実際にはロードされません。しかし、Eloquentでは、親のモデルに対するクエリと同時にリレーションを「Eagerロード」可能です。EagerロードはN+1クエリ問題の解決策です。
つまり当初、親モデル(この場合Userモデル)にアクセスするときにはlazyloadをしてしまっているので、eagarloadすることによってN+1問題を解決できるということになると考えられます。
そしてこのeagarloadをするのがwithメソッドになります。
$users = User::with('posts')->get();
foreach($users as $user) {
$user->posts;
}
select * from users;
select * from posts where id in (1,2,3,4....);
このようにeagarloadを用いると先程はレコードの数だけ発行されていたクエリも2つに抑えることができるようになりました!
eagarloadと lazyloadでどのような違いがあるのか確認してみます。
###eagarloadとlazyloadの違い
eagarloadされたコレクションを見ていきます。
$users = User::with('posts')->get();
foreach($users as $user) {
$user->posts;
}
#items: Illuminate\Database\Eloquent\Collection {#1252 ▼
#items: array:3 [▼
0 => App\User {#1255 ▼
#dates: array:2 [▶]
#fillable: array:4 [▶]
#connection: "mysql"
#table: "articles"
#primaryKey: "id"
#keyType: "int"
+incrementing: true
#with: []
#withCount: []
#perPage: 15
+exists: true
+wasRecentlyCreated: false
#attributes: array:9 [▶]
#original: array:9 [▶]
#changes: []
#casts: []
#dateFormat: null
#appends: []
#dispatchesEvents: []
#observables: []
#relations: array:1 [▼
"posts" => App\Post {#1261 ▶}
]
#touches: []
+timestamps: true
#hidden: []
#visible: []
#guarded: array:1 [▶]
#forceDeleting: false
}
1 => App\Post {#1256 ▶}
2 => App\Post {#1257 ▶}
]
}
App\Post
の右の右にある▶
を押してもらえばその中にPostの内容が入っています!
よってeagarloadすることによってリレーション先の情報を取得しているのでまたデータを取得する必要がなくなっているということになります!
これが
プロパティとしてEloquentリレーションにアクセスする場合、そのリレーションデータは「遅延ロード」されます。つまり、そのリレーションデータへ最初にアクセスするまで、実際にはロードされません
この部分に当たるのですね・・・。
ではlazyloadではどうなっているのでしょうか?
lazyloadで返されたコレクションの中身を確認します。
$users = User::all()
dd($users)
#items: Illuminate\Database\Eloquent\Collection {#1253 ▼
#items: array:3 [▼
0 => App\User {#1254 ▼
#dates: array:2 [▶]
#fillable: array:4 [▶]
#connection: "mysql"
#table: "posts"
#primaryKey: "id"
#keyType: "int"
+incrementing: true
#with: []
#withCount: []
#perPage: 15
+exists: true
+wasRecentlyCreated: false
#attributes: array:9 [▶]
#original: array:9 [▶]
#changes: []
#casts: []
#dateFormat: null
#appends: []
#dispatchesEvents: []
#observables: []
#relations: [] //なにもない!!
#touches: []
+timestamps: true
#hidden: []
#visible: []
#guarded: array:1 [▶]
#forceDeleting: false
}
1 => App\User {#1255 ▶}
2 => App\User {#1256 ▶}
]
}
リレーション先のプロパティにアクセスされていないので何もeagarloadの時と違い、relations
には何も入ってませんでした。これがN+1を引き起こすクエリを出す理由になるのだとようやくわかりました。
##感想
冒頭にも述べたとおり、N+1問題の概念程度は知っていてwithメソッドを使うことで解決ができるということは知っていました。
しかしどういった理由で引き起こされてしまうのかしっかりと理解していなかったのでこれを機会に学んでみた結果、すごく大事なことをたくさん知ることができたと感じました。
複数ページなどを参考に自分なりに解釈しながら書いたのでもし間違っていればコメントで突っ込んで下さい・・・
$user->post;
と$user->posts();
が全く別物だったと知ることができたのも大きな収穫でした・・・。
N+1問題しっかり知っておけよ!とよく言われますが知らべてみると、しっかり解説してあるページってなかなかないものですね・・・。
これで面接で聞かれても胸はって答えることもできる??
##参考リンク
クエリビルダ
【Laravel】N+1問題を完全理解!解消法も!
LaravelのEagerLoadまとめ。動的プロパティとEloquentリレーションの違いなど
【Laravel】Eloquentを理解する
【Laravel】リレーションの違いを整理してみた
[Laravel] Eloquent リレーションと Eager Loading