実際に使ってみた
使う model は下記 (実際は login_id, password とかもあるけど割愛
今回は users#show products#index 相当の処理を実装する
$ rails g model User name:string
$ rails g model Product user_id:integer name:string
$ rails g model Love user_id:integer product_id:integer
model 周りの実装も割愛
初期設定
+gem 'graphql'
+gem 'graphql-batch'
Gemfile に常軌を追加して bundle install する
その後諸々の初期ファイルを作ってくれる下記を実行
$ bundle exec rails generate graphql:install
Running via Spring preloader in process 58662
create app/graphql/types
create app/graphql/types/.keep
create app/graphql/ponica_schema.rb
create app/graphql/types/query_type.rb
add_root_type query
create app/graphql/mutations
create app/graphql/mutations/.keep
create app/graphql/types/mutation_type.rb
add_root_type mutation
create app/controllers/graphql_controller.rb
route post "/graphql", to: "graphql#execute"
gemfile graphiql-rails
route graphiql-rails
Gemfile に graphiql-rails が追加されたり routes.rb に graphql の endpoint や graphiql-rails の mount が追加されたりしている。
graphiql-rails の認証周りの設定
graphiql-rails から graphql の API を呼ぶ際にヘッダーにデフォで○○設定したい〜とかいう場合は下記のように initializer で設定する
GraphiQL::Rails.config.headers["X-Hoge"] = -> (context) { "Hoge" }
Github みたいに v4 にしてみる
まずは routes.rb 編集
scope 'v4', module: 'v4' do
post "/graphql", to: "graphql#execute"
end
controller を移動
mv app/controllers/graphql_controller.rb app/controllers/v4/
- class GraphqlController < ApplicationController
+ class V4::GraphqlController < ApplicationController
...
graphiql で使う path も変更
- mount GraphiQL::Rails::Engine, at: "/graphiql", graphql_path: "/graphql"
+ mount GraphiQL::Rails::Engine, at: "/graphiql", graphql_path: "/v4/graphql"
この状態で rails s で server を立ち上げる。
http://localhost:3000/graphiql にアクセスすると GraphiQL の画面が表示される
# request
query {
testField
}
# response
{
"data": {
"testField": "Hello World!"
}
}
みんな大好き Hello World が出来た
認証周り
ログイン認証とかは今あるものをそのまま使えば ok で GraphQL の処理の中で current_user を使えるように context に格納しておく
context = {
- # Query context goes here, for example:
- # current_user: current_user,
+ current_user: current_user
}
schema 設定
初期設定 の時に app/graphql 以下に諸々のファイルが生成されるのでそこら辺をいじる
app/graphql/ponica_schema.rb というのができているので ponica 部分をいい感じの名前に変える
mv app/graphql/ponica_schema.rb app/graphql/minne_schema.rb とか、で下記のように編集する
-PonicaSchema = GraphQL::Schema.define do
+ MinneSchema = GraphQL::Schema.define do
- result = PonicaSchema.execute(query, variables: variables, context: context, operation_name: operation_name)
+ result = MinneSchema.execute(query, variables: variables, context: context, operation_name: operation_name)
type を定義していく
順番的には type を定義した後に graphql-batch の処理を実装した方がわかりやすいと思いますが長くなるのでまとめてます。
MinneSchema = GraphQL::Schema.define do
+ use GraphQL::Batch
Types::UserType = GraphQL::ObjectType.define do
name "User"
global_id_field :id
field :id, !types.ID
field :name, !types.String
connection :products, Types::ProductType.connection_type
end
Types::ProductType = GraphQL::ObjectType.define do
name "Product"
global_id_field :id
field :id, !types.ID
field :name, !types.String
field :user, !Types::UserType do
resolve -> (obj, args, context) { RecordLoader.for(User).load(obj.user_id) }
end
field :loved, !types.Boolean do
resolve ->(obj, args, ctx) {
LovedLoader.for(ctx[:current_user]).load(obj.id)
}
end
end
class RecordLoader < GraphQL::Batch::Loader
def initialize(model)
@model = model
end
def perform(ids)
@model.where(id: ids).each { |record| fulfill(record.id, record) }
ids.each { |id| fulfill(id, nil) unless fulfilled?(id) }
end
end
class LovedLoader < GraphQL::Batch::Loader
def initialize(user)
@user = user
end
def perform(ids)
love_product_ids = @user.loves.where(product_id: ids).pluck(:product_id)
ids.each { |id| fulfill(id, love_product_ids.include?(id)) }
end
end
コメントで書いた通り graphql-ruby はページネーション用の処理も簡単に実装してくれる。
また graphql-batch を使えば N+1 の問題も解消可能
loved に関しても 1 レコード毎いいねをしたかを判定するのではなくまとめて判定している
query を定義する
Types::QueryType = GraphQL::ObjectType.define do
name "Query"
- # Add root-level fields here.
- # They will be entry points for queries on your schema.
- # TODO: remove me
- field :testField, types.String do
- description "An example field added by the generator"
+ field :user do
+ type Types::UserType
+
+ argument :id, !types.String
+
+ description "Find a User By account"
+ resolve ->(obj, args, ctx) {
+ User.find(args[:id])
+ }
+ end
+
+ connection :products, Types::ProductType.connection_type do
+ description "all Product"
resolve ->(obj, args, ctx) {
- "Hello World!"
+ Product.all # 雑に all とかしてるので実際はごにょごにょする必要があります
}
end
end
実際に使ってみる
直接 id を指定してもいいが variables を使うこともできる
__typename とかで type を見たり__hogeで色々見れる
# users#show っぽいやつ
# request
query GetUser($id: String!) {
user(id: $id) {
__typename
name
products(first: 2, after: "Mg==") { # first が件数 after がカーソル
edges {
node {
id
name
}
}
pageInfo {
startCursor
endCursor
}
}
}
}
# variables
{
"id": "2"
}
# response
{
"data": {
"user": {
"__typename": "User",
"name": "test user",
"products": {
"edges": [
{
"node": {
"id": "25",
"name": "hoge"
}
},
{
"node": {
"id": "24",
"name": "fuga"
}
}
],
"pageInfo": {
"startCursor": "MQ==",
"endCursor": "Mg=="
}
}
}
}
}
# products#index っぽいやつ
# request
query {
products(first: 2) {
edges {
node {
id
name
loved
user {
name
}
}
}
pageInfo {
startCursor
endCursor
}
}
}
# response
{
"data": {
"products": {
"edges": [
{
"node": {
"id": "1",
"name": "hoge",
"loved": true,
"user": {
"name": "hoge user"
}
}
},
{
"node": {
"id": "2",
"name": "fuga",
"loved": false,
"user": {
"name": "fuga user"
}
}
}
],
"pageInfo": {
"startCursor": "MQ==",
"endCursor": "Mg=="
}
}
}
}
わかりやすいようにバラバラで呼んでますがクエリを変更すれば上記の 2 つを一回のリクエストで取得することが可能です。
Rails のログを見ればわかりますが N+1 も発生していません。
最後に
まだ query をどう定義するのがベストなのか? + cache 周りの検証が出来ていないのでその辺り頑張る + mutation メリットが見出せてないのでそこら辺知見がある人と話してみたい。 REST に引っ張られがちになるので考え方を変えるのが大変だったりする。
個人的には GraphQL すごくいいなと感じていて、graphql-ruby, graphql-batch, graphiql-rails もすごく良く出来ているという感想なんで導入進めるぞという気持ち
下記のページを参考にしました。(英語のページを見ながら実装したんでなんか間違ってたら指摘を〜
GraphQL | A query language for your API
GraphQL - Welcome
書いてて疲れたんで最後雑になっちゃったな