RailsでGraphQLサーバー構築体験🤔💭

一応 `RailsでWeb APIを作成する方法とメリット🤔💭` の記事の続きですが前回の記事を読んでいなくても構築可能です。

https://qiita.com/digitter/items/53f83ce50036b2773d55

RailsでGraphQLサーバーを立ち上げて基本的なQuery、Mutation機能を実装してみました!

# 前提

- GraphQLをある程度知っていないとコードの理解は困難です。m(_ _)m

- GraphQLが何なのかは説明しません。ある程度、GraphQLがどういうものかを知っている前提で進めます。

>ぜひ[公式サイト](https://graphql.org/)などで仕様をご参照ください。

- HTTP リクエストメソッドについては説明しません。

>MDNをぜひ (https://developer.mozilla.org/ja/docs/Web/HTTP/Methods)

- HTTPリクエストボディの説明はしていません。

>[「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典](https://wa3.i-3-i.info/word1845.html#:~:text=HTTP%E3%83%AA%E3%82%AF%E3%82%A8%E3%82%B9%E3%83%88%E3%83%A1%E3%83%83%E3%82%BB%E3%83%BC%E3%82%B8%E3%83%9C%E3%83%87%E3%82%A3%E3%81%AF,%E3%82%82%E6%9B%B8%E3%81%8B%E3%82%8C%E3%81%BE%E3%81%9B%E3%82%93%E3%80%82)
>このサイト初心者向けに用語説明しててわかりやすいです。

- DBにbooksテーブルがある状態で始めます。

>先ほどのstringのtitle, textのbodyカラムを持ったテーブルです。

# RailsをGraphQLサーバーにするには?

今回は、まずRailsをAPIモードを用意します。（[前回の記事](https://qiita.com/digitter/items/53f83ce50036b2773d55)で用意しています。）

[完成版ソースコード](https://github.com/digitter/rails_api_sample)もあります。

その後、Gem `graphql-ruby` をGemfileに記載してbundle installしてGraphQLサーバー化していきます。

APIサーバーは既に用意しているのでGemをインストールするところから進めます。

# RailsでGraphQLサーバーを作成していきます

Gemfileで以下を追記し、installします。

```Gemfile
gem 'graphql'
```

```
$ bundle install
```

GraphQLのテンプレートを生成します。以下のコマンドを実行するとGraphQL関連のファイル群が生成されます。

```
$ rails g graphql:install

Running via Spring preloader in process 35807
      create  app/graphql/types
      create  app/graphql/types/.keep
      create  app/graphql/graphql_server_sample_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"
Skipped graphiql, as this rails project is API only
  You may wish to use GraphiQL.app for development: https://github.com/skevy/graphiql-app
      create  app/graphql/types/node_type.rb
      insert  app/graphql/types/query_type.rb
      create  app/graphql/types/base_connection.rb
      create  app/graphql/types/base_edge.rb
      insert  app/graphql/types/base_object.rb
      insert  app/graphql/types/base_object.rb
      insert  app/graphql/types/base_union.rb
      insert  app/graphql/types/base_union.rb
      insert  app/graphql/types/base_interface.rb
      insert  app/graphql/types/base_interface.rb
      insert  app/graphql/graphql_server_sample_schema.rb
```


## Object typeを定義

Object typeはどういう形のデータをreturnするかを決めるものです。

```
$ rails g graphql:object Book
Running via Spring preloader in process 97118
      create  app/graphql/types/book_type.rb
```

`app/graphql/types/book_type.rb` というファイルが作成されます。

ファイルを開くと以下の様にfieldが定義されているはずです。

＊Bookモデルを作成していると、自動でfieldが定義されます。


```app/graphql/types/book_type.rb
module Types
  class BookType < Types::BaseObject
    field :id, ID, null: false
    field :title, String, null: true
    field :body, String, null: true
    field :created_at, GraphQL::Types::ISO8601DateTime, null: false
    field :updated_at, GraphQL::Types::ISO8601DateTime, null: false
  end
end
```

## Queryを定義

```app/graphql/types/query_type.rb
module Types
  class QueryType < Types::BaseObject
    # bookのデータがあれば[本の情報、本の情報, ...]という形式でデータを返します。
    # 角括弧内でnull: trueしているので空の[ ]を返してもOKにしてます。
    # null: falsede必ずデータは何かしら返さなきゃいけないようにしています。（なかったらエラー）
    field(:books, [BookType, null: true], null: false) do
      description 'fetch all books'
    end

    # bookのデータを一つ返すようにしています。
    # null: falseなので必ずデータ返さなきゃいけないようにしてます。（なかったらエラー）
    field(:book, BookType, null: false) do
      description 'find a book by id'
      argument :id, ID, required: true
    end

    def books
      Book.all
    end

    def book(id:)
      Book.find(id)
    end
  end
end
```

これで実際にデータを取得する準備が整いました。実際にAPIの開発ツールを使って検証してみましょう。

Gem `graphiql-rails`を使って`Graphiql`などでも確認できますが個人的にPostmanが好きなので[Postman](https://www.postman.com/downloads/)で確認します。

PostmanはAPI開発の手助けをしてくれるツールです。

URIを指定して通信リクエストを実行してどういうデータが返ってくるかなぁ〜とか色々確認ができるわけです。

早速、GraphQLのQueryを指定してデータが取得できるかどうがみてみましょう。

## Queryを実行

- `rails s` でサーバーを起動します。

- HTTP verbを `POST`にします。

- GraphQLのラジをボタンを選択します。

- HTTP リクエストメッセージボディクエリを記述します。

![スクリーンショット 2021-12-29 20.42.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/240416/c391cd43-765e-fd00-9521-7b9b90a88c59.png)

別のクエリでGraphQLにデータ要求してみましょう。

**id 1のbookのtitleだけを取得します。**

![スクリーンショット 2021-12-29 20.44.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/240416/8dba2d5f-aa0c-9fbb-cb7a-c88a33c3ae94.png)

**複数のクエリを一度に実行してみましょう。**
**すべてのbookのidと、id 1のbookの全ての情報を取得してみます。**

![スクリーンショット 2021-12-30 17.15.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/240416/faac628a-eb91-0f17-1257-48ff70367a67.png)

このように、**クエリがどんなデータを返却してくれるのかが直感的にわかりますね！**

さらに、**必要なデータだけに絞って取得することもできたので過剰に無駄なデータ取得することも無くなります。**


## Mutationを定義

先ほどはデータの取得はQueryで行いましたが、データの作成、更新、削除はMutationというもので行います。

`データを作成して、取得する`という機能を制作していきます。

今回はbookのcreate(作成用)のmutationを定義していきます。

以下を実行して、`create_book`というmutationを実装していきます。

```
$ rails g graphql:mutation create_book

Running via Spring preloader in process 61114
       exist  app/graphql/mutations
   identical  app/graphql/mutations/.keep
   identical  app/graphql/mutations/base_mutation.rb
   identical  app/graphql/types/mutation_type.rb
add_root_type  mutation
      create  app/graphql/mutations/create_book.rb
        gsub  app/graphql/types/mutation_type.rb
```

`app/graphql/mutations/create_book.rb` というファイルが作成されているのがわかります。

以下の様に、変更してみます。

```app/graphql/mutations/create_book.rb
module Mutations
  class CreateBook < BaseMutation
    # 返却されるデータの形を定義しています。
    field :book, Types::BookType, null: false

    # GraphQL Variableの情報を定義してます。
    # これがないと作成時の情報も利用できないです。
    argument :body, String, required: true
    argument :title, String, required: true

    # Resolverで返却したいデータをreturnします。
    def resolve(**attributes)
      # attributesには次のHashが渡ってきます。(attributesという名前以外でもOKです。)
      # {:body=>"GraphQL理解しないとGraphql-rubyわからんね", :title=>"GraphQLな奴"}
      book = Book.create!(attributes)
      { book: book } # このデータが返却されます。(Rubyはreturn省略できます。)
    end
  end
end
```

`field :book, Types::BookType, null: false`と定義していますがこれは最終的に
以下の様なデータが返ってきます。

```json
{
    "data": {
        "createBook": {
            book: {
              "title": "GraphQLな奴",
              "body": "GraphQL理解しないとGraphql-rubyわからんね"
            }
        }
    }
}
```

## Mutationを実行

![スクリーンショット 2021-12-30 1.34.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/240416/b3cfb7cd-1f40-8960-b374-d373d881f734.png)

ところで、以下の様にデータを受け取りたい場合はどうするでしょうか？

```json
{
    "data": {
        "createBook": {
            "title": "GraphQLな奴",
            "body": "GraphQL理解しないとGraphql-rubyわからんね"
        }
    }
}
```

返却されるデータの形はfieldで定義されていました。

なので返却されるfieldを変えてしまえばいいわけです。

```app/graphql/mutations/create_book.rb
module Mutations
  class CreateBook < BaseMutation
    # 既にapp/graphql/types/book_type.rbで定義しているfield群を利用します。
    # その場合はfieldではなくてtypeで呼び出します。
    type Types::BookType

    argument :body, String, required: true
    argument :title, String, required: true

    def resolve(**attributes)
      Book.create!(attributes)
    end
  end
end

```

実際にクエリするとこんな感じ

![スクリーンショット 2021-12-30 1.46.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/240416/5e647500-eb1f-a499-4a0c-1fa0af791480.png)

コードの流れをざっくり説明していきます。

- Mutationが `GraphQL::Schema::RelayClassicMutation` を継承してると、デフォルトで**Input objectが自動で定義される**ようになっています。クエリにもinput型の定義が必要になります。(GraphQL Rubyの仕様です。)

- input型を定義して、それに相当するデータもGraphQL Variableに指定してクエリを実行します。

> input型はストロングパラメーターの型があるパージョンと言えばRails触ってる人はわかりやすいかな...？ 🤔💭

- fieldで定義した通りのデータがResolverから（resolveメソッドから）返ってきます。

> 最終的にデータを返すメソッドがresolveです。Resolverとか呼ばれてる奴です。

- 今回はbookのtitleとbodyだけを取得するようにしているので、title, bodyの情報が返ってきました。

> クエリ実行時のfieldには、title, bodyだけ指定しているのでそれ以外のデータは返ってきませんね。


![スクリーンショット 2021-12-30 2.24.49 2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/240416/33720ee7-5a08-bcaa-51f4-65b30370f5ca.png)

結構色々定義して面倒だと思われた方！！！

仮にですが、input型の指定無しでクエリ実行したい！ という方は `GraphQL::Schema::RelayClassicMutation` を継承させないようにすればOKです。

```app/graphql/mutations/create_book.rb
module Mutations
  # 継承元を変更します。
  class CreateBook < GraphQL::Schema::Mutation
    field :book, Types::BookType, null: false

    argument :title, String, required: true
    argument :body, String, required: true

    def resolve(**attributes)
      book = Book.create!(attributes)
      { book: book }
    end
  end
end
```

この状態でクエリを実行すると以下のように**input型の指定無し、GraphQL Variableの指定無し**でクエリの実行ができました。

![スクリーンショット 2021-12-30 16.42.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/240416/3bc0c058-2bd3-8eb0-74a4-8aa1ca35ddff.png)

**しかし、input型がある方がストロングパラメーターみたいに使いまわせたり、型の面で便利なので、本当はinput型を使っていた方がいいと思います。**

# GraphQLの何がよいの？🤔💭

これまでQuery、Mutationでデータの取得、作成を行ってきました。

- **GraphQLのいいところはやはり直感的にどういうデータが返ってくるかが分かりやすい。**
  - データの型も指定できるのでどういう型のデータが来るかも分かりやすいです。

- **本当に必要な必要最小限のデータだけを柔軟に取得できる**
  - クエリ実行時にbookのidだけ必要であればクエリのfieldにidだけ書けば良いのです。

- **一つのエンドポイントで、かつ一度のリクエストで色々なデータにアクセス可能**
  - この記事では全データの取得、データの一つを取得、データの作成をたった一つのエンドポイントで成し遂げました。
  - http://localhost:3000/graphql だけでやりとげました。たくさんRouting定義しなくてもいいわけです。
  - クエリは一度に複数指定も可能でした。

他にも色々メリットがありますがそんなAPIサーバーが作成をしていきました。

# フロントエンドからどうやってクエリ実行すれば良いの？

Apollo ClientとかGraphQLに特化したライブラリを使うと良いです。

ライブラリを使わなくともPOSTメソッドなHTTPリクエストのボディにクエリを持たせているだけですのでFetch APIやAxiosを使ってリクエストボディにクエリを含ませれば実行できます。

**要は、HTTPリクエストができればクエリは実行できるわけです。**(なんならHTTPじゃなくてもできるらしい)

Apollo Clientもまとめてみたいのですが記事の範疇を超えるので割愛します。m(_ _)m


素敵なAPI、GraphQLライフを٩(ˊωˋ*)و✧*