Laravelでの関連データ取得：withとwhereHasの使い分け

# 背景
LaravelのORMであるEloquentを使用して、対象を絞り込もうとした際に意図した条件のレコードが取得できない事がありました。

具体的には、
①「親テーブルの全レコード」
②「それに紐づいた子テーブルのレコード」
の上記２つを絞り込む意図がありました。しかし、whereHas句を使用したところ意図した結果が返ってこなかったため、with句を使ったEager Loadingでの解決方法を「ゲームテーブル」と「ゲームレビューテーブル」を例に解説します。上記①と②のような条件の絞り込みをするときに参考にしていただければと思います。


# テーブル例
以下のようなテーブルがあるとします。
```mermaid
erDiagram
    GAMES {
        bigint id PK
        string title
        string genre
        date release_date
        timestamp created_at
        timestamp updated_at
    }

    GAME_REVIEWS {
        bigint id PK
        bigint game_id FK
        integer rating
        text review
        timestamp created_at
    }

    GAMES ||--o{ GAME_REVIEWS : "has many"
```
## 親テーブル
### 1.  ゲームテーブル`games`
| id | title            | genre  | release_date | created_at         | updated_at         |
|----|------------------|--------|--------------|--------------------|--------------------|
| 1  | スーパーアドベンチャー  | Action | 2023-01-15   | 2023-01-01 10:00  | 2023-01-01 10:00  |
| 2  | パズルワールド    | Puzzle | 2023-05-20   | 2023-05-01 10:00  | 2023-05-01 10:00  |


## 子テーブル
### 2. ゲームレビューテーブル`game_reviews` 
| id | game_id | rating | review             | created_at         |
|----|---------|--------|--------------------|--------------------|
| 1  | 1       | 3      | 悪くない          | 2023-02-11 10:00  |
| 2  | 2       | 4      | 満足です          | 2023-06-02 12:00  |





# 実現したいこと

- ゲームテーブルから取って来たいレコード
  - `id = 1`のアドベンチャーワールド
  - `id = 2`のパズルワールド
- ゲームレビューテーブルから取って来たいレコード
  - `'rating' = 4`のレコード 

# 解決方法

## 実現できなかったwherehasを使ったクエリ(✖)
```
Games::whereHas('game_reviews', function ($query) {
    $query->where('rating', 4);
})->get();
```
whereHasクエリでは子レコードの'rating'が4である親レコードのみを取得します。
| id | title         | genre  | release_date | created_at         | updated_at         |
|----|---------------|--------|--------------|--------------------|--------------------|
| 2  | パズルワールド | Puzzle | 2023-05-20   | 2023-05-01 10:00  | 2023-05-01 10:00  |

- ゲームテーブルから取得できるレコード
  - <font color="Red">~~`id = 1`のアドベンチャーワールド~~ ✖︎
  - `id = 2`のパズルワールド(子レコードのratingが4のため) ◯
- ゲームレビューテーブルから取得できるレコード
  - <font color="Red">~~`'rating' = 4`のレコード~~ ✖︎

## 実現できたwithを使ったクエリ(⚪︎)
```
$query = Games::with([
    'game_reviews' => function ($query) {
         $query->where('rating', 4);
    }
])->get();
```
withを使ったEager Loadingでは、親テーブルを全件＋`rating=4`の子テーブルのレコードを取得します。
| id | title            | genre  | release_date | created_at         | updated_at         |
|----|------------------|--------|--------------|--------------------|--------------------|
| 1  | スーパーアドベンチャー  | Action | 2023-01-15   | 2023-01-01 10:00  | 2023-01-01 10:00  |
| 2  | パズルワールド    | Puzzle | 2023-05-20   | 2023-05-01 10:00  | 2023-05-01 10:00  |

| game_id | rating | review     | created_at         |
|---------|--------|------------|--------------------|
| 2       | 4      | 満足です   | 2023-06-02 12:00  |
- ゲームテーブルから取得できるレコード
  - <font color="Red">`id = 1`のアドベンチャーワールド ◯</font>
  - `id = 2`のパズルワールド  ◯
- ゲームレビューテーブルから取得できるレコード
  - <font color="Red">`'rating' = 4`のレコード  ◯
:::note
リレーション先のメソッドを直接呼び出さずにwithを使う理由

リレーション先を呼び出す方法として「game->gameReview」のように定義したメソッドから関連レコードを呼び出す方法(Lazy Loading)があります。しかし例えばfor文で複数レコードを取って来たいときに、これではループの回数分クエリを発行してしまうためデータベース的に効率が悪いです。なのでwithを使って一括でデータベースからデータを取ってきて処理をします。これによりデータベース間のやり取りは1回ですみます。
:::

# まとめ
以上のように、目的のレコードを取得することができました。
個人的にwithは「共に」という意味があるので、レコードが追加でローディングされて、whereHasは「持つ」という意味があるため条件を持ったレコードのみ返される、という理解をしました。↓はwhereHasとwithの違いのまとめです。
## **whereHasとwithの違い**

### **whereHas**

**目的**  
リレーション先の条件に基づいて、元テーブルのデータを絞り込む。

**特徴**
- 元テーブルのデータをリレーション先の条件でフィルタリングする
- 元テーブルのレコードは、リレーション先の条件を満たす場合のみ取得される
- 条件に合致したリレーション先テーブルのデータは、取得結果には含まれない（フィルタリングにのみ使用）

---

### **with**

**目的**  
元テーブルのデータを取得しつつ、リレーション先のデータもロードする

**特徴**
- 元テーブルのデータは、条件に関係なくすべて取得される
- リレーション先のデータには、指定した条件を適用してロード（Eager Loading）する
- 元テーブルを取得しながら、条件付きのリレーション先テーブルのデータを必要に応じて追加ロードできる


# 参考
EagerLoadingについて

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