Graphql(graphql-ruby)でネストした関連テーブルをincludesするとキャッシュがきかずN+1になってしまう件について

# Graphql(graphql-ruby)でネストした関連テーブルをincludesするとキャッシュが効かずN+1になってしまう件について

## 結論
railsのincludesを使ってネストした関連をキャッシュしていても、has_many through関連を使用して関連テーブルを取得しようとするとクエリを発行してしまう。
例： `User.includes(user_tags: :tag).where(user_tags: {tag_id: tag_id})`

userから直接has_many through関連先を取得せず、中間テーブルを経由して取得するようにする
例： 
```ruby
user.tags ❌
user.user_tags.map(&:tag) ⭕️
```

## テーブル構成

このようなテーブルがあったとします。
・usersテーブル
|  id  |  name  |
| ---- | ---- |
|  1  |  太郎  |
|  2  |  花子 |

・user_tagsテーブル(中間テーブル)
|  id  |  user_id  |  tag_id |
| ---- | ---- | ---- |
|  1  |  1  |   1 |
|  2  |  1  | 2 |
|  2  |  2  | 3 |


・tagsテーブル
|  id  |  name  |
| ---- | ---- |
|  1  |  高身長  |
|  2  |  美形 |
|  2  |  黒髪 |

この場合各モデルの関連はざっくり以下のようになるかと思います。

```user.rb
class User < ActiveRecord::Base
  has_many :user_tags, dependent: :destroy
  has_many :tags, through: :user_tags

  # ユーザー一覧をタグで絞り込むためにUserモデルにsearch_by_tagスコープを定義します。
  scope :search_by_tag, -> (tag_id) {
    includes(user_tags: :tag).where(user_tags: {tag_id: tag_id})
  }
end
```

```user_tag.rb
class UserTag < ActiveRecord::Base
  belongs_to :user
  belongs_to :tag
end
```

```tag.rb
class User < ActiveRecord::Base
  has_many :user_tags, dependent: :destroy
end
```

## graphql側の実装

```query_type.rb
module Types
  class QueryType < Types::BaseObject
    field :users, [Types::UserType], null: true, description: "ユーザー一覧" do
      argument :tag_id, Int, required: false, description: "タグID"
    end

    def users(tag_id:)
      User.search_by_tag(tag_id)
    end
  end
end
```

```user_type.rb
module Types
  class UserType < Types::BaseObject
    field :id, ID, null: false
    field :name, String, null: true
    field :tags, [Types::TagType], null: true
  end
end
```

```tag_type.rb
module Types
  class TagType < Types::BaseObject
    field :id, ID, null: false
    field :name, String, null: true
  end
end
```


## フロントからの呼び出し

graphiqlなどで以下のqueryを実行します。
```
query {
  users(tagId: 1) {
    id
    name
    tags {
      id
      name
    }
  }
}
```

## 問題と原因
この時問題なくresponseが返ってきますが、 なぜかN+1が発生してしまい、userの数だけクエリが多く発行されてしまいます。
user.rbのscope内でincludesをしているはずなのになぜ？？と疑問に感じるかと思います。

原因は関連テーブルをrailsのアソシエーションである`has_many :tags, through: :user_tags`を使用している点にあります。

実は以下のquery_type.rbで呼び出された時点では問題なくキャッシュされており、正しい関連テーブルの呼び出し方を行えばキャッシュが使用されるためクエリが発行されることはありません。

```query_type.rb
def users(tag_id:)
  User.search_by_tag(tag_id)
end
```

#### 問題がある箇所は以下のuser_type.rbです。

```user_type.rb
module Types
  class UserType < Types::BaseObject
    field :id, ID, null: false
    field :name, String, null: true
    field :tags, [Types::TagType], null: true
  end
end
```

ここではuserに関連するtagsを `field :tags, [Types::TagType], null: true`として定義しています。
しかし、これは実際のコードとしては以下と同様の内容です。
```ruby
users = User.search_by_tag(tag_id)
users.map(&:tags) # <- userから直接tagsを取得しようとしている
```
これを実行するとキャッシュしたtagsではなく新規のクエリを発行してtagsを取得しようとしてします。
そのためこれを解決するためにはhas_many through関連を使用せずに関連テーブルであるuser_tagsをtypeに追加しuser_tag_type.rbを経由してtagsを取得する必要がある。

#### 追加・修正する実装

追加
```user_tag_type.rb
module Types
  class UserTagType < Types::BaseObject
    field :id, ID, null: false
    field :tags, [Types::TagType], null: true
  end
end
```

修正
```user_type.rb
module Types
  class UserType < Types::BaseObject
    field :id, ID, null: false
    field :name, String, null: true
    field :user_tags, [Types::UserTagType], null: true
  end
end
```

#### フロントからの呼び出し方
```.gql
query {
  user(tagId: 1) {
    id
    name
    user_tags {
      tags {
        id
        name
      }
    }
  }
}
```

どうしてもuser_tagsの階層を入れたくない場合は以下の実装でいけるかも(未確認)

```user_type.rb
module Types
  class UserType < Types::BaseObject
    field :id, ID, null: false
    field :name, String, null: true
    field :tags_by_through, [Types::TagType], null: true
  end
end
```

```user.rb
class User < ActiveRecord::Base
  has_many :user_tags, dependent: :destroy
  has_many :tags, through: :user_tags

  # ユーザー一覧をタグで絞り込むためにUserモデルにsearch_by_tagスコープを定義します。
  scope :search_by_tag, -> (tag_id) {
    includes(user_tags: :tag).where(user_tags: {tag_id: tag_id})
  }

  def tags_by_through   # <- 追加
    user_tags.map(&:tag)
  end
end
```