概要
Laravelのドキュメントに例としてあげられているUserモデルとPhoneモデル。
https://readouble.com/laravel/8.x/ja/eloquent-relationships.html#one-to-one
ユーザー情報取得時には、それに紐づくPhoneモデルの情報を、
電話情報取得時には、それに紐づくUserモデルの情報を付け加えた形で返す
シンプルなAPIをLaravelのAPI resourceを使って作りたかった。
期待するレスポンス
GET /api/users/1
{
"data": {
"id": 1,
"name": "test",
"phone": {
"id": 1,
"number": "xxx-xxxx-xxxx"
}
}
}
GET /api/phones/1
{
"data": {
"id": 1,
"number": "xxx-xxxx-xxxx",
"user": {
"id": 1,
"name": "test"
}
}
}
ソース
Userモデル
class User extends Model
{
use HasFactory;
public function phone()
{
return $this->hasOne(Phone::class);
}
}
Phoneモデル
class Phone extends Model
{
use HasFactory;
public function user()
{
return $this->belongsTo(User::class);
}
}
Userリソース
class UserResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'phone' => new PhoneResource($this->phone),
];
}
}
Phoneリソース
class PhoneResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'number' => $this->number,
'user' => new UserResource($this->user),
];
}
}
Userコントローラー
class UserController extends Controller
{
public function show(User $user)
{
return new UserResource($user);
}
}
Phoneコントローラー
class PhoneController extends Controller
{
public function show(Phone $phone)
{
return new PhoneResource($phone);
}
}
APIルート
Route::apiResource('users', UserController::class);
Route::apiResource('phone', PhoneController::class);
結果と問題点
上記のソースではPostmanからAPIを呼び出すとハングアウトする。
GET http://localhost/api/users/1
Error: socket hang up
理由はAPI resouceの定義でUserモデルとPhoneモデルのリレーションの
読み込みが無限ループしていること。
例としてUserモデルを挙げるが、今回はルーティング定義時にモデルクラスを
タイプヒントすることでモデルを自動取得する方法を採用していた。
Route::apiResource('users', UserController::class);
これによりコントローラーのアクションメソッドでは、指定されたIDに紐づくモデルが
既に取得された状態となるが
Userコントローラー
public function show(User $user)
{
return new UserResource($user);
}
実はこの時点ではUserモデルに紐づくPhoneモデルは取得されていない。
なぜならLaravelのリレーションは遅延ロードであるため。
このことは公式ドキュメントにも記載されている。
https://readouble.com/laravel/8.x/ja/eloquent-relationships.html#eager-loading
プロパティとしてEloquentリレーションへアクセスすると、関連するモデルは「遅延読み込み」されます。
つまりこれは、最初にプロパティへアクセスするまで、リレーションデータが実際にロードされないことを意味します。
なので実際にUserモデルに紐づくPhoneモデルが取得されるのは、UserリソースでのPhoneResource生成時。
これにより、以下のコメントに振っている番号のような順で呼び出しあった結果、無限ループしている。
ユーザー取得のAPIを呼び出した場合(一部抜粋)
// UserResource
return [
'id' => $this->id,
'name' => $this->name,
// 1. Phoneモデルの取得
// 3. Phoneモデルの取得
// 5. Phoneモデルの取得.....
'phone' => new PhoneResource($this->phone),
];
// PhoneResource
return [
'id' => $this->id,
'number' => $this->number,
// 2. Userモデルの取得
// 4. Userモデルの取得
// 6. Userモデルの取得.....
'user' => new UserResource($this->user),
];
解消方法
この無限ループを解消するためには、eager load
を使う。
eager loadに関して
eager loadの内容に関しては公式ドキュメントを参照。
つまり都度読み込みではなく先にリレーションまで読み込んでしまう方法。
https://readouble.com/laravel/8.x/ja/eloquent-relationships.html#eager-loading
モデル取得方法の変更
これを行うにはモデルを取得する部分に手を入れないと行けないので、
仕方なくルーティング時にタイプヒントでモデルを自動取得する方法から、
手動で取得する方法に切り替える。
※他にやり方があるのかもしれないがみつからない。。。
APIルート
// Route::apiResource('users', UserController::class);
Route::get('/users/{id}', [UserController::class, 'show']);
// Route::apiResource('phone', PhoneController::class);
Route::get('/phones/{id}', [PhoneController::class, 'show]);
それに伴いコントローラーも変更
例としてUserコントローラーのみを挙げるがPhoneコントローラーの方も変更する。
// public function show(User $user)
public function show (int $id)
{
// これがeager load. withで指定されたリレーションをこの時点で読み込む.
$user = User::with('phone')->get()->find($id);
return new UserResource($user);
}
whenLoadedを使う
さらにAPI resourceにも手を加える。
$this->phone
ではなくwithLoaded('phone')
を使う。
このメソッドは既にリレーションがロードされている場合に限りリレーションを返し、
ロードされていない場合はそのプロパティごとレスポンスから削る。
つまり、リレーションを返すか否かをeager load
されているかに委ねる。
例としてUserリソースのみを挙げるがPhoneリソースの方も変更する。
Userリソース
class UserResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
// 'phone' => new PhoneResource($this->phone),
'phone' => new PhoneResource($this->whenLoaded('user')),
];
}
}
これによりリレーションはコントローラでeager loadにより取得する経路だけとなり、
API resourceからリレーションを取得することがなくなるため、無限ループが解消する。
参考
https://laracasts.com/discuss/channels/laravel/api-resource-infinite-loop
https://readouble.com/laravel/8.x/ja/eloquent-relationships.html#eager-loading