Posted at

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

久々に投稿します。

最近業務で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を使って開発する方にとって参考になれば幸いです。