Rails + GraphQLでAPI作成
各バージョン
ruby: 2.7.1
rails: 6.0.3.4
graphql-ruby: 1.11.6
GraphQL Ruby
RailsでGraphQLを扱う場合↑のgemを使ってAPIを実装していきます。
graphiql-rails
合わせて graphiql-rails gemを入れておくとブラウザ上で実装したGraphQLの
確認ができるIDEが使えるようになります :sparkles:
※ graphql-ruby
のinstall時に graphiql-rails
のgemをGemfileに追加してくれます
イメージ画像
環境構築
gem 'graphql'
gem 'graphiql-rails' # 今回は先に入れました
gemがインストールされたら rails generate graphql:install
コマンドを実行し各ファイルを生成します。
生成されたファイルは以下の通り↓
$ rails generate graphql:install
create app/graphql/types
create app/graphql/types/.keep
create app/graphql/app_schema.rb
create app/graphql/types/base_object.rb
create app/graphql/types/base_argument.rb
create app/graphql/types/base_field.rb
create app/graphql/types/base_enum.rb
create app/graphql/types/base_input_object.rb
create app/graphql/types/base_interface.rb
create app/graphql/types/base_scalar.rb
create app/graphql/types/base_union.rb
create app/graphql/types/query_type.rb
add_root_type query
create app/graphql/mutations
create app/graphql/mutations/.keep
create app/graphql/mutations/base_mutation.rb
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
この時点での routes.rb
は以下のようになっています。
Rails.application.routes.draw do
# GraphQL
if Rails.env.development?
mount GraphiQL::Rails::Engine, at: '/graphiql', graphql_path: '/graphql'
end
post '/graphql', to: 'graphql#execute'
end
実装
Query 作成
まずは各テーブルに対応するTypeを定義しないといけないので、
例として以下の users
テーブルに対応する user_type
を作成してみたいと思います。
create_table :users do |t|
t.string :name, null: false
t.string :email
t.timestamps
end
以下コマンドを実行すると user_type
が作成されます。
(指定する型は ID
がGraphQLで定義されているid用の型です(実態はString)
また語尾に !
が付いているものはnullを許容しない型となり、!
が付いてないものはnull許容になります。)
$ bundle exec rails g graphql:object User id:ID! name:String! email:String
【補足】既にDBにテーブルが存在している場合はよろしくやってくれるっぽいので
$ bundle exec rails g graphql:object User
↑これでも大丈夫でした :sparkles:
生成されたファイル graphql/type/user_type.rb
は以下のようになっていました。
module Types
class UserType < Types::BaseObject
field :id, ID, null: false
field :name, String, null: false
field :email, String, null: true
field :created_at, GraphQL::Types::ISO8601DateTime, null: false
field :updated_at, GraphQL::Types::ISO8601DateTime, null: false
end
end
既に生成されている graphql/type/query_type.rb
に以下を追加します。
field :users, [Types::UserType], null: false
def users
User.all
end
http://localhost:3000/graphiql
上で以下クエリを投げるとレスポンスが返ってくるかと思います。
{
users {
id
name
email
}
}
Mutationsの作成
次にユーザーを作成するMutations CreateUser
を作成してみたいと思います。
$ bundle exec rails g graphql:mutation CreateUser
graphql/mutations/create_user.rb
が作成されるので、以下の様に修正します。
module Mutations
class CreateUser < BaseMutation
field :user, Types::UserType, null: true
argument :name, String, required: true
argument :email, String, required: false
def resolve(**args)
user = User.create!(args)
{
user: user
}
end
end
end
既に生成されている graphql/types/mutation_type.rb
に以下を追記します。
module Types
class MutationType < Types::BaseObject
field :createUser, mutation: Mutations::CreateUser # 追記
end
end
http://localhost:3000/graphiql
上で以下を実行するとUserが作成されます。
mutation {
createUser(
input:{
name: "user"
email: "user@email.com"
}
){
user {
id
name
email
}
}
}
Association
- 1:1の関連テーブルの場合
例として Post
が Label
と1:1で関連付されている場合
module Types
class LabelType < Types::BaseObject
field :id, ID, null: false
field :name, String, null: false
...
end
end
module Types
class PostType < Types::BaseObject
field :label, LabelType, null: true
end
end
↑の様に label
を LabelType
として定義できます。
この場合の Query のイメージとしては
{
posts {
id
label {
id
name
}
}
}
上記の様に label
を LabelType
として必要な値をQueryできます。
- 1:Nの関連テーブルの場合
例として User
が Post
と1:Nの場合
module Types
class PostType < Types::BaseObject
field :id, ID, null: false
field :label, LabelType, null: true
end
end
module Types
class UserType < Types::BaseObject
field :posts, [PostType], null: false
end
end
上記の様に posts
を [PostType]
として定義でき、Queryとしては
{
user(id: 1234) {
id
posts {
id
label {
id
name
}
}
}
}
↑の様に呼び出す事ができます。
graphql-batch
↑の説明の様に 1:1や1:Nの関連テーブルのデータも取ってくる事ができますが
今のままだとDBへの問い合わせが大量に発生してしまう場合があります。
User
が Post
と1:Nの場合の例で Post
が100件ある場合、それぞれ100回問い合わせが発生してしまいます。
そこで解決方法の一つである複数問い合わせをまとめやってくれる graphql-batch を導入してみます。
gem 'graphql-batch'
Gemをインストールしたら、loader
を作成していきます。
loader
は「複数問い合わせをまとめる」部分の実装になります。
module Loaders
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
end
これを先程の Post
が Label
と1:1で関連付されている場合に適用すると
module Types
class PostType < Types::BaseObject
field :label, LabelType, null: true
def label
Loaders::RecordLoader.for(Label).load(object.label_id)
end
end
end
こんな感じで書けます。
User
が Post
と1:Nの場合には別途loaderを作成します。
module Loaders
class AssociationLoader < GraphQL::Batch::Loader
def self.validate(model, association_name)
new(model, association_name)
nil
end
def initialize(model, association_name)
@model = model
@association_name = association_name
validate
end
def load(record)
raise TypeError, "#{@model} loader can't load association for #{record.class}" unless record.is_a?(@model)
return Promise.resolve(read_association(record)) if association_loaded?(record)
super
end
# We want to load the associations on all records, even if they have the same id
def cache_key(record)
record.object_id
end
def perform(records)
preload_association(records)
records.each { |record| fulfill(record, read_association(record)) }
end
private
def validate
unless @model.reflect_on_association(@association_name)
raise ArgumentError, "No association #{@association_name} on #{@model}"
end
end
def preload_association(records)
::ActiveRecord::Associations::Preloader.new.preload(records, @association_name)
end
def read_association(record)
record.public_send(@association_name)
end
def association_loaded?(record)
record.association(@association_name).loaded?
end
end
end
※ loaderはgraphql-batchのリポジトリにサンプルがあるので、そちらを参考にして実装すると良さそうです
以下の様に書くと、まとめて問い合わせしてくれるようになります。
module Types
class UserType < Types::BaseObject
field :posts, [PostType], null: false
def posts
Loaders::AssociationLoader.for(User, :posts).load(object)
end
end
end
スキーマファイルからドキュメント生成
最後に定義したスキーマファイルから良い感じのドキュメントを自動で生成するようにしてみたいと思います。
routes.rb
にマウントできてデプロイ毎に自動でgraphdocが更新される
便利なgemを探していたらgraphdoc-rubyというgemがあったので試してみます。
Gemfile
に以下を追加
gem 'graphdoc-ruby'
また、npmパッケージの@2fd/graphdocも必要なので
予めDockerイメージ内でインストールしておきます。(Docker使用してない場合はローカル環境にインストールすれば良いかと思います)
例)
RUN set -ex \
&& wget -qO- https://deb.nodesource.com/setup_10.x | bash - \
&& apt-get update \
&& apt-get install -y \
...
--no-install-recommends \
&& rm -rf /var/lib/apt/lists/* \
&& npm install -g yarn \
&& npm install -g @2fd/graphdoc # インストールしとく
config/routes.rb
に以下を追記
Rails.application.routes.draw do
mount GraphdocRuby::Application, at: 'graphdoc'
end
※ エンドポイントを変更している場合、config/initializers/graphdoc.rb
を修正する
例)
GraphdocRuby.configure do |config|
config.endpoint = 'http://0.0.0.0:3000/api/v1/graphql'
end
Railsを再起動して、http://localhost:3000/graphdoc でドキュメントが生成されればOKです :sparkles:
バッドノウハウ
-
http://localhost:3000/graphiql
アクセス時に以下エラーが発生する場合Sprockets::Rails::Helper::AssetNotPrecompiled in GraphiQL::Rails::Editors#show
-
解決方法1
app/assets/config/manifest.js
に以下を追加する//= link graphiql/rails/application.css //= link graphiql/rails/application.js
AssetNotPrecompiled error with Sprockets 4.0 · Issue #75 · rmosolgo/graphiql-rails
-> ただこれだとProduction時にSprockets::FileNotFound: couldn't find file 'graphiql/rails/application.css'
エラーが出て使えない... -
解決方法2 (うまくいった方法)
gem 'sprocket'のバージョン3.7.2に下げる
gem 'sprockets', '~> 3.7.2' [#1098: slowdev/knowledge/ios/FirebaseをCarthageで追加する](/posts/1098)
↑を追加し、
bundle update
Rails6のAPIモードでGraphQLを使う方法(エラー対策も含む) - Qiita
-
-
graphiqlの画面に
TypeError: Cannot read property 'types' of undefined
が表示される
-> 手元の環境だとRails再起動で治りました -
graphiqlの画面に
SyntaxError: Unexpected token < in JSON at position 0
が表示される
-> エラーが発生してる可能性がるのでログを見て修正する
参考になったURL
- 【Rails】graphql-rubyでAPIを作成 - Qiita
- REST APIが主流のプロジェクトの中でGraphQLを導入してみた話(サーバーサイド編) - Sansan Builders Blog
- 「GraphQL」徹底入門 ─ RESTとの比較、API・フロント双方の実装から学ぶ - エンジニアHub|若手Webエンジニアのキャリアを考える!
- GraphQLを使ったAPI仕様中心開発の導入とその効果の紹介 - Kaizen Platform 開発者ブログ
- 雑に始める GraphQL Ruby【class-based API】 - Qiita
- hawksnowlog: Ruby (Sinatra) で GraphQL 入門
- 既存のRailsプロジェクトにGraphQL APIを追加してみた - Qiita
- Ruby on Rails で sprockets から Webpacker へ移行し、移行できないものは共存させる方法 - Qiita
- Reading: 初めてGraphQL - 型の基礎|tkhm|note
- https://github.com/loopstudio/rails-graphql-api-boilerplate
- https://github.com/rmosolgo/graphql-ruby-demo
Discussion