環境
- PHP 7.2
- Laravel 5.6
前提
- ブログサービスを想定、Userが複数のPost(投稿)を持つ(1対多関係)
- Userテーブルはid, name, email, password, created_at, updated_atカラムを持つ
- Postテーブルはid, content, created_at, updated_atカラムを持つ
リレーションメソッドと動的プロパティ
$user = User::find($id); // ユーザを取得
$posts = $user->posts()->get(); // ユーザが持つ投稿一覧を取得
2行目は user->posts
と書いても同じ結果が得られる。
https://readouble.com/laravel/5.6/ja/eloquent-relationships.html
リレーションが定義できたらEloquentの動的プロパティを使って、関係したレコードを取得できます。動的プロパティによりモデル上のプロパティのようにリレーションメソッドにアクセスできます。
ドキュメントにならって $user->posts()->get()
をリレーションメソッドを扱ったアクセス、 $user->posts
を動的プロパティを扱ったアクセスと呼ぶことにする。
筆者はてっきり動的プロパティの記法はリレーションメソッドの省略形と思っていた。
しかしこの後の検証で動作上の違いを認識することができた。
LaravelのREPL環境である php artisan tinker
を実行する。
>>> $user = User::find(5)
=> App\User {#2952
id: 5,
name: "test",
created_at: "2019-02-26 20:12:50",
updated_at: "2019-02-26 20:12:50",
}
>>> $user
=> App\User {#2952
id: 5,
name: "test",
created_at: "2019-02-26 20:12:50",
updated_at: "2019-02-26 20:12:50",
}
>>> $user->posts()->get()
=> Illuminate\Database\Eloquent\Collection {#2958
all: [
App\Post {#2950
id: 67,
content: "test",
user_id: 5,
created_at: "2019-02-26 20:13:14",
updated_at: "2019-02-26 20:13:14",
},
],
}
>>> $user
=> App\User {#2952
id: 5,
name: "test",
created_at: "2019-02-26 20:12:50",
updated_at: "2019-02-26 20:12:50",
}
>>> $user->posts
=> Illuminate\Database\Eloquent\Collection {#2955
all: [
App\Post {#2942
id: 67,
content: "test",
user_id: 5,
created_at: "2019-02-26 20:13:14",
updated_at: "2019-02-26 20:13:14",
},
],
}
>>> $user
=> App\User {#2952
id: 5,
name: "test",
created_at: "2019-02-26 20:12:50",
updated_at: "2019-02-26 20:12:50",
posts: Illuminate\Database\Eloquent\Collection {#2955
all: [
App\Post {#2942
id: 67,
content: "test",
user_id: 5,
created_at: "2019-02-26 20:13:14",
updated_at: "2019-02-26 20:13:14",
},
],
},
}
リレーションメソッドはレコードの結果を返す。
動的プロパティもレコードの結果を返す。
違いは、動的プロパティを使った場合はレシーバである $user
インスタンスに posts
プロパティが追加される点。
つまり、 $user->posts
を実行すると $user
に posts
情報がキャッシュされる。
これ以降、 $user->posts
を実行した場合は、クエリを発行せずにキャッシュされたデータを返すようだ。
遅延ロード
この挙動は遅延ローディングとその目的であるN+1問題の解消のために存在しているようだ。
動的プロパティは「遅延ロード」されます。つまり実際にアクセスされた時にだけそのリレーションのデータはロードされます。そのため開発者は多くの場合にEagerローディングを使い、モデルをロードした後にアクセスするリレーションを前もってロードしておきます。Eagerロードはモデルのリレーションをロードするため実行されるSQLクエリを大幅に減らしてくれます。
ドキュメントではAuthorとBookの例で説明されている。
$books = App\Book::with('author')->get();
foreach ($books as $book) {
echo $book->author->name;
}
with
でN+1を防止できることは知っていたが、そちらばかりに注目がいっており動的プロパティは雑な理解に留まってしまっていた。
結論
まとめると下記のようになる。
- リレーションメソッドによるアクセスは常にクエリ発行する
- 動的プロパティによるアクセスは、キャッシュデータが存在しなければクエリ発行し、レシーバインスタンスにキャッシュとなるプロパティを生成する。キャッシュが存在すればそれを返すためクエリ発行しないという挙動をする
App\Book::with('author')->get();
で返ってくるBooksコレクションにはその一つ一つにAuthorのデータがキャッシュされている状態になっており、キャッシュデータを利用するには動的プロパティでアクセスしなければならない。
すなわち、
$books = App\Book::with('author')->get();
foreach ($books as $book) {
echo $book->author()->first()->name; // リレーションメソッドによるアクセス(NGパターン)
}
このようにリレーションメソッドでアクセスすると都度クエリが発行されてしまうため駄目ですよ、遅延ロードの際は動的プロパティを使いましょうという話。
何か誤解があれば指摘頂きたいと思います。