🎡

GraphQL × Railsで超簡単なCRUDを実装

2023/09/11に公開

とりあえず雑に動かしてみました。
コードの全体像は、以下より確認できます。

https://github.com/nyshk97/graphql-sample

ざっくりポイント

  • queryでデータの取得をする
  • mutationでデータの作成、更新、削除をする
  • RESTと違って、エンドポイントは単一
    • hogehoge.com/graphql
    • RESTでcontrollerに書いてたような処理は、app/graphql/queries/hoge.rbとかapp/graphql/mutations/fuga.rbとかに書く
    • クエリの内容と、query_type.rbmutation_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)

参考

https://zenn.dev/lilac/books/37bdf5d90b5f9b/viewer/e22e29

Discussion