Laravelにおけるデータ取得のリファクタリング〜実体験を添えて〜

:::note info
これは [フェンリル デザインとテクノロジー Advent Calendar 2023](https://adventar.org/calendars/9360) 21日目の記事です
:::

## はじめに
今年新卒で入社して、主にバックエンドを担当しているせーと申します。
入社してからあっという間に8ヶ月が経過し、アドベントカレンダーの季節がやってきました。ﾊﾔｲ。
当記事では、私が入社後初のプロジェクトで意識したデータ取得方法について共有します。

## TL; DR
- N+1問題を引き起こさないように
- Eager Loadingを利用する
- selectメソッドで取得するカラムを制限し、メモリを節約する

## 何が起こったか
初めてプロジェクトにアサインされ、Laravel10・MySQL8という環境でコーディングをしていました。
ローカルで開発していたときはデータ数が少なかったので、何事もなくコードを書いていました。
しかし、開発環境にデプロイして数万のデータが入った途端、APIの取得に10秒ほどかかってしまう状況になってしまいました。

これはダメだ、と先輩と確認したところ、以下のような原因が考えられました。
- クエリの呼び出しが多すぎる
- テーブルのカラム数が多くなりすぎてメモリの使用量が多すぎる

## 改善手順
### N+1問題
問題の原因を調査するためにログを確認したところ、クエリが大量に発行されているのを発見し、言葉を失いました。
N+1問題について知っていましたが、これまで規模の大きなプロジェクトに携わったことがなく、知らず知らずのうちに問題を引き起こすコードを書いてしまっていました。

N+1問題とは、名前の通りクエリの呼び出し回数がデータの個数(N)+1になってしまう問題のことです。

もう少し詳しく説明すると、usersというテーブルにユーザー情報が5つ入っています。usersはarticlesというテーブルと紐づいており、一人のユーザーがいくつかの記事を持っています。


![アドベントカレンダー画像.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1087368/751c290b-3b7d-d8d5-2306-6fd3be5c29b8.png)

ここで全ユーザーの記事を取得したい場合、どうすれば良いでしょうか？
1. 全てのユーザーを取得して、その後ユーザーの記事を取得する
1. 全てのユーザーとそれに紐づく記事を取得する

ここでお察しの方もいるかもしれませんが、この例の1がN+1問題に該当します。
全ユーザーを取得するクエリで1回、その後ユーザーごとの記事を取得する処理で5回、計6回クエリが呼び出されてしまっています。

この例では6回だけですが、実際のデータでは数万回呼び出してしまうことになるので、考えるだけでゾッとしますね......。

これはいけないということで、最初に全てのデータを取得するため、joinでテーブルを結合しようと思いました。

しかしちょっと待てよ。Laravelにはwithというメソッドがあるらしい。そしてこのwithというメソッドはEager Loadingという仕組みを利用するらしい。
ということで、次のセクションではEager Loadingについてお話しします。

### Eager Loading
Laravelは通常、プロパティに最初にアクセスするまで、リレーションデータ（今回の例ではarticles）は実際にロードされないようになっています。
毎回関連するデータを取得していたら問題ですが、今回のように数千、数万回クエリを呼び出すよりかは一括でデータを取得した方が良いというケースもあります。
ここで利用できるのがEager Loadingという仕組みです。
親モデルへのクエリ時にEager Loadingを利用することで、最初にリレーションデータを取得でき、クエリの呼び出し回数を減らすことができます。
今回の例の場合、全ユーザーを取得する処理で1回、ユーザーに紐づく記事を取得する処理で1回の計2回に減らすことができます。
数千、数万回の呼び出しが2回になるなら、使わない手はありませんよね。

ちなみに、eagerの意味を調べてみると「すぐに」という意味があり、すぐにデータを取得するという意でこの単語が使われているようです。

https://jp.quora.com/%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E3%82%92%E3%81%97%E3%81%A6%E3%81%84%E3%82%8B%E3%81%A8%E5%87%BA%E3%81%A6%E3%81%8F%E3%82%8B%E8%8B%B1%E5%8D%98%E8%AA%9E-eager-%E3%81%AE%E6%84%8F%E5%91%B3

Laravelではwithを利用することでEager Loadingを実現できます。
modelにリレーションを定義して、データを取得する際にwithを利用する。そうすると、クエリを呼び出すときにリレーションに定義したデータもまとめて呼び出すことができます。

```php
class User extends Model
{
    public function articles(): hasMany {
        return $this->hasMany(Article::class);
    }
}

$user = User::with('articles')->find(1);

```

そのほかにも、ネストした関係をロードしたり、取得時に制約をかけたりすることもできます。

https://readouble.com/laravel/10.x/ja/eloquent-relationships.html#eager-loading

### 取得カラムの制限
続いては取得カラムの制限についてです。今回の件ではテーブルを数十個結合していたので、絞り込まなければ膨大な量のカラムが存在し、メモリもかなりの量を使用してしまいます。
幸いLaravelでは、selectメソッドを利用してカラムを絞り込むことができるので、必要なカラムだけを絞り込むことでメモリの節約ができます。
また、クエリビルダでは同名のカラムがあった場合上書きされてしまうので、取得するカラムは最小限にするのが良いと思います。

https://qiita.com/pipiox/items/395bef543e16c93a1fa6

## 終わりに
個人開発や長期インターンではデータ数が少ないアプリケーションを開発していたので、意識することがあまりなかったのですが、実際に体験するとどれだけ重要な要素かを実感しました。
大失態をしてしまいましたが、経験を積むことができよかったかなと思っています。プロジェクトメンバーの皆さまにはご迷惑をおかけしました🙇
今後はこれまで以上にパフォーマンスを意識して実装したいと思います。

## 参考

https://readouble.com/laravel/10.x/ja/eloquent-relationships.html#eager-loading

https://jp.quora.com/%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E3%82%92%E3%81%97%E3%81%A6%E3%81%84%E3%82%8B%E3%81%A8%E5%87%BA%E3%81%A6%E3%81%8F%E3%82%8B%E8%8B%B1%E5%8D%98%E8%AA%9E-eager-%E3%81%AE%E6%84%8F%E5%91%B3

https://qiita.com/pipiox/items/395bef543e16c93a1fa6