🎡
GraphQL × Railsで超簡単なCRUDを実装
とりあえず雑に動かしてみました。
コードの全体像は、以下より確認できます。
ざっくりポイント
- queryでデータの取得をする
- mutationでデータの作成、更新、削除をする
- RESTと違って、エンドポイントは単一
hogehoge.com/graphql
- RESTでcontrollerに書いてたような処理は、
app/graphql/queries/hoge.rb
とかapp/graphql/mutations/fuga.rb
とかに書く - クエリの内容と、
query_type.rb
やmutation_type.rb
等に書かれたルーティングっぽいもので、どこで処理されるかが振り分けられる
Railsアプリを作成
$ ruby -v
ruby 3.1.2p20 (2022-04-12 revision 4491bb740a) [arm64-darwin21]
$ rails -v
Rails 7.0.4
$ rails new graphql-sample --api
$ cd graphql-sample
$ rails s
$ rails g model Song title:string
$ rails db:migrate
db/seeds.rb
3.times do |i|
Song.create!(title: "曲 #{i + 1}")
end
$ rails db:seed
GraphQLのセットアップ
Gemfile
+ gem 'graphql'
shell
$ bundle install
$ rails g graphql:install
graphql関連のファイルがいろいろ生成され、routes.rb
に以下のルーティングが追加される。
config/routes.rb
Rails.application.routes.draw do
+ post "/graphql", to: "graphql#execute"
en
queryでデータを取得
Songオブジェクトの型定義ファイルを作成
$ rails g graphql:object Song
以下が生成される
app/graphql/types/song_type.rb
# frozen_string_literal: true
module Types
class SongType < Types::BaseObject
field :id, ID, null: false
field :title, String
field :created_at, GraphQL::Types::ISO8601DateTime, null: false
field :updated_at, GraphQL::Types::ISO8601DateTime, null: false
end
end
全ての曲を取得(Select)
$ mkdir app/graphql/queries
$ touch app/graphql/queries/base_query.rb
$ touch app/graphql/queries/songs.rb
app/graphql/queries/base_query.rb
module Queries
class BaseQuery < GraphQL::Schema::Resolver
end
end
app/graphql/queries/songs.rb
module Queries
class Songs < Queries::BaseQuery
type [Types::SongType], null: false
def resolve
::Song.all.order(:id)
end
end
end
app/graphql/types/query_type.rb
module Types
class QueryType < Types::BaseObject
...
+ field :songs, resolver: Queries::Songs
↑songsクエリが実行されると、Queries::Songsクラスのresolveメソッドが呼ばれるように設定
end
end
APIを叩いて動作確認
$ curl -X POST \
-H "Content-Type: application/json" \
--data '{ "query": "{ songs { id title } }" }' \
http://localhost:3000/graphql
{"data":{"songs":[{"id":"1","title":"曲 1"},{"id":"2","title":"曲 2"},{"id":"3","title":"曲 3"}]}}
idを元に特定の曲を取得(Select)
$ touch app/graphql/queries/song.rb
app/graphql/queries/song.rb
module Queries
class Song < Queries::BaseQuery
argument :id, ID, required: true
type Types::SongType, null: false
def resolve(id:)
::Song.find(id)
end
end
end
app/graphql/types/query_type.rb
module Types
class QueryType < Types::BaseObject
...
field :songs, resolver: Queries::Songs
+ field :song, resolver: Queries::Song
end
end
APIを叩いて動作確認
$ curl -X POST -H "Content-Type: application/json" \
--data '{"query": "{ song(id: 2) { id title } }"}' \
http://localhost:3000/graphql
{"data":{"song":{"id":"2","title":"曲 2"}}}
mutationでデータを作成、更新、削除
引数の型定義ファイルを作成
$ touch app/graphql/types/song_input_type.rb
app/graphql/song_input.rb
class Types::SongInputType < Types::BaseInputObject
argument :title, String, required: true
end
データの作成(Create)
$ touch app/graphql/mutations/create_song.rb
app/graphql/mutations/create_song.rb
module Mutations
class CreateSong < Mutations::BaseMutation
argument :params, Types::SongInputType, required: true
field :song, Types::SongType, null: false
def resolve(params:)
song = Song.create!(params.to_h)
{ song: song }
rescue => e
GraphQL::ExecutionError.new(e.message)
end
end
end
app/graphql/types/mutation_type.rb
module Types
class MutationType < Types::BaseObject
...
+ field :create_song, mutation: Mutations::CreateSong
end
end
APIを叩いて動作確認
$ curl -X POST -H "Content-Type: application/json" \
--data '{"query": "mutation { createSong(input: {params: {title: \"New Song\"}}) { song { id title } } }"}' \
http://localhost:3000/graphql
{"data":{"createSong":{"song":{"id":"4","title":"New Song"}}}}
$ rails c
irb(main):001:0> Song.last
=>
#<Song:0x000000010917ff98
id: 4,
title: "New Song",
created_at: Mon, 11 Sep 2023 09:31:05.324591000 UTC +00:00,
updated_at: Mon, 11 Sep 2023 09:31:05.324591000 UTC +00:00>
データの更新(Update)
$ touch app/graphql/mutations/update_song.rb
app/graphql/mutations/update_song.rb
module Mutations
class UpdateSong < Mutations::BaseMutation
argument :id, ID, required: true
argument :params, Types::SongInputType, required: true
field :song, Types::SongType, null: false
def resolve(id:, params:)
song = Song.find(id)
song.update!(params.to_h)
{ song: song }
rescue => e
GraphQL::ExecutionError.new(e.message)
end
end
end
app/graphql/types/mutation_type.rb
module Types
class MutationType < Types::BaseObject
...
field :create_song, mutation: Mutations::CreateSong
+ field :update_song, mutation: Mutations::UpdateSong
end
end
APIを叩いて動作確認
$ curl -X POST -H "Content-Type: application/json" \
--data '{"query": "mutation { updateSong(input: {id: 2, params: {title: \"updated song\"}}) { song { id title } } }"}' \
http://localhost:3000/graphql
{"data":{"updateSong":{"song":{"id":"2","title":"updated song"}}}}
$ rails c
irb(main):001:0> Song.find(2)
=>
#<Song:0x000000011343edd8
id: 2,
title: "updated song",
created_at: Mon, 11 Sep 2023 09:03:23.951399000 UTC +00:00,
updated_at: Mon, 11 Sep 2023 09:38:37.887105000 UTC +00:00>
irb(main):002:0>
データの削除(Delete)
$ touch app/graphql/mutations/delete_song.rb
app/graphql/mutations/delete_song.rb
module Mutations
class DeleteSong < Mutations::BaseMutation
argument :id, ID, required: true
field :id, ID, null: false
def resolve(id:)
Song.find(id).delete
{ id: id }
rescue => e
GraphQL::ExecutionError.new(e.message)
end
end
end
app/graphql/types/mutation_type.rb
module Types
class MutationType < Types::BaseObject
...
field :create_song, mutation: Mutations::CreateSong
field :update_song, mutation: Mutations::UpdateSong
+ field :delete_song, mutation: Mutations::DeleteSong
end
end
APIを叩いて動作確認
$ curl -X POST -H "Content-Type: application/json" \
--data '{"query": "mutation { deleteSong(input: {id: 2}) { id } }"}' \
http://localhost:3000/graphql
{"data":{"deleteSong":{"id":"2"}}}
$ rails c
irb(main):001:0> Song.find(2)
=>
Couldn't find Song with 'id'=2 (ActiveRecord::RecordNotFound)
参考
Discussion