Rails6.1で追加されたdelegated typeとGraphQLのUnion typesは相性よさそうなので試したみた

VISITS Technologiesでマネージャー兼バックエンドエンジニアをしている@ham0215です。

今年のアドベントカレンダー3つ目の記事です。
直前までどのような内容の記事を書こうか迷っていたのですが、直前で @woods0918 さんが[GraphQLの記事](https://qiita.com/woods0918/items/2d7b2421d8a8f1b6d724)を投稿したので、私も流れに乗ってGraphQLの記事にしてみました。

Rails6.1で追加されたdelegated typeとGraphQLのUnion typesは相性よさそうだなと思ったので、実際に触ってみて使い心地を確かめてみました。

# GraphQLのUnion typesとは
GraphQLにはUnion typesというタイプがあります。
公式ホームページの下記に記載されています。
https://graphql.org/learn/schema/#union-types

Union typesを使うと複数のタイプを組み合わせたタイプを表現することができます。
この記事では、公式ホームページのサンプルをそのまま使わせていただきます。

```graphql
{
  search(text: "an") {
    __typename
    ... on Human {
      name
      height
    }
    ... on Droid {
      name
      primaryFunction
    }
    ... on Starship {
      name
      length
    }
  }
}
```

`search`クエリーを実行すると`Human`か`Droid`か`Starship`という３種類のタイプが混在したリストが返却されます。
`name`属性は全タイプ共通ですが、`Human`の場合は`height`、`Droid`の場合は`primaryFunction`、Starshipの場合は`length`という独自の属性を持っています。

実行結果は下記のようになります。
`text: 'an'`というパラメーターがついているので`name`に'an'が含まれているデータを検索しています。
`data`の中にHumanとStarshipのデータが混在しています（Droidは'an'がつくデータがなかったようですね）

```json
{
  "data": {
    "search": [
      {
        "__typename": "Human",
        "name": "Han Solo",
        "height": 1.8
      },
      {
        "__typename": "Human",
        "name": "Leia Organa",
        "height": 1.5
      },
      {
        "__typename": "Starship",
        "name": "TIE Advanced x1",
        "length": 9.2
      }
    ]
  }
}
```

# delegated typeとは
下記のプルリクエストで実装された機能です。
DHHが自らプルリク出していて、Rails6.1に入ったようです。
https://github.com/rails/rails/pull/39341

内容はプルリクの説明にサンプルコードも貼ってあるので、それを読む方が理解しやすいと思いますが、一言でいうとテーブルの継承関係を表現しやすくなる機能です。
...と言ってもイメージが湧きづらいと思うので、Union typesと同じ例を使って説明します。
この例のデータを保持するには1テーブルで表現する方法と複数テーブルで表現する方法が考えられます。
それぞれのどのようなテーブル構造になるのか確認しながら説明します。

## １テーブルで表現する
１テーブルで表現する場合、下記のようなテーブルになると思います。

| Field | Type | Null |
|:-----------|:------------|:-------------|
| id         | bigint(20)  | NO           |
| name       | varchar(255)| NO           |
| type       | tinyint(4)  | NO           |
| height     | float       | YES          |
| primary_function | varchar(255)| YES    |
| length     | float       | YES          |

この構成の場合、タイプを横断して検索するときにもこのテーブルをリードすればよいだけなのでパフォーマンスが出しやすいです。
ただ、タイプごとの属性(height, primary_function, length)が任意項目になっているため、例えばHumanタイプの場合はheightカラムが必須で、その他はnullにするという制御をアプリケーション側で担保する必要があります。

## 複数テーブルで表現する
１テーブルで表現する場合、どうしてもRDB側の制約がゆるくなってしまいます。
これを回避するためにタイプごとにテーブルを作成するという方法があります。

* Humans

| Field | Type | Null |
|:-----------|:------------|:-------------|
| id         | bigint(20)  | NO           |
| name       | varchar(255)| NO           |
| height     | float       | NO           |

* Droids

| Field | Type | Null |
|:-----------|:------------|:-------------|
| id         | bigint(20)  | NO           |
| name       | varchar(255)| NO           |
| primary_function | varchar(255)| NO     |

* Starships

| Field | Type | Null |
|:-----------|:------------|:-------------|
| id         | bigint(20)  | NO           |
| name       | varchar(255)| NO           |
| length     | float       | NO           |

１テーブルの場合と違い、タイプごとにテーブルが分かれているのでtypeカラムは不要になります。
また、タイプごとに必要な属性(height, primary_function, length)も必須カラムにすることができるので、RDBの制約で制御することができるようになります。

データを保存するという観点ではメリットが多いのですが、データを取得する時は３テーブルを参照する必要があります。
特に最初の例のように混在したデータを取得する場合はSQLのUNION句を使ったり、データは別々に取得してアプリケーション側の処理で結合したりする必要がありパフォーマンスが悪くなる可能性があります。

## １テーブルと複数テーブルの良いとこ取り

「複数テーブルで表現する」で書いた方法だと、タイプが混在したデータを取得するときに苦労します。
これを解消させるためにもう１テーブル作ります。
char_typeと各テーブルへのidを持っているテーブルです。例えばchar_typeがHumanの場合は、char_idにhumans.idが入ります。
共通項目であるnameで検索することがあるので、こちらのテーブルにname属性を追加しました。
二重管理になってしまうので各テーブルからはnameを消します。

* Characters

| Field | Type | Null |
|:-----------|:------------|:-------------|
| id         | bigint(20)  | NO           |
| name       | varchar(255)| NO           |
| char_type  | tinyint(4)  | NO           |
| char_id    | bigint(20)  | NO           |

* Humans

| Field | Type | Null |
|:-----------|:------------|:-------------|
| id         | bigint(20)  | NO           |
| height     | float       | NO           |

* Droids

| Field | Type | Null |
|:-----------|:------------|:-------------|
| id         | bigint(20)  | NO           |
| primary_function | varchar(255)| NO     |

* Starships

| Field | Type | Null |
|:-----------|:------------|:-------------|
| id         | bigint(20)  | NO           |
| length     | float       | NO           |

このように1階層上のテーブルを作ることで、混在したデータを取得する際はこのテーブルをリードすればできるようになります。
また、タイプごとの必須制約にRDBの制約を使うこともできています。

このようなテーブル構成のデータをRailsから簡単に扱えるようにした機能が`delegated type`です。

# 実装

それでは実装してみます。
実装する内容ははここまでに使ったGraphQL公式ページの例を使います。

## バージョン
この記事を書いた時点の主要ライブラリのバージョンは下記の通り。

* Ruby: 2.7.2
* Rails: 6.1
* graphql-ruby: 1.11.6
* mysql: 5.7.27

## migration
例に合わせてマイグレーションします。
面倒なので１ファイルにしましたが、きちんとやる場合はテーブルごとに分けたほうがいいと思います。
typeを格納するカラム(今回の例ではchar_type)には'Human'や'Droid’のように文字列が入るので、stringで定義します。

```ruby:db/migrate/20201215031635_create_table_delegated_type.rb
class CreateTableDelegatedType < ActiveRecord::Migration[6.1]
  def change
    create_table :characters do |t|
      t.string :name, null: false
      t.string :char_type, null: false, limit: 10
      t.bigint :char_id, null: false

      t.timestamps
    end

    create_table :humans do |t|
      t.float :height, null: false

      t.timestamps
    end

    create_table :droids do |t|
      t.string :primary_function, null: false

      t.timestamps
    end

    create_table :starships do |t|
      t.float :length, null: false

      t.timestamps
    end
  end
end
```

下記のようにテーブルができました。

```
> desc characters;
+----------------+--------------+------+-----+---------+----------------+
| Field          | Type         | Null | Key | Default | Extra          |
+----------------+--------------+------+-----+---------+----------------+
| id             | bigint(20)   | NO   | PRI | NULL    | auto_increment |
| name           | varchar(255) | NO   |     | NULL    |                |
| char_type      | varchar(10)  | NO   |     | NULL    |                |
| char_id        | bigint(20)   | NO   |     | NULL    |                |
| created_at     | datetime(6)  | NO   |     | NULL    |                |
| updated_at     | datetime(6)  | NO   |     | NULL    |                |
+----------------+--------------+------+-----+---------+----------------+

> desc droids;
+------------------+--------------+------+-----+---------+----------------+
| Field            | Type         | Null | Key | Default | Extra          |
+------------------+--------------+------+-----+---------+----------------+
| id               | bigint(20)   | NO   | PRI | NULL    | auto_increment |
| primary_function | varchar(255) | NO   |     | NULL    |                |
| created_at       | datetime(6)  | NO   |     | NULL    |                |
| updated_at       | datetime(6)  | NO   |     | NULL    |                |
+------------------+--------------+------+-----+---------+----------------+

> desc humans;
+------------+-------------+------+-----+---------+----------------+
| Field      | Type        | Null | Key | Default | Extra          |
+------------+-------------+------+-----+---------+----------------+
| id         | bigint(20)  | NO   | PRI | NULL    | auto_increment |
| height     | float       | NO   |     | NULL    |                |
| created_at | datetime(6) | NO   |     | NULL    |                |
| updated_at | datetime(6) | NO   |     | NULL    |                |
+------------+-------------+------+-----+---------+----------------+

> desc starships;
+------------+-------------+------+-----+---------+----------------+
| Field      | Type        | Null | Key | Default | Extra          |
+------------+-------------+------+-----+---------+----------------+
| id         | bigint(20)  | NO   | PRI | NULL    | auto_increment |
| length     | float       | NO   |     | NULL    |                |
| created_at | datetime(6) | NO   |     | NULL    |                |
| updated_at | datetime(6) | NO   |     | NULL    |                |
+------------+-------------+------+-----+---------+----------------+
```

## models
モデルを作成します。

まずは上位階層のCharacterモデルを定義します。
`delegated_type`に必要な情報を設定します。
`:char`
各テーブルとのリレーションに使うxxx_type, xxx_idのxxxの部分を定義します。
今回はchar_typeとchar_idなので`:char`を設定しています。

`types`
対応するモデルを指定します。

`dependent`
charactersを削除したときに関連テーブルの挙動を定義します。アソシエーションの設定と同じです。

```ruby:app/models/character.rb
class Character < ApplicationRecord
  delegated_type :char, types: %w[Human Droid Starship], dependent: :destroy
end
```

各typeのモデルがincludeするモジュールを作成します。
Characterモデルへのリレーション(has_one)と、共通項目（name)のdelegateを定義しました。

```ruby:app/models/concerns/char.rb
module Char
  extend ActiveSupport::Concern

  included do
    has_one :character, as: :char, touch: true, dependent: :destroy
    delegate :name, to: :character
  end
end
```

各タイプに対応するモデルを作ります。
Charモジュールをincludeします。

```ruby:app/models/human.rb
class Human < ApplicationRecord
  # humenテーブルを参照してしまうので
  self.table_name = "humans"
  include Char
end
```

```ruby:app/models/droid.rb
class Doroid < ApplicationRecord
  include Char
end
```

```ruby:app/models/starship.rb
class Starship < ApplicationRecord
  include Char
end
```

### CRUD
一通りモデルが作成できたので、HumanへのCRUDを試してみました。

#### Create
humansとcharactersへinsertが行われます。整合性を保つため同一transactionで実行されるようです。
途中でcharactersへselectしていますが、これは重複チェックだと思います。

```irb
irb(main)> Character.create!(name: 'Han Solo', char: Human.new(height: 1.8))
  TRANSACTION (0.3ms)  BEGIN
  Human Create (0.4ms)  INSERT INTO `humans` (`height`, `created_at`, `updated_at`) VALUES (1.8, '2020-12-15 05:26:45.730884', '2020-12-15 05:26:45.730884')
  Character Load (0.5ms)  SELECT `characters`.* FROM `characters` WHERE `characters`.`char_id` = 1 AND `characters`.`char_type` = 'Human' LIMIT 1
  Character Create (0.3ms)  INSERT INTO `characters` (`name`, `char_type`, `char_id`, `created_at`, `updated_at`) VALUES ('Han Solo', 'Human', 1, '2020-12-15 05:26:45.833648', '2020-12-15 05:26:45.833648')
  TRANSACTION (1.8ms)  COMMIT
=> #<Character id: 1, name: "Han Solo", char_type: "Human", char_id: 1, created_at: "2020-12-15 05:26:45.833648000 +0000", updated_at: "2020-12-15 05:26:45.833648000 +0000">
```

#### Read

Characterモデルのオブジェクトから各タイプにアソシエーションと同じようにアクセスできるようになります。
違うタイプを指定したらnilが返ってきました。
また、human?のようなタイプを判定するメソッドも使えるようになります。

```irb
irb(main)> char = Character.first
  Character Load (0.9ms)  SELECT `characters`.* FROM `characters` ORDER BY `characters`.`id` ASC LIMIT 1
=> #<Character id: 1, name: "Han Solo", char_type: "Human", char_id: 1, created_at: "2020-12-15 05:26:45.833648000 +0000", updated_at: "2020-12-15 05:26:45.833648000 +0000">
irb(main)> char.human?
=> true
irb(main)> char.human
  Human Load (0.7ms)  SELECT `humans`.* FROM `humans` WHERE `humans`.`id` = 1 LIMIT 1
=> #<Human id: 1, height: 1.8, created_at: "2020-12-15 05:26:45.730884000 +0000", updated_at: "2020-12-15 05:26:45.730884000 +0000">
irb(main)> char.droid?
=> false
irb(main)> char.droid
=> nil
irb(main)> char.char_name
=> "human"
```

#### Update

Characterモデルの項目を修正するとcharactersテーブルのみ更新されます。

```irb
irb(main)> char.name = 'Han Soloooooo'
=> "Han Soloooooo"
irb(main)> char.save!
  TRANSACTION (0.5ms)  BEGIN
  Character Update (2.7ms)  UPDATE `characters` SET `characters`.`name` = 'Han Soloooooo', `characters`.`updated_at` = '2020-12-15 05:42:12.659673' WHERE `characters`.`id` = 1
  TRANSACTION (2.3ms)  COMMIT
=> true
```

Humanモデルの項目を修正するとcharactersテーブルのupdated_atも更新しています。
これはdelegated typeの機能というよりはchar moduleのhas_oneにtouch: trueを付けているからです。

```ruby
irb(main)> char.human.height = 2.1
=> 2.1
irb(main)> char.human.save!
  TRANSACTION (0.3ms)  BEGIN
  Human Update (0.6ms)  UPDATE `humans` SET `humans`.`height` = 2.1, `humans`.`updated_at` = '2020-12-15 05:42:30.095070' WHERE `humans`.`id` = 1
  Character Load (0.9ms)  SELECT `characters`.* FROM `characters` WHERE `characters`.`char_id` = 1 AND `characters`.`char_type` = 'Human' LIMIT 1
  Character Update (0.5ms)  UPDATE `characters` SET `characters`.`updated_at` = '2020-12-15 05:42:30.103572' WHERE `characters`.`id` = 1
  TRANSACTION (2.5ms)  COMMIT
=> true
```

#### Delete

Characterモデルを削除しました。
`dependent: :destroy`を指定しているので関連するStarshipモデルも削除されています。

```irb
irb(main)> a = Character.last
  Character Load (0.5ms)  SELECT `characters`.* FROM `characters` ORDER BY `characters`.`id` DESC LIMIT 1
=> #<Character id: 3, name: "TIE Advanced x1", char_type: "Starship", char_id: 1, created_at: "2020-12-15 05:29:53.110094000 +0000", updated_at: "2020-12-15 05:29:53.110094000 +0000">
irb(main)> a.destroy
  TRANSACTION (0.3ms)  BEGIN
  Character Destroy (1.0ms)  DELETE FROM `characters` WHERE `characters`.`id` = 3
  Starship Load (0.5ms)  SELECT `starships`.* FROM `starships` WHERE `starships`.`id` = 1 LIMIT 1
  Character Load (1.0ms)  SELECT `characters`.* FROM `characters` WHERE `characters`.`char_id` = 1 AND `characters`.`char_type` = 'Starship' LIMIT 1
  Starship Destroy (0.9ms)  DELETE FROM `starships` WHERE `starships`.`id` = 1
  TRANSACTION (3.9ms)  COMMIT
=> #<Character id: 3, name: "TIE Advanced x1", char_type: "Starship", char_id: 1, created_at: "2020-12-15 05:29:53.110094000 +0000", updated_at: "2020-12-15 05:29:53.110094000 +0000">
```

Droidモデルを削除しました。
`dependent: :destroy`を指定しているので関連するCharacterモデルも削除されています。

```irb
irb(main)> b = Character.last
  Character Load (0.7ms)  SELECT `characters`.* FROM `characters` ORDER BY `characters`.`id` DESC LIMIT 1
=> #<Character id: 2, name: "C-3PO", char_type: "Droid", char_id: 1, created_at: "2020-12-15 05:29:45.752745000 +0000", updated_at: "2020-12-15 05:29:45.752745000 +0000">
irb(main)> b.droid.destroy
  Droid Load (0.7ms)  SELECT `droids`.* FROM `droids` WHERE `droids`.`id` = 1 LIMIT 1
  TRANSACTION (0.4ms)  BEGIN
  Character Load (0.6ms)  SELECT `characters`.* FROM `characters` WHERE `characters`.`char_id` = 1 AND `characters`.`char_type` = 'Droid' LIMIT 1
  Character Destroy (0.7ms)  DELETE FROM `characters` WHERE `characters`.`id` = 2
  Droid Destroy (0.7ms)  DELETE FROM `droids` WHERE `droids`.`id` = 1
  TRANSACTION (2.2ms)  COMMIT
=> #<Droid id: 1, primary_function: "talk", created_at: "2020-12-15 05:29:45.744823000 +0000", updated_at: "2020-12-15 05:29:45.744823000 +0000">
```

## GraphQL
ここからはGraphQLのsearchクエリーを実装していきます。

まずは各モデルのタイプを作ります。

```ruby:app/graphql/types/human_type.rb
module Types
  class HumanType < BaseObject
    field :id, ID, null: false
    field :name, String, null: false
    field :height, Float, null: false
  end
end
```

```ruby:app/graphql/types/droid_type.rb
module Types
  class DroidType < BaseObject
    field :id, ID, null: false
    field :name, String, null: false
    field :primary_function, String, null: false
  end
end
```

```ruby:app/graphql/types/starship_type.rb
module Types
  class StarshipType < BaseObject
    field :id, ID, null: false
    field :name, String, null: false
    field :length, Float, null: false
  end
end
```

上記をまとめたCharacterタイプを作ります。やっとUnionタイプの登場です。
`possible_types`に登場するタイプを指定します。
`self.resolve_type`でタイプの判定方法と対応するオブジェクトを返却します。

```ruby:app/graphql/types/character_type.rb
module Types
  class CharacterType < Types::BaseUnion
    possible_types Types::HumanType, Types::DroidType, Types::StarshipType

    def self.resolve_type(object, context)
      if object.human?
        [Types::HumanType, object.human]
      elsif object.droid?
        [Types::DroidType, object.droid]
      elsif object.starship?
        [Types::StarshipType, object.starship]
      end
    end
  end
end
```

続いてクエリータイプです。
私はresolverをquery_type.rbと別クラスに定義する書き方をよくするので、今回も別クラスでSearchResolverを定義しています。
例のようにtextを指定するとnameで部分一致検索をするようにしました。

```ruby:app/graphql/resolvers/search_resolver.rb
module Resolvers
  class SearchResolver < BaseResolver
    type Types::CharacterType.connection_type, null: false

    argument :text, String, required: false

    def resolve(text: nil)
      text.nil? ? Character.all : Character.where('name like ?', "%#{text}%")
    end
  end
end
```

```ruby:app/graphql/types/query_type.rb
module Types
  class QueryType < Types::BaseObject
    field :search, resolver: Resolvers::SearchResolver
  end
end
```

## 実行
ここまでで実装完了です。
早速実行してみましょう。

まずは全件検索です。

```graphql
{
  search {
    edges {
      node {
        ... on Human {
          name
          height
        }
        ... on Droid {
          name
          primaryFunction
        }
        ... on Starship {
          name
          length
        }
      }
    }
  }
}
```

下記のように各タイプが混在して返却されました。

```json
{
  "data": {
    "search": {
      "edges": [
        {
          "node": {
            "name": "Han Solo",
            "height": 1.8
          }
        },
        {
          "node": {
            "name": "C-3PO",
            "primaryFunction": "talk"
          }
        },
        {
          "node": {
            "name": "TIE Advanced x1",
            "length": 9.2
          }
        },
        {
          "node": {
            "name": "Han Soloooo",
            "height": 1.8
          }
        }
      ]
    }
  }
}
```

続いてtextで絞り込んでみます。

```graphql
{
  search(text: "an") {
    edges {
      node {
        ... on Human {
          name
          height
        }
        ... on Droid {
          name
          primaryFunction
        }
        ... on Starship {
          name
          length
        }
      }
    }
  }
}
```

きちんと"an"が含まれるデータのみ返却されるようになりました。

```json
{
  "data": {
    "search": {
      "edges": [
        {
          "node": {
            "name": "Han Solo",
            "height": 1.8
          }
        },
        {
          "node": {
            "name": "TIE Advanced x1",
            "length": 9.2
          }
        }
        {
          "node": {
            "name": "Han Soloooo",
            "height": 1.8
          }
        }
      ]
    }
  }
}
```

最後にクエリーを確認してみます。
対応するテーブル(humans, droids, starships)へのselectが１件ごとに実行されています。
また共通項目nameの取得時にcharactersテーブルへのselectも動いているようです。
まさにN+1地獄ですね・・・
これだと実践での利用は厳しいので先読みできないか調査してみました。

```console
  Character Load (0.8ms)  SELECT `characters`.* FROM `characters`
  ↳ app/controllers/graphql_controller.rb:13:in `execute'
  Human Load (0.7ms)  SELECT `humans`.* FROM `humans` WHERE `humans`.`id` = 1 LIMIT 1
  ↳ app/graphql/types/character_type.rb:7:in `resolve_type'
  Character Load (0.7ms)  SELECT `characters`.* FROM `characters` WHERE `characters`.`char_id` = 1 AND `characters`.`char_type` = 'Human' LIMIT 1
  ↳ app/models/concerns/char.rb:6:in `name'
  Droid Load (0.7ms)  SELECT `droids`.* FROM `droids` WHERE `droids`.`id` = 1 LIMIT 1
  ↳ app/graphql/types/character_type.rb:9:in `resolve_type'
  Character Load (0.6ms)  SELECT `characters`.* FROM `characters` WHERE `characters`.`char_id` = 1 AND `characters`.`char_type` = 'Droid' LIMIT 1
  ↳ app/models/concerns/char.rb:6:in `name'
  Starship Load (0.5ms)  SELECT `starships`.* FROM `starships` WHERE `starships`.`id` = 1 LIMIT 1
  ↳ app/graphql/types/character_type.rb:11:in `resolve_type'
  Character Load (0.4ms)  SELECT `characters`.* FROM `characters` WHERE `characters`.`char_id` = 1 AND `characters`.`char_type` = 'Starship' LIMIT 1
  ↳ app/models/concerns/char.rb:6:in `name'
  Human Load (0.5ms)  SELECT `humans`.* FROM `humans` WHERE `humans`.`id` = 2 LIMIT 1
  ↳ app/graphql/types/character_type.rb:7:in `resolve_type'
  Character Load (0.7ms)  SELECT `characters`.* FROM `characters` WHERE `characters`.`char_id` = 2 AND `characters`.`char_type` = 'Human' LIMIT 1
  ↳ app/models/concerns/char.rb:6:in `name'
```

SearchResolverで取得するときにpreloadするようにリファクタリングしてみました。
最初は`preload(:char)`だけでいけるかなと思ったのですが、各モデルからCharacterモデルを参照するname取得処理でselectが発行されてしまったので、`:character`も追記しました。

```diff:app/graphql/resolvers/search_resolver.rb
    def resolve(text: nil)
-     text.nil? ? Character.all : Character.where('name like ?', "%#{text}%")
+     chars = Character.all
+     chars = chars.where('name like ?', "%#{text}%") if text
+     chars.preload(char: :character)
    end
```

下記のようにまとめて取得されるようになりました。（クエリーが多いので見ずらいですが、データが２件存在するhumansがまとめて取得されるようになりました）

```console
  Character Load (0.5ms)  SELECT `characters`.* FROM `characters`
  ↳ app/controllers/graphql_controller.rb:13:in `execute'
  Human Load (0.7ms)  SELECT `humans`.* FROM `humans` WHERE `humans`.`id` IN (1, 2)
  ↳ app/controllers/graphql_controller.rb:13:in `execute'
  Droid Load (0.3ms)  SELECT `droids`.* FROM `droids` WHERE `droids`.`id` = 1
  ↳ app/controllers/graphql_controller.rb:13:in `execute'
  Starship Load (0.4ms)  SELECT `starships`.* FROM `starships` WHERE `starships`.`id` = 1
  ↳ app/controllers/graphql_controller.rb:13:in `execute'
  Character Load (0.4ms)  SELECT `characters`.* FROM `characters` WHERE `characters`.`char_type` = 'Human' AND `characters`.`char_id` IN (1, 2)
  ↳ app/controllers/graphql_controller.rb:13:in `execute'
  Character Load (0.5ms)  SELECT `characters`.* FROM `characters` WHERE `characters`.`char_type` = 'Droid' AND `characters`.`char_id` = 1
  ↳ app/controllers/graphql_controller.rb:13:in `execute'
  Character Load (0.9ms)  SELECT `characters`.* FROM `characters` WHERE `characters`.`char_type` = 'Starship' AND `characters`.`char_id` = 1
  ↳ app/controllers/graphql_controller.rb:13:in `execute'
```

# 最後に
最後まで読んでいただきありがとうございます :bow:
明日はVISITSのエンジニアリングマネージャー @kotala_b の記事です。お楽しみに！！