1
0

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-10-09

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関連先を取得せず、中間テーブルを経由して取得するようにする
例:

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として定義しています。
しかし、これは実際のコードとしては以下と同様の内容です。

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
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0