😽
Laravelアクセサアンチパターン ~ アクセサでN+1問題を引き起こす ~
僕はLaravelの便利機能の一つのアクセサを頻繁に使用するのですが、
あるとき、アクセサを使用することによってN+1問題を引き起こしてしまっていることに気づきました。
仮定
ブログ記事の投稿サービスを作ろうとしていると仮定します。
- ユーザー情報を格納する「usersテーブル
- ブログ記事を格納する「articlesテーブル」
- ブログ記事のコメント情報を格納する「comments」テーブル
の3つのテーブルがあるとします。
何が起こったのか
ブログのモデル内でアクセサを使用して、コメントの件数を取得するロジックを書いていました。
<?php
class Article extends Model
{
protected $appends = [comment_count];
public function comments()
{
return $this->hasMany(Comment::class, "article_id", id");
}
public function getCommentCountAttribute()
{
return count($this->comments);
}
}
この状態で記事を20記事取得するロジックを記入して、記事を20記事取得してきました。
そうすると、なんと!!
1記事取得するごとにcommentsテーブルを読みに行って、コメントの個数を数えているではありませんか!
つまり、 N+1問題が発生してしまっているんですね。。クエリ発行しすぎだよ!っつって
アクセサでのN+1問題をeager load(with)で解決する
この場合、アクセサを使うとN+1問題が発生することがわかりました。
では、どうすればいいのか?
withを使います。
20記事を一気に取得する際に、withを使用して、関連するコメントを取得します。
$articles = Article::with("comments")->paginate(20);
このようにすると、フロント側でコメントの個数を計算して出してあげれば簡単ですね。
<div>{{ count($article->comments) }}</div>
終わりに
アクセサ信者で、何でもかんでもアクセサを使用していたので、今回の件を反省して気をつけようと思いました。
常にLaravelのクエリが増えてないかとか、N+1問題が発生していないかはチェックしておかないといけないですね。
Discussion
コメント失礼します。
withCountという専用メソッドもありますよ。
関連モデルのカウント
記事中の方法で、確かにN+1問題は解決していますが、コメントは全件取得したままになっています。(取得したコメントを表示する場合は別として)
もし仮にコメントの数が非常に多っかたりすると、その分のコメントをDBから取得して、Eloquentモデルにインスタンス化したりするので、その分のメモリと時間を必要とします。
上記メソッド使うとDBに処理を任せつつ、N+1問題も解決できます。