[N+1 제거] Rails의 GraphiQL에서 graphil-batch 가져오기
래일스를 이용해 그라피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를 얻을 때까지 설치하는 방법에 대해 이 글에서 설명하였다.현재 모델은 다음과 같습니다.
# 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.rbclass 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 제거 라이브러리를 사용합니다.
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를 설치합니다.Loader 내에서 무엇을 하는지 알고 싶은 사람이 이 글을 참고했으니 참고하세요.
$ touch app/graphql/loaders/association_loader.rb
app/graphql/loaders/association_loader.rbmodule 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와 같은 방법으로 변경되었습니다.
$ touch app/graphql/loaders/association_count_loader.rb
app/graphql/loaders/association_count_loader.rbmodule 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와 같은 합계 함수를 사용하여 관련 데이터를 실현할 수 있다.참고 자료
Reference
이 문제에 관하여([N+1 제거] Rails의 GraphiQL에서 graphil-batch 가져오기), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://zenn.dev/necocoa/articles/setup-graphql-batch텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)