[N+1 제거] Rails의 GraphiQL에서 graphil-batch 가져오기

48324 단어 GraphQLRailsRubytech
이 글은 그래픽QL 자체 설명과 그래픽QL 특유의 용어 설명 등을 하지 않는다.
래일스를 이용해 그라피QL의 API를 빠르게 제작하자는 취지로 제작됐다.
2021년 5월 27일 시점의 최신 Version입니다.
ruby: 3.0.1
rails: 6.1.3.2
graphql-ruby: 1.12.10
graphql-batch: 0.4.3
GraphiQL을 0에서 시작하여 Post를 얻을 때까지 설치하는 방법에 대해 이 글에서 설명하였다.
https://zenn.dev/necocoa/articles/setup-graphql-ruby
현재 모델은 다음과 같습니다.
# app/models/post.rb
class Post < ApplicationRecord
  has_many :comments
end

# app/models/comment.rb
class Comment < ApplicationRecord
  belongs_to :post
end

N+1 생성Query


PostsQuery 작성


우선 query type에 posts field를 추가합니다.
app/graphql/types/query_type.rb
module Types
  class QueryType < Types::BaseObject
    # Add `node(id: ID!) and `nodes(ids: [ID!]!)`
    include GraphQL::Types::Relay::HasNodeField
    include GraphQL::Types::Relay::HasNodesField

    field :post, resolver: Resolvers::PostResolver
+   field :posts, resolver: Resolvers::PostsResolver
  end
end

Posts Resolver 만들기


PostsResolver를 작성합니다.
모든 Posts의 반환 값은 간단명료합니다.
이번에는 페이지의 단자를 언급하지 않습니다.
app/graphql/resolvers/posts_resolver.rb
module Resolvers
  class PostsResolver < Resolvers::BaseResolver
    description 'Find all posts'
    type [Types::PostType], null: false

    def resolve
      Post.all.recent
    end
  end
end
app/models/post.rb
class PostReview < ApplicationRecord
  belongs_to :post

+ scope :recent, -> { order(created_at: :desc) }
end

N+1 확인Query


포스터와has-many의comments를 얻으려고 합니다.
query {
  posts {
    id
    title
    comments {
      id
      body
    }
  }
}

Comment SQL이 N+1으로 변경되었습니다.
graphiql-batch를 사용하여 이 문제를 제거합니다.
Started POST "/graphql"
  Post Load (1.8ms)  SELECT "posts".* FROM "posts" ORDER BY "posts"."created_at" DESC
  Comment Load (3.1ms)  SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = $1  [["post_id", 1]]
  Comment Load (4.9ms)  SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = $1  [["post_id", 2]]
  Comment Load (3.1ms)  SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = $1  [["post_id", 3]]

GraphiQL Batch 가져오기


graphiql-batch gem 설치


GraphiQL은 그래프 구조로 지정된 필드를 얻기 때문에 includes 등을 사용하여 preload를 할 수 없습니다.
따라서 Graphiql-batch라는 GraphiQL을 위한 N+1 제거 라이브러리를 사용합니다.
https://github.com/Shopify/graphql-batch
Gemfile.rb
+ gem 'graphql-batch'
$ bundle install

GraphiQL:Batch 활성화


그런 다음 GraphiQL:Batch를 활성화합니다.
디렉터리에 있는 임의의 응용 이름 schema입니다.rb가 있으니까 거기서 보충해 주세요.
app/graphql/rails_graphql_api_schema.rb
class RailsGraphqlApiSchema < GraphQL::Schema
  mutation(Types::MutationType)
  query(Types::QueryType)

+ use GraphQL::Batch

  # 略
end

Loader 설치


GraphiQL:Batch::Loader를 계승하여 Loader를 제작합니다.

AssociationLoader 설치


Record와 관련된 Record에서 얻은 Loader를 요약하여 작성합니다.
graphiql 아래에서loaders 디렉터리를 만듭니다.
$ mkdir app/graphql/loaders
example의 associationloader를 설치합니다.
https://github.com/Shopify/graphql-batch/blob/master/examples/association_loader.rb
Loader 내에서 무엇을 하는지 알고 싶은 사람이 이 글을 참고했으니 참고하세요.
https://blog.kymmt.com/entry/graphql-batch-examples
$ touch app/graphql/loaders/association_loader.rb
app/graphql/loaders/association_loader.rb
module Loaders
  class AssociationLoader < GraphQL::Batch::Loader
    def self.validate(model, association_name)
      new(model, association_name)
      nil
    end

    def initialize(model, association_name)
      super()
      @model = model
      @association_name = association_name
      validate
    end

    def load(record)
      raise TypeError, "#{@model} loader can't load association for #{record.class}" unless record.is_a?(@model)
      return Promise.resolve(read_association(record)) if association_loaded?(record)

      super
    end

    # We want to load the associations on all records, even if they have the same id
    def cache_key(record)
      record.object_id
    end

    def perform(records)
      preload_association(records)
      records.each { |record| fulfill(record, read_association(record)) }
    end

    private

    def validate
      return if @model.reflect_on_association(@association_name)

      raise ArgumentError, "No association #{@association_name} on #{@model}"
    end

    def preload_association(records)
      ::ActiveRecord::Associations::Preloader.new.preload(records, @association_name)
    end

    def read_association(record)
      record.public_send(@association_name)
    end

    def association_loaded?(record)
      record.association(@association_name).loaded?
    end
  end
end


그런 다음 PostType에서 comments 메서드를 작성합니다.
AssociationLoader를 통해 comments를 가져올 수 있습니다.
app/graphql/types/post_type.rb
module Types
  class PostType < Types::BaseObject
    field :body, String, null: false
    field :comments, [Types::CommentType], null: false
    field :created_at, GraphQL::Types::ISO8601DateTime, null: false
    field :id, ID, null: false
    field :title, String, null: false
    field :updated_at, GraphQL::Types::ISO8601DateTime, null: false

+   def comments
+     Loaders::AssociationLoader.for(Post, :comments).load(object)
+   end
  end
end
Types:BaseObject의field에서 이 파일에 방법이 있으면 이 파일을 우선적으로 실행합니다. object.comments임의로 방법을 지정할 수 있습니다field :comments, [Types::CommentType], null: false, method: :loader_comments.
지금 다시 실행해 봅시다.
Started POST "/graphql"
  Post Load (2.0ms)  SELECT "posts".* FROM "posts" ORDER BY "posts"."created_at" DESC
  Comment Load (2.4ms)  SELECT "comments".* FROM "comments" WHERE "comments"."post_id" IN ($1, $2, $3)  [[nil, 1], [nil, 2], [nil, 3]]
Completed 200 OK
이렇게 간단한 연관 기록의 N+1이 해제되었다.

계수기를 설치하다


다음은 투고에 대한 평론 수를 구해 보세요.
간단하게 쓰면 이렇게 N+1이 나온다.
post_type.rb
module Types
  class PostType < Types::BaseObject
    # 略

+   def total_comments
+     object.comments.count
+   end
  end
end
query {
  posts {
    id
    title
    totalComments
  }
}
Started POST "/graphql"
  Post Load (6.5ms)  SELECT "posts".* FROM "posts" ORDER BY "posts"."created_at" DESC
   (2.9ms)  SELECT COUNT(*) FROM "comments" WHERE "comments"."post_id" = $1  [["post_id", 1]]
   (4.9ms)  SELECT COUNT(*) FROM "comments" WHERE "comments"."post_id" = $1  [["post_id", 2]]
   (1.9ms)  SELECT COUNT(*) FROM "comments" WHERE "comments"."post_id" = $1  [["post_id", 3]]
Completed 200 OK
역시 Loader를 구현합니다.
CountLoader의 설치는 이곳을 참고하여 AssitionLoader와 같은 방법으로 변경되었습니다.
https://blog.jamesbrooks.net/graphql-batch-count-loader.html
$ touch app/graphql/loaders/association_count_loader.rb
app/graphql/loaders/association_count_loader.rb
module Loaders
  class AssociationCountLoader < GraphQL::Batch::Loader
    def self.validate(model, association_name)
      new(model, association_name)
      nil
    end

    def initialize(model, association_name, where: nil)
      super()
      @model = model
      @association_name = association_name
      @reflection = reflection
      @where = where
    end

    def load(record)
      raise TypeError, "#{@model} loader can't load association for #{record.class}" unless record.is_a?(@model)

      super
    end

    def perform(records)
      counts = query(records)
      records.each do |record|
        key = record_key(record)
        fulfill(record, counts[key] || 0)
      end
    end

    private

    def reflection
      reflection = @model.reflect_on_association(@association_name)
      return reflection if reflection

      raise ArgumentError, "No association #{@association_name} on #{@model}"
    end

    def query(records)
      column = @reflection.join_primary_key
      scope = @reflection.klass
      scope = scope.where(@where) if @where
      scope.where(column => records).group(column).count
    end

    def record_key(record)
      record[@reflection.active_record_primary_key]
    end
  end
end
Loader를 사용하여 Count를 가져오도록 변경합니다.
app/graphql/types/post_type.rb
module Types
  class PostType < Types::BaseObject
    # 略

    def total_comments
-     object.comments.count
+     Loaders::AssociationCountLoader.for(Post, :comments).load(object)
    end
  end
end
그럼 실행해 보세요.

Started POST "/graphql"
  Post Load (2.6ms)  SELECT "posts".* FROM "posts" ORDER BY "posts"."created_at" DESC
   (2.6ms)  SELECT COUNT(*) AS count_all, "comments"."post_id" AS comments_post_id FROM "comments" WHERE "comments"."post_id" IN ($1, $2, $3) GROUP BY "comments"."post_id"  [[nil, 1], [nil, 2], [nil, 3]]
Completed 200 OK
이렇게 하면 SQL의 COUNT와 같은 합계 함수를 사용하여 관련 데이터를 실현할 수 있다.

참고 자료


https://blog.agile.esm.co.jp/entry/2017/06/16/113248
https://blog.kymmt.com/entry/graphql-batch-examples
https://blog.jamesbrooks.net/graphql-batch-count-loader.html

좋은 웹페이지 즐겨찾기