20
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

graphql-rubyを使ってみて感じたこと、学んだこと

Posted at

久々に投稿します。

最近業務でgraphql-rubyを使ってみたので、色々と感じたこと、学んだことをまとめようと思いました。
GraphQL自体使うのが初めてだったので、なかなか最初は困惑しましたが、使ってみると便利だなと感じるポイントがたくさんあったので、今後、APIはGraphQLを最優先の選択肢でいきたいなと思いました。

環境

Ruby 2.5.1
GraphQL 1.8.7

学んだこと、感じたこと

その① 自分が使っているバージョンのドキュメントをちゃんと読み込むこと

当たり前のことですが、これがめちゃくちゃ大事です。

ちなみに、ドキュメントはこちら

私は、2018年8月時点の最新バージョンである「1.8.7」を使ったのですが、1.8からclass-based APIに変わっていまして、それで大分苦戦を強いられました。
ちなみに1.8が公開されたのは、2018年5月18日です。

具体的にどういうことかというと、QueryやMutationなどの書き方が、全然変わっていました。

# 1.7以前

## Mutation
MutationType = GraphQL::ObjectType.define do
  name "Mutation"
  field :createPost, types.Post do
    resolve ->(obj, args, ctx) {
      obj # => #<Organization id=456 ...>
      # ...
    }
  end
end

## Schema定義
ObjectTypes::User = GraphQL::ObjectType.define do
  name "User"
  field :id, !types.ID
  field :name, types.String
  field :posts, ObjectTypes::PostList do
    resolve ->(obj, args, ctx) {
      Post.where(...)
    }
  end
end

# 1.8以降

## Mutation
class MutationType < GraphQL::Schema::Object
  field :create_post, Post, null: true
  def create_post(**args)
    object # => #<Organization id=456 ...>
    # ...
  end
end

## Schema定義
class UserType < GraphQL::Schema::Object
  field :posts, [ObjectTypes::PostType], null: true

  def posts
    Post.where(...)
  end
end
    

マイナーバージョンが上がっただけなのに、この変わりよう、、、
ちなみに、2018年8月の時点では、巷に掲載されている記事は1.7までのもので公開されていたので、それを参考にするとめちゃくちゃつまずきます。

マイナーバージョンでこんなにも書き方が変わった例があると、1.9になったときも変わる可能性は十分にあるので、自分の使っているバージョンのドキュメントを読み込むことはすごく大事です。

ただ、最新のドキュメントのコード例にも、ちらほらと1.7の記述のものがあったりなかったり。英語に自信がある方はPR出したら通ると思います。

その② graphiql-railsは必須

これはもう必須です。
graphiql-railsを入れるだけで、Graphiqlを使用することができるため、開発がそれだけで爆速になります。
以下は、GitHubが公開しているGraphiqlです。使用してみると、何が便利かがより分かるかと思います。

Graphiqlのサンプル→https://developer.github.com/v4/explorer/

これからgraphql-rubyを使用して開発しようとしている方は、まずはgrapiql-railsを入れてください。

その③ 普通に書いているとN+1問題が多発する

GraphQLの便利さにかまけて、めちゃくちゃ書いていると、こんなことが起きてしまいます。
例えば、以下のようなQueryを投げた場合、確実にN+1問題がおきます。

{
  user {
    id
    name
    posts {
      edges {
        node {
          id
          title
        }
      }
    }
  }
}

postsを取得するところで、N+1になってしまいますね。

これは色々なところで語られていて、graphql-batchを使えばいいとか、QueryのFieldsの定義でincludesを使えばいいとか色々出ています。

Q: Prevent N+1 queries
graphql-rubyにおけるN+1対策

色々と出ていますが、私が担当しているプロジェクトは結構リリースまで時間がなく、追われている状況だったので、最も簡単な方法として、

  • 確実にQueryで取得すると分かっているものは、eager_loadで結合する
  • それ以外のものは、includesを使って、呼び出されるときに、N+1が起きないようにする

という手法を取りました。
この方法なら、時間もかからず実装できるし、SQLの発行回数が格段に減ります。
特に、開発初期なら、発行されるQueryは全て予想できますからね。

リリース後になると思いますが、できたら以下のようなことをやって、もっとパフォーマンス上げたいですね。

シンプルさとパフォーマンスを両立した API 設計と実装の一例

その④ ファイルレイアウトは、種類ごとにディレクトリを分ける

こちらは以下の記事を参考にさせていただき、ディレクトリごとにファイルを分けるようにしました。

参考:RailsでGraphQL APIを作る時に悩んだ5つのこと

公式ドキュメントでは、Baseクラスを作成することを推奨していますが、全てtypeディレクトリ以下に配置していました。
ただ、Modelの数が増えれば増えるほど、EnumTypeScalarTypeObjectTypeが混在して、見にくくなってきます。

そのため、予めtypeごとにディレクトリを分けてみました。
ほぼ、上記の参考記事と同様に分けてみました。

app/graphql/enum_types/
           /input_types/
           /interface_types/
           /mutations/
           /object_types/
           /scalar_types/

ほぼ参考記事通りですが、input_typesのみ、再利用性だけではなく、見やすさも考慮して、Mutationで更新したいものは、以下のようにInputTypesにまとめるようにしました。

module InputTypes
  class CourseInputType < InputTypes::BaseInputObject
    graphql_name 'CourseInputType'

    argument :name, String, required: true
    argument :content, String, required: true
    argument :price, Integer, required: false
    #...
  end
end

もちろん、再利用性も考えていますが、見やすさを一番に考慮してまとめています。

その⑤ エラーハンドリングの扱い

こちらについては意見が分かれそうですが、私は公式ドキュメントに則って、errorsフィールドを返す仕様にしてみました。

具体的には、以下のような感じです(公式ドキュメント抜粋)。

def resolve(id:, attributes:)
  post = Post.find(id)
  if post.update(attributes)
    {
      post: post,
      errors: [],
    }
  else
    # Convert Rails model errors into GraphQL-ready error hashes
    user_errors = post.errors.map do |attribute, message|
      # This is the GraphQL argument which corresponds to the validation error:
      path = ["attributes", attribute.camelize]
      {
        path: path,
        message: message,
      }
    end
    {
      post: post,
      errors: user_errors,
    }
  end
end

GrqphQLはその特性上、エラーコードを返しても、「どこでエラーが起きたのか」がわからないので、基本は200を返して、errorsにエラーの内容を返すようにしました。
こちらも以下の記事を参考にさせていただきました。

参考:RailsでGraphQL APIを作る時に悩んだ5つのこと

その⑥ テストはQuery、Mutationごとに書く

これはすごく意見が分かれそうですが、このようにまとめてみました。
公式ドキュメントにも、どのようにspecを書けばいいかが記載されています。

Executing GraphQL queries

以下のようにしてQueryのテストをしています(公式ドキュメント抜粋)。

RSpec.describe MySchema do
  # You can override `context` or `variables` in
  # more specific scopes
  let(:context) { {} }
  let(:variables) { {} }
  # Call `result` to execute the query
  let(:result) {
    res = MySchema.execute(
      query_string,
      context: context,
      variables: variables
    )
    # Print any errors
    if res["errors"]
      pp res
    end
    res
  }

  describe "a specific query" do
    # provide a query string for `result`
    let(:query_string) { %|{ viewer { name } }| }

    context "when there's no current user" do
      it "is nil" do
        # calling `result` executes the query
        expect(result["data"]["viewer"]).to eq(nil)
      end
    end

    context "when there's a current user" do
      # override `context`
      let(:context) {
        { current_user: User.new(name: "ABC") }
      }
      it "shows the user's name" do
        user_name = result["data"]["viewer"]["name"]
        expect(user_name).to eq("ABC")
      end
    end
  end
end

トップレベルのdescribeはQuery名にして、より分かりやすい名前にしました。
また、query、mutationごとにファイルを分けて、以下のような構造でファイルを作成しました。

spec/request/graphql/query/course_spec.rb
                           user_spec.rb
                    /mutation/create_course_spec.rb
                              create_user_spec.rb

総評

色々と最初は戸惑うところもありましたが、使ってみるととても便利で、開発も慣れてくると、graphiql-railsのおかげで爆速です。
APIをRailsで開発するのであれば、選択肢として、まずGraphQLを考えてみてもよいかなと思います。

つらつらと書き並べてみましたが、少しでもこれからgraphql-rubyを使って開発する方にとって参考になれば幸いです。

20
10
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
20
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?