patorashのブログ

方向性はまだない

GraphQLでWebAPIを作っている

自分が担当のプロダクトのWebAPIを整理していかんとほんまヤバいなぁ〜と思いつつ、数年過ごしていましたが、ここ最近で急激にGraphQL熱が湧き上がり、今お試しで実装していってる最中です。

ビビッときたのは、@gfxさんの書かれたGraphQL徹底入門の記事からです。

employment.en-japan.com

これを元に簡単な実装をしてみて、他の情報を探して…という感じ。

qiita.com

N+1にはgraphql-batch

概要を掴んだら、ModelにマッピングするようにTypeを生成していってたのですが、N+1問題が発生しやすいみたいな話を聞いていた通り、すぐ出始めました。graphql-batchを使えば、発生が抑えられるとのことだったので、情報を集めてやってみたところ、N+1は収まりました。

blog.agile.esm.co.jp

blog.kymmt.com

preloadして、極力クエリの発行を抑えるようになっています。ありがたい。

ちなみに最初は『preloadやeager_loadを使えば別にN+1起きないんじゃない?』と思って、Foo.preload(:bar)とかしていたのですが、これだとbarの情報にアクセスしていなくてもbarの情報を取得するためのクエリが実行されてしまいました(N+1にはならないけれど、無駄なクエリ)。graphql-batchは、barの情報にアクセスするときだけ、barをpreloadしてくれたので、graphql-batchを使った方がいいでしょう。

Loaderの実装は、サンプルからそのまま拝借しました。

graphql-batch/examples at 058213b78775c791135bf7db784b7d10007d5ade · Shopify/graphql-batch · GitHub

Loaderの記述がしんどい

とはいえ、Loaderの記述が多くなると、同じようなコードになってしまい辛いです。

module Types
  class HogeType < Types::BaseObject
    field :id, ID, null: false
    field :piyos, [Types::PiyoType], null: false
    field :fugas, [Types::FugaType], null: false

    # piyosもfugasも殆ど同じ…
    def piyos
      Loaders::AssociationLoader.for(Hoge, :piyos).load(object)
    end

    def fugas
      Loaders::AssociationLoader.for(Hoge, :fugas).load(object)
    end
  end
end

というわけでメタプログラミングします。継承元のTypes::BaseObjectにクラスメソッドpreload_associationsを追加しました。やってることは、引数のシンボル名のメソッドを定義してしまうということだけです。

module Types
  class BaseObject < GraphQL::Schema::Object

    def self.preload_associations(*assciations)
      model_name = self.class_name[0..-5] # HogeTypeのTypeを削除する
      model = Kernel.const_get model_name # model Hoge を取得
      assciations.each do |association_name|
        define_method association_name do
          Loaders::AssociationLoader.for(model, association_name).load(object)
        end
      end
    end
  end
end

これにより、preload_assciationsを呼べば済むようになりました🎉

module Types
  class HogeType < Types::BaseObject
    field :id, ID, null: false
    field :piyos, [Types::PiyoType], null: false
    field :fugas, [Types::FugaType], null: false

    # これでOK
    preload_associations :piyos, :fugas
  end
end

配列型のせいでN+1発生もLoaderで解決

PostgreSQLの配列型を一部で使っているのですが、これもN+1問題が発生しました。配列型ではRailsのAssociationを表現しているわけではないので、自分でLoadしてあげないといけません。

module Types
  class HogeType < Types::BaseObject
    field :id, ID, null: false

    # これらはAssociation
    field :piyos, [Types::PiyoType], null: false 
    field :fugas, [Types::FugaType], null: false

    preload_associations :piyos, :fugas

    # これらは配列型
    field :foos, [Types::FooType], null: false
    field :bars, [Types::BarType], null: false

    # 配列型はload_manyを使って登録しておくとよい
    def foos
      Loaders::RecordLoader.for(Foo).load_many(object.foo_ids)
    end

    def bars
      Loaders::RecordLoader.for(Bar).load_many(object.bar_ids)
    end
  end
end

今の所の感想

graphqlの実装していくのは楽しい!!graphiqlからクエリ発行して芋づる式にデータが取れるのがめちゃくちゃ面白いです。ただし、クエリのネストが深くなると途端に重くなりますが…。 まだ更新系のクエリの実装はしていないので、そちらでも知見が貯まったら何か書こうと思います。