こんにちは。Studyplusでサーバーサイドを担当している金澤です。 弊社ではいまapiの一部にGraphQLを導入するべく取り組んでいます。
GraphQLってなんだという話や導入手順などはweb上にすでに沢山あると思います。 なのでそのへんはあっさりめで、検証にあたってどのような実装をしているかという話をします。
で、GraphQLってなんだ
公式ページから言葉を借りれば、
A query language for your API
です。
apiに対する問い合わせをクライアントで組み立てて柔軟にできます。
上記ページのデモがとても分かりやすいのでピンと来ない方は是非ご覧ください。
GraphQLでできる3つのこと
query
- データの問い合わせ
- 今回はこの話だけします
mutation
- データの変更
subscription
- いわゆるpub/sub
なぜGraphQLなのか
動機としては
- パフォーマンスの改善のため、各種apiを一つにまとめようという話が以前からあった
- GraphQLによってパフォーマンスがよくなるという話ではなく、apiが細分化しすぎていてインデックスも複雑(あるいは効いてない)というケースを整理しましょうという流れ
- 画面をトライアンドエラーするとき、いちいち微調整にコミュニケーションコストがかかるのは勿体無い
- ある程度好きにできるapiを用意するので、開発フェーズによってはそれを柔軟に使って完結してほしい
- クライアントとサーバを同じ人が作るような体制の場合はこういう凝った仕組み必要ないかもしれない
- ナウい
- たまには新しいものを取り入れないと澱む
といったところです。
導入方法
まっさらなrailsプロジェクトにGraphQLを導入する手順です。
railsプロジェクトを作る
$ rails new graphql_test
graphql-rubyのインストール
https://github.com/rmosolgo/graphql-ruby
gem 'graphql'
$ bundle install
$ rails generate graphql:install
上記コマンドでGemfileに
gem 'graphiql-rails', group: :developmentというのが追加されるので、使う場合はもう一度bundle install- graphiqlは開発用のインタラクティブな画面です
- 多分graphicとgraphqlの言葉遊びなのかな?
app/graphql以下にいろいろ関連ファイルができます
application.rbにconfig.eager_load_paths << "#{config.root}/app/graphql"を足すと便利
graphiql
rails s して http://localhost:3000/graphiql から見れます
- 弊社は認証にヘッダ情報を使っているので、下記設定から追加する必要がありました
- しかし下記のように画面へ埋め込まれる形になるので、rails再起動だけではなく画面をリロードする必要があります(30分ぐらいはまった!)
Hello, World!
インストールが終わったら、graphiql画面に{ testField }と入力して実行しましょう。
いつものが返ってきます。
Queryで頻出しそうなパターンをどう実装したかの話
ここからが本題です。 データの問い合わせapiを作るにあたって、よく登場するパターンをどう解決したかという例を三つほどご紹介します。
ユーザー権限をチェックしたいパターン
正しくインストールされていると、app/controllers/graphql_controller.rb というコントローラができてます。
下記のような感じです。
class GraphqlController < ApplicationController
def execute
variables = ensure_hash(params[:variables])
query = params[:query]
operation_name = params[:operationName]
context = {
# Query context goes here, for example:
# current_user: current_user,
}
result = GraphqlTestSchema.execute(
query, variables: variables, context: context, operation_name: operation_name
)
render json: result
rescue => e
raise e unless Rails.env.development?
handle_error_in_development e
end
# 以下略
コメントにあるように、コンテキストを意識したい場合はcontext に追加し、後から取り出せます。
権限チェックのためにログインユーザーが欲しい、というような場合は例えば下記のように実現できます(実際のコードではないです)。
GraphiQL::Rails.config.headers["Authorization"] = -> (context) { "Bearer hogehoge" }
# 前略
auth_header = request.headers["Authorization"]
session = find_session_by_header(auth_header)
context = {
session: session
}
# 以下略
こうすると、query_typeやobject_type内でresolveする際にctxからセッションを取得できるようになります。
field :test, types.String do
resolve ->(obj, args, ctx) {
ctx[:session] # コントローラでセットしたオブジェクト
}
}
ページングするパターン
データの問い合わせにページを指定したり、検索文字列を指定したりなどよくあると思います。
以下のようにクエリにはargumentを設定することができます。
Types::QueryType = GraphQL::ObjectType.define do
name "Query"
field :page, !types[types.Int] do
description "ページングテスト"
argument :per_page, types.Int, default_value: 5, prepare: ->(limit, _ctx) { [limit, 5].min }
argument :page, types.Int, default_value: 1
resolve ->(_obj, args, ctx) {
page = args[:page]
limit = args[:per_page]
arr = %w(1 2 3 4 5 6 7 8 9 10)
offset = [(page - 1), 0].max * limit
last = offset + limit
arr[offset...last]
}
end
end
上記query_type.rbを上記の内容にし、下記クエリをqraphiql画面から入力することで、ページングが確認できます。
{ page(per_page: 3, page: 2) }
{
"data": {
"page": [
4,
5,
6
]
}
}
lazyに呼ぶパターン
GraphQLはapi呼び出し側でクエリを自由に書けるので、呼び出される側が無駄な計算をしないように気をつける必要がありました。
例えば、下記のようなTypeがあったとします。
Types::Test::LazyType = GraphQL::ObjectType.define do name 'Lazy' field :karui, !types.String field :omoi, !types.String field :sinu, !types.String end
このtypeを使った返事を以下のように実装した場合、
Types::QueryType = GraphQL::ObjectType.define do
name "Query"
field :lazy, !Types::Test::LazyType do
resolve ->(_obj, _args, _ctx) {
karui = "軽い!"
omoi = -> () {
sleep(3)
"重い..."
}.call # ここが重い
sinu = ->() {
raise "死ぬ"
}.call # ここで死ぬ
OpenStruct.new(karui: karui, omoi: omoi, sinu: sinu)
}
end
end
- クエリで必要なものだけ取得できる意味がない
- というかこの場合は必ず死ぬ
- しかも順番に実行されていくので重い作業を待った末に死ぬ
という本末転倒なことになります。
そこで下記のように、必要な分だけ計算するようにしました。
Types::QueryType = GraphQL::ObjectType.define do
name "Query"
field :lazy, !Types::Test::LazyType do
resolve ->(_obj, _args, _ctx) {
res = OpenStruct.new
def res.karui
"軽い!"
end
def res.omoi # ここでは重くない
sleep(3)
"重い..."
end
def res.sinu # ここでは死なない
raise "死ぬ"
end
res
}
end
end
こうすることで、
{ lazy { karui } }実行時は軽い{ lazy { karui omoi } }実行時は重いものを取得したいのでしょうがない{ lazy { sinu karui omoi } }とすることで一瞬で死ぬため計算資源を無駄にしない- ただし、
{ lazy { karui omoi sinu} }とすると順番に実行されてやはりomoi分が無駄になる
- ただし、
当たり前の話ですが、個別に実行できるようにし、必要なものを必要なだけ取るようにできると無駄がありませんでした。
終わりに
まだ手探り状態なので、実運用に乗せたら何が起きるかなどはわかっていませんし、テスト運用して採用を取りやめる可能性もゼロではないと思っています。
起きそうな問題としてパッと思いつくのは、
- cartesian product問題やN+1問題に代表されるようなデータアクセス上の典型的諸問題が、レイヤーを一段またいで脳みそが二つになることによってより厄介になったりしないだろうか
- たくさん取得してクライアントで選別しよう、みたいなコードを書こうと思えば書けてしまう
- もちろんapiの作りっぷりで制限することは可能で、そういう意味では従来の作りとあまり変わっていないかもしれない
- endpointが一つになるので、NewRelicなどでパフォーマンス計測する際よくわからないことにならないか?
- クエリ別に集計する必要がありそうだが対応しているのか?
{ hoge fuga piyo}とfuga piyo hogeは同じものとして集計されてほしいのか違うのかなど場合によって違いそう
- 関係ないけどNewRelicの無料プランがだいぶ制限されるようなのでどなたか良い代替プロダクトを教えてください
- クエリ別に集計する必要がありそうだが対応しているのか?
といったところでしょうか。
とはいえ、うまく使えば開発効率が上がりそうな手応えも感じています。 色々な導入例や運用例から、ベストプラクティスやアンチパターンが出来上がっていくと思いますし、追随していきたいと思います。