[Elixir] Dataloader를 가져와서 GraphiQL의 N+1 문제를 해결합니다
이 기사는 N+1 문제를 해결하기 위해 데이터loader를 가져오는 절차를 소개한다.
!
데이터로드러가 뭔지 기사 검색하면 이 기사가 많이 나오니까.자세한 내용은 참조로 구현된 JavaScript의 웨어하우스 등에 관한 기사를 참조하십시오.
샘플 항목
여기에는 잠시 이쪽 기사에 실린 코드가 놓여 있습니다.
샘플 생성 명령을 입력하고 있습니다.
$ mix phx.new phx-dataloader-sample --app dataloader_sample --no-html --database sqlite3
$ mix phx.gen.context Blog Post posts title:string body:text
$ mix phx.gen.context Blog Comment comments body:text post_id:references:posts
테이블 정의
투고에 여러 논평을 덧붙이는 단순한 구성으로 추진된다.
seed로 여러 개의commeent와post가 연관된 상황을 신속하게 제작합니다.faker 사용하면 편리해요.
alias DataloaderSample.Blog.Comment
alias DataloaderSample.Blog.Post
Enum.each(1..5, fn _ ->
post =
%Post{title: Faker.Lorem.sentence(), body: Faker.Lorem.paragraph()}
|> DataloaderSample.Repo.insert!()
Enum.each(1..5, fn _ ->
%Comment{body: Faker.Lorem.paragraph(), post_id: post.id} |> DataloaderSample.Repo.insert()
end)
end)
문제가 발생하려고 시도하다
우선 N+1에서 발생한 실복을 살펴보자.schema의 실현은 다음과 같다.해석기는
phx.gen.context
생성된 함수만 두드린다.schema.ex
defmodule DataloaderSampleWeb.Schema do
use Absinthe.Schema
alias DataloaderSampleWeb.BlogResolver
object :post do
field :id, non_null(:id)
field :title, non_null(:string)
field :body, non_null(:string)
field :comments, non_null(list_of(:comment)) do
resolve(fn post, _, _ ->
comments = Ecto.assoc(post, :comments) |> DataloaderSample.Repo.all()
{:ok, comments}
end)
end
end
object :comment do
field :id, non_null(:id)
field :body, non_null(:string)
end
query do
field :posts, non_null(list_of(non_null(:post))) do
resolve(&BlogResolver.list_posts/3)
end
end
end
blog_resolver.exdefmodule DataloaderSampleWeb.BlogResolver do
alias DataloaderSample.Blog
def list_posts(_, _, _) do
{:ok, Blog.list_posts()}
end
end
모든 평론을 얻기 위해query를 발행합니다.{
posts {
title
body
comments {
id
body
}
}
}
로그에서 보듯이comments표에posts가 발행된 기록에 대해 여러 차례 조회했다.[debug] QUERY OK source="posts" db=0.0ms idle=673.2ms
SELECT p0."id", p0."body", p0."title", p0."inserted_at", p0."updated_at" FROM "posts" AS p0 []
↳ DataloaderSampleWeb.BlogResolver.list_posts/3, at: lib/dataloader_sample_web/resolvers/blog_resolver.ex#5
[debug] QUERY OK source="comments" db=0.1ms queue=0.1ms idle=707.0ms
SELECT c0."id", c0."body", c0."post_id", c0."inserted_at", c0."updated_at" FROM "comments" AS c0 WHERE (c0."post_id" = ?) [1]
↳ anonymous fn/3 in DataloaderSampleWeb.Schema.__absinthe_function__/2, at: lib/dataloader_sample_web/schema.ex#13
[debug] QUERY OK source="comments" db=0.1ms idle=709.2ms
SELECT c0."id", c0."body", c0."post_id", c0."inserted_at", c0."updated_at" FROM "comments" AS c0 WHERE (c0."post_id" = ?) [2]
↳ anonymous fn/3 in DataloaderSampleWeb.Schema.__absinthe_function__/2, at: lib/dataloader_sample_web/schema.ex#13
[debug] QUERY OK source="comments" db=0.1ms idle=299.9ms
SELECT c0."id", c0."body", c0."post_id", c0."inserted_at", c0."updated_at" FROM "comments" AS c0 WHERE (c0."post_id" = ?) [3]
↳ anonymous fn/3 in DataloaderSampleWeb.Schema.__absinthe_function__/2, at: lib/dataloader_sample_web/schema.ex#13
[debug] QUERY OK source="comments" db=0.1ms idle=280.4ms
SELECT c0."id", c0."body", c0."post_id", c0."inserted_at", c0."updated_at" FROM "comments" AS c0 WHERE (c0."post_id" = ?) [4]
↳ anonymous fn/3 in DataloaderSampleWeb.Schema.__absinthe_function__/2, at: lib/dataloader_sample_web/schema.ex#13
[debug] QUERY OK source="comments" db=0.1ms idle=37.5ms
SELECT c0."id", c0."body", c0."post_id", c0."inserted_at", c0."updated_at" FROM "comments" AS c0 WHERE (c0."post_id" = ?) [5]
↳ anonymous fn/3 in DataloaderSampleWeb.Schema.__absinthe_function__/2, at: lib/dataloader_sample_web/schema.ex#13
이 상태에서 투고 수, 평론 수가 증가하면 성능이 악화된다.데이터 가져오기 도우미
일부러 N+1을 깨워도 문제 해결을 위해 Dataloader 라이브러리를 가져왔습니다.
mix.exs
defp deps do
[
# ... 略
{:dataloader, "~> 1.0.0"}
]
end
$ mix deps.get
deps에 추가한 후 schema 파일을 수정합니다.해야 할 일은 대략 다음과 같은 세 가지가 있다.:loader
의 키로 context에 데이터 로더 구조체 추가 Absinthe.Middleware.Dataloader
plugensAbsinthe.Resolution.Helpers.dataloader/1
로 다시 쓰기defmodule DataloaderSampleWeb.Schema do
use Absinthe.Schema
# Helpersをimport。dataloader/1 を呼べるようにする
import Absinthe.Resolution.Helpers
alias DataloaderSample.Blog
alias DataloaderSampleWeb.BlogResolver
object :post do
field :id, non_null(:id)
field :title, non_null(:string)
field :body, non_null(:string)
field :comments, non_null(list_of(:comment)) do
# dataloader/1を実行。Dataloader.add_source/3の第2引数に指定したものと同じ値(source)を指定
resolve(dataloader(Blog))
end
end
# ... 略
# context/1を追加(Absinthe.Schemaマクロでdefoverridableに指定されている)
def context(ctx) do
loader =
Dataloader.new()
|> Dataloader.add_source(Blog, Blog.data())
Map.put(ctx, :loader, loader)
end
# plugins/0を追加(Absinthe.Schemaマクロでdefoverridableに指定されている)
def plugins() do
[Absinthe.Middleware.Dataloader] ++ Absinthe.Plugin.defaults()
end
end
blog.exdefmodule DataloaderSample.Blog do
@moduledoc """
The Blog context.
"""
import Ecto.Query, warn: false
alias DataloaderSample.Repo
alias DataloaderSample.Blog.Post
# Dataloader.add_source/3 で利用するdata/0を実装
def data() do
Dataloader.Ecto.new(DataloaderSample.Repo, query: &query/2)
end
def query(queryable, _params) do
queryable
end
# ...略
end
Dataloader.new/0
loader를 제작하는 부분에서absinthehexdocs의 예법에 따라 다음과 같은 함수로 나뉜다.Blog
상하문의 모듈을 데이터 원본으로 지정In a Phoenix application you'll generally have one source per context, so that each context can control how its data is loaded.
data/0
, 그리고 반환Dataloader.Ecto.new/2
의 결과.Dataloader에서 사용할 쿼리 지정 def query(Post, %{has_admin_rights: true}), do: Post
def query(Post, _), do: from p in Post, where: is_nil(p.deleted_at)
def query(queryable, _), do: queryable
또 resolve(dataloader(Blog))
만 쓰면field명:comments
에서 포스트구조체에서 자란has_many: :comments, Comment
까지 연결해 해결할 수 있다.이거 영리하네.https://hexdocs.pm/absinthe/dataloader.html#usage
In this example, the query returned by query/2 is used as a starting point by Dataloader to build the final query, which it does by traversing schema associations. In other words, Dataloader can determine that an author has many posts, and that to retrieve posts it needs to get those with the relevant author_id. If that's sufficient for your needs, query/2 need not modify the query it's given. But if you only want to load published posts, query/2 can narrow the query accordingly.
schema "posts" do
# ... 略
has_many :comments, Comment
end
실제 행동을 봐라.같은query를 던져봐, 1~5의post 발견나는 id가 하나의 select로 정리되었다는 것을 안다.이렇게 N+1 문제가 해결되었습니다![debug] QUERY OK source="posts" db=0.1ms queue=0.1ms idle=874.9ms
SELECT p0."id", p0."body", p0."title", p0."inserted_at", p0."updated_at" FROM "posts" AS p0 []
↳ DataloaderSampleWeb.BlogResolver.list_posts/3, at: lib/dataloader_sample_web/resolvers/blog_resolver.ex#5
[debug] QUERY OK source="comments" db=0.3ms idle=923.1ms
SELECT c0."id", c0."body", c0."post_id", c0."inserted_at", c0."updated_at", c0."post_id" FROM "comments" AS c0 WHERE (c0."post_id" IN (?,?,?,?,?)) ORDER BY c0."post_id" [5, 4, 3, 2, 1]
↳ Dataloader.Source.Dataloader.Ecto.run_batch/2, at: lib/dataloader/ecto.ex#680
총결산
Absinthe를 사용하는 GraphiQL 서버의 설치에서 데이터loader를 가져와 N+1을 제거하는 절차에 대한 간단한 샘플을 소개했다.뇌사로 이뤄지면 N+1 벽에 부딪히기 쉬우므로 연기가 악화하지 않도록 함께 지내면서 시행했으면 좋겠다.
영어책이지만 다음 책의 chapter9에서 데이터로드러의 기술을 배울 수 있다.관심 있는 사람은 꼭 봐야 한다.
Reference
이 문제에 관하여([Elixir] Dataloader를 가져와서 GraphiQL의 N+1 문제를 해결합니다), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://zenn.dev/koga1020/articles/14a49472394b22텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)