久々に投稿します。
最近業務で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の数が増えれば増えるほど、EnumType
やScalarType
やObjectType
が混在して、見にくくなってきます。
そのため、予め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を書けばいいかが記載されています。
以下のようにして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
を使って開発する方にとって参考になれば幸いです。