GraphQLのスキーマと型を学び直した

# はじめに
既存のシステムでGraphQLを触っているのですが、スキーマを一から作成した経験がないことと、スキーマと型システムの基礎的な部分の理解が浅いなぁと感じていたので学び直しました。

学習方法としては、具体的な題材を元にスキーマを作っていくと知識が定着しやすかったので、その過程も含め、スキーマと型システムの基礎的な内容を共有しようと思います。

# 本記事の題材と作成するもの
本記事では、ピザとパスタの2種類の商品を提供するオンラインデリバリーサービスを題材にし、2つの商品情報をCRUDするためのGraphQL APIの仕様（スキーマ）を作っていきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/319375/c11fd622-4564-1765-2ba8-ce76c3677d58.png)


# 前提情報
最初にGraphQLのスキーマと型システムについてざっくり説明します。

## GraphQLのスキーマとは？
前述の通り、Graphql APIの仕様を定義するものです。
具体的には、GraphQL APIがどのようなデータを持っており、クライアントがどうやってそのデータを操作できるのかを明確にするためのルールを定めた設計書のようなイメージです。

スキーマは以下のような構造となっており、スキーマ定義言語（SDL）を使って記述します。
```
type Pizza implements Product {
    id: ID!
    name: String!
    price: Float!
    size: String
}

type Pizza {
    id: ID!
    name: String!
    price: Int!
    size: String
    hasGlutenFreeCrust: Boolean
}

type Query {
    products: [Product!]
}
```

## GraphQLの型システムとは？
GraphQLの型システムは、APIを通じてやりとりされるデータの種類や形式を定義するものです。
この型システムによってGraphQL APIのデータは厳密に型付けされ、エラーの少ないデータ交換が可能になります。

詳細は後述しますが、型システムとしては基本的なスカラー型（String, Int, Boolean など）から複雑なオブジェクト型まで、様々なデータ型が存在します。


# 題材を元にスキーマを作っていく
スキーマと型システムの概要がざっくり理解できたところで、ここからは各型システムの説明を交えつつ、ピザとパスタの商品情報を管理するための具体的なスキーマの作成を進めていきます。

## `オブジェクト型`で具体的なエンティティを定義する
オブジェクト型は、GraphQLで扱う主要なデータ構造の1つであり、データベースに例えると「テーブル」に近しい概念です。

オブジェクト型は「ユーザー」や「商品」などの具体的なエンティティを表すために使用します。

オブジェクト型を定義するには、`type`キーワードに続いてオブジェクトの名前を指定し、波括弧で囲みます。

今回は、具体的なエンティティ（オブジェクト）としてピザとパスタがあるのでそれぞれ以下のように定義します。
```
type Pizza {
}

type Pasta {
}
```

## `スカラー型`でオブジェクトが持つフィールドのデータ型を定義する
上記で作成したオブジェクトは基本的に属性（フィールド）を持ちます。
データベースに例えると「カラム」に相当するものです。

データベースのカラムにはデータ型を定義することで、どのような種類のデータが格納されるのかを制限できますよね。
例えば、整数型（INT）、文字列型（VARCHAR）、日付型（DATE）などがあります。

GraphQLでは、これに相当するものとして「スカラー型」と呼ばれるものがあります。

GraphQLのスカラー型は以下の5つがあります。
- 文字列型（`String`）
- 整数型（`Int`）
- 浮動小数点数型（`Float`）
- Boolean型（`Boolean`）
- 一意な識別子（`ID`）
    - 厳密にいうとIDは文字列ですが、GraphQLでは各オブジェクトのIDが一意であることを保証してくれます。

今回は、以下のフィールドおよび型を定義します。
```diff
type Pizza {
+    id: ID!
+    name: String!
+    price: Int!
+    # 在庫があるかどうか
+    available: Boolean!
+    # グルテンフリーの生地が提供されているかどうかを示す
+    hasGlutenFreeCrust: Boolean
}

type Pasta {
+    id: ID!
+    name: String!
+    price: Int!
+    # 在庫があるかどうか
+    available: Boolean!
+    # ビーガン対応の材料で作られているかどうかを示す
+    isVeganFriendly: Boolean
}
```

### エクスクラメーションマーク（`!`マーク）について
上記のフィールドの型には`ID!`や`String!`のように、`!`が付いています。
これはエクスクラメーションマークと呼ばれるものであり、`!`が付いているものはnullが許容されないフィールドであることを意味します。

上記例だと、`id`、`name`、`price`、`available`がnullになることがなく、`hasGlutenFreeCrust`と`isVeganFriendly`はnullになる可能性があります。

## `カスタムスカラー型`で任意のデータ型を作る
多くの場合、GraphQLの組み込みのスカラー型で十分ですが、場合によってスカラー型のみだと対応できないケースがあります。
例えば、日付型やメールアドレス型、URL型などを表現できるようにしたいケースが考えられます。

この場合、「カスタムスカラー型」として任意の型を定義することができます。

カスタムスカラー型は`scalar`キーワードを使って宣言します。

今回は、オブジェクトが更新された日時を表現するためのDatetimeのカスタムスカラー型を定義し、各オブジェクトの`lastupdated`フィールドに割り当てます。
```diff
+scalar Datetime

type Pizza {
    id: ID!
    name: String!
    price: Int!
    available: Boolean!
    hasGlutenFreeCrust: Boolean
+   lastupdated: Datetime!
}

type Pasta {
    id: ID!
    name: String!
    price: Int!
    available: Boolean!
    isVeganFriendly: Boolean
+   lastupdated: Datetime!
}
```

## `Enum型`でフィールドが扱う値を事前に定義する
多くのプログラミング言語で使われている列挙をGraphQLでも定義することができます。

GraphQLでEnum型を宣言するには、enumキーワードに続いて列挙の名前を指定します。

今回は、ピザとパスタの注文サイズを定義するEnum型を作り、各オブジェクトの`size`フィールドに割り当てます。
```diff

+enum Size {
+    LARGE
+    MEDIUM
+    SMALL
+}

type Pizza {
    id: ID!
    name: String!
    price: Int!
    available: Boolean!
+   size: Size!
    hasGlutenFreeCrust: Boolean
    lastupdated: Datetime!
}

type Pasta {
    id: ID!
    name: String!
    price: Int!
    available: Boolean!
+   size: Size!
    isVeganFriendly: Boolean
    lastupdated: Datetime!
}
```

## `リスト`でフィールドのコレクションを表す
リストは型の配列です。
1つのフィールドに複数の値やオブジェクトを格納するために使用されます。

リストは型を角括弧`[]`で囲むことによって表現できます。

今回は、各商品の説明を複数要素格納できるよう、`description`フィールドが扱う値を`String`型のリストにしてみます。
```diff
type Pizza {
    id: ID!
    name: String!
    price: Int!
    available: Boolean!
    size: Size!
    hasGlutenFreeCrust: Boolean
    lastupdated: Datetime!
+   description: [String!]
}

type Pasta {
    id: ID!
    name: String!
    price: Int!
    available: Boolean!
    size: Size!
    isVeganFriendly: Boolean
    lastupdated: Datetime!
+   description: [String!]
}
```

### リストのエクスクラメーションマークについて
上記の例では`[String!]`としていますが、リストのエクスクラメーションマークはこれ以外にも合計3パターンの表現方法があります。

各パターンにおけるエクスクラメーションマークの組み合わせパターンと、パターンごとの挙動を以下に示します。

| 渡す値 | `[String]` | `[String!]` | `[String]!` | `[String!]!` | 
| ---- | ---- | ---- | ---- | ---- |
| null | 有効 | 有効 | 無効 | 無効 |
| [] | 有効 | 有効 | 有効 | 有効 |
| ["word"] | 有効 | 有効 | 有効 | 有効 |
| [null] | 有効 | 無効 | 有効 | 無効 |
| ["word", null] | 有効 | 無効 | 有効 | 無効 |

一例として`[String!]`を言葉で説明すると、**リスト自体のnullは許容するが、リスト内の要素のnullは許容しない**ということです。

## オブジェクト型の間でコネクションを構築する
GraphQLスキーマでは、異なるオブジェクト同士を接続することができ、これによってリレーションシップ（1対1、1対多など）を構築することができます。
具体的には、後述するエッジやスルー型を利用します。

### エッジ
エッジはグラフ理論から来ている概念であり、2つのオブジェクト間の接続を表現するものです。

エッジを説明する上で、まずは新しいオブジェクトとして
- `Ingredient`（ピザやパスタの原材料を扱うオブジェクト）
- `Supplier`（原材料の生産者を扱うオブジェクト）

の２つをGraphQLスキーマに定義します。
```
Type Ingredient {
    id: ID!
    name: String!
}

Type Supplier {
    id: ID!
    name: String!
    address: String!
}
```

#### 1対1のコネクションを作る
GraphQLで`Ingredient`の情報を取得する際に、`Ingredient`に紐づく`Supplier`の情報も取得できるようにするにはどうすればいいでしょうか。

1つの原材料は、1人の生産者に紐づいていることを前提にした場合、
以下のように`Ingredient`オブジェクトのフィールドとして`Supplier`型を指定します。
```diff
Type Ingredient {
    id: ID!
    name: String!
+   supplier: Supplier!
}

Type Supplier {
    id: ID!
    name: String!
    address: String!
}
```
これにより、1対1のコネクションを作ることができます。

![エッジ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/319375/848400b1-c2fe-ecef-c17b-f2810129ed14.png)


なお、接続の向きとしては上図の通りで、この状態だと`Ingredient`から`Supplier`にアクセスすることはできますが、その逆はできません。



#### 双方向（1対多）のコネクションを作る
逆のケースで`Supplier`の情報を取得する際、提供している`Ingredient`も取得できるようにするにはどうすればいいでしょうか。

`Supplier`は、複数の`Ingredient`を提供していることを前提にした場合、
以下のように`Supplier`オブジェクトのフィールドとして`Ingredient`型を要素に持つリストを指定すると1対多の接続ができます。
```diff
Type Ingredient {
    id: ID!
    name: String!
    supplier: Supplier!
}

Type Supplier {
    id: ID!
    name: String!
    address: String!
+   ingredients: [Ingredient!]!
}
```
![エッジ-ページ2のコピー.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/319375/7c2c5c12-03ac-547e-fbe8-2213a4629fca.png)


これにより、`Supplier`が提供しているすべての原材料を取得することができます。


### スルー型
スルー型は2つのオブジェクト間に中間的なデータ構造を設け、それぞれの関連に追加情報を付与することができるものです。

例えば、どのピザにどの原材料がどれくらいの量で使用されているのかを管理したい場合に利用できます。
これを実現するため、今回はスルー型として`Recipe`を追加し、原材料とその量をフィールドとして持たせます。
```diff
+type Recipe {
+    ingredient: Ingredient!
+    quantity: Float!
+}

type Pizza {
    id: ID!
    name: String!
    price: Int!
    available: Boolean!
    size: Size!
    hasGlutenFreeCrust: Boolean
    lastupdated: Datetime!
    description: [String!]
+   Ingredients: [IngredientRecipe!]!
}

type Pasta {
    id: ID!
    name: String!
    price: Int!
    available: Boolean!
    size: Size!
    isVeganFriendly: Boolean
    lastupdated: Datetime!
    description: [String!]
+   Ingredients: [IngredientRecipe!]!
}

Type Ingredient {
    id: ID!
    name: String!
    supplier: Supplier!
}

Type Supplier {
    id: ID!
    name: String!
    address: String!
    ingredients: [Ingredient!]!
}
```
図にすると以下のようなイメージです。
![スルー型.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/319375/826b1aad-3200-8146-9516-8d1745535b4b.png)


## `インターフェース型`を使って共通のフィールドを定義する
インターフェース型は異なる型が共通のフィールドを持つことを保証するための抽象型です。
JavaやC#のインターフェースに似ており、インターフェースで定義されたフィールドは実装側で全て定義しなくてはなりません。

インターフェースの型は`interface`キーワードを使って宣言します。

今回は、`Pizza` と `Pasta` が同じ「商品」であり、共通するフィールドを持っているためインターフェースにまとめられそうです。
新たに`ProductInterface`を定義し、`Pizza` と `Pasta` に実装します。 
```diff
+interface ProductInterface {
+  　　id: ID!
+   name: String!
+   price: Int!
+   size: Size!
+   lastupdated: Datetime!
+   description: [String!]
+   Ingredients: [IngredientRecipe!]!
+}

+type Pizza implements ProductInterface {
    id: ID!
    name: String!
    price: Int!
    available: Boolean!
    size: Size!
    hasGlutenFreeCrust: Boolean
    lastupdated: Datetime!
    description: [String!]
    Ingredients: [IngredientRecipe!]!
}

+type Pasta implements ProductInterface {
    id: ID!
    name: String!
    price: Int!
    available: Boolean!
    size: Size!
    isVeganFriendly: Boolean
    lastupdated: Datetime!
    description: [String!]
    Ingredients: [IngredientRecipe!]!
}
```
これにより、GraphQLの利用者は`ProductInterface`型を調べることで、`Pizza` と `Pasta` の両方で利用できるフィールドをすばやく特定することができます。

また、将来的に新しい商品が追加された場合は、その新しい商品で`ProductInterface`を実装することで共通するフィールドの定義漏れを防ぐことができます。


## `ユニオン型`で複数の型を1つにまとめる
インターフェースが様々な型に共通するフィールドをまとめるのに対し、ユニオンは様々な型を同じ1つの型にまとめるの役立ちます。

簡単に言うと、ユニオン型は**複数の型のいずれか1つを返すことができる型**であり、異なる型のオブジェクトを1つのオブジェクトとして扱いたい場合などに非常に便利です。
なお、ユニオンはインターフェースとは異なり、共通のフィールドを持つことを要求しません。

ユニオンの型は`union`キーワードを使って宣言し、パイプ（`|`）を使って1つにまとめる型を記述します。

今回は、`Pizza`と`Pasta`のオブジェクト型を同じ商品として取り扱えるようにするため、unionを使って以下のようにまとめます。

```diff
union Product = Pizaa | Pasta
```

さらに、`Ingredient`オブジェクトのスルー型フィールドとして、上記のユニオン型`Product`を定義します。
```diff
interface ProductInterface {
    id: ID!
    name: String!
    price: Int!
    available: Boolean!
    size: Size!
    lastupdated: Datetime!
    description: [String!]
    Ingredients: [IngredientRecipe!]!
}

type Pizza implements ProductInterface {
    id: ID!
    name: String!
    price: Int!
    available: Boolean!
    size: Size!
    hasGlutenFreeCrust: Boolean
    lastupdated: Datetime!
    description: [String!]
    Ingredients: [IngredientRecipe!]!
}

type Pasta implements ProductInterface {
    id: ID!
    name: String!
    price: Int!
    available: Boolean!
    size: Size!
    isVeganFriendly: Boolean
    lastupdated: Datetime!
    description: [String!]
    Ingredients: [IngredientRecipe!]!
}

+union Product = Pizaa | Pasta

Type Ingredient {
    id: ID!
    name: String!
    supplier: Supplier!
+   products: [Product!]!
}
```
上記のように定義することで、同じ原材料が使われている商品（`Pizza`, `Pasta`を意識することなく）をすべて取得することができるようになります。

## ルート型
ルート型は今まで説明してきた他の型とは少し毛色が異なるものです。

ルート型はGraphQL APIのエントリーポイントとして機能するものであり、GraphQL APIに対して実行可能なオペレーションを定義する型となります。
以下の3つがあります。
- **query**
    - GraphQL APIからデータを取得するオペレーション
- **mutation**
    - GraphQL APIに対してデータの変更を行うオペレーション
- **subscription**（本記事では説明しません）
    - GraphQL APIが保持するデータの変更が行われた際、クライアント側でその情報をリアルタイムに受け取るためのオペレーション

スキーマ内にクエリやミューテーションを定義することにより、GraphQL APIに対してデータ操作が行えるようになります。 [^1]:

[^1]: クエリやミューテーションのスキーマはあくまで定義のみであり、実際のデータ操作は行いません。実際にデータ操作を実行するのがリゾルバというものになります。リゾルバはデータを返す関数であり、リゾルバ内にデータ操作の具体的な処理を別途実装する必要があります。

### `クエリ`でAPIからデータを取得できるようにする
クエリを定義するには、`type`キーワードに続いて`Query`を定義します。
波括弧内にクエリの名前とコロンの後に戻り値の型を指定します。

例えば、全商品を取得するための単純なクエリを作る場合は以下の通りです。
```
# products()クエリにリクエストを投げると、ユニオン型のProductsをリストで返す
type Query {
    products: [Products!]!
}
```
ただ、APIの利用者としては在庫状況や価格でフィルタリングしたい場合があると思います。
この場合は以下のようにクエリパラメータを`()`内に定義することができます。
```diff
type Query {
+    products(available: Boolean, maxPrice: Int, minPrice: Int): [Products!]!
}
```
#### `input型`で渡すパラメータを定義する

今は3つのクエリパラメータで済んでいますが、渡すパラメータが多くなると読みにくく管理しにくいクエリとなってしまいます。
この場合、`input`型を使ってパラメータを定義することで可読性が向上します。
```diff
+input ProductsFilter {
+   maxPrice: Int
+   minPrice: Int
+   available: Boolean = true  # デフォルト値がつけられる
+}

type Query {
+   products(input: ProductsFilter!): [Products!]!　　　# inputパラメータ型をProductFilterに設定
}
```


### `ミューテーション`でデータを変更できるようにする
ミューテーションを定義するには、`type`キーワードに続いて`Mutation`を定義します。
波括弧内の書き方はクエリと同じです。

今回は、商品を追加・更新・削除するための以下のミューテーションを定義します。
- addProduct()
- updateProduct()
- deleteProduct()

まず、新しい商品を追加するための`addProduct()`は以下のように書きます。
```
# 登録できる商品をenum型で制限する
enum ProductType {
    pizza
    pasta
}

# Recipeを登録する用のパラメータを定義
input RecipeInput {
    ingredient: ID!
    quantity: Float!
}

type Mutation {
    addProduct(
        name: String,
        price: Int,
        size: Size,
        hasGlutenFreeCrust: Boolean = False,
        isVeganFriendly: Boolean = False,
        description: [String!],
        type: ProductType!,
        Ingredients: [RecipeInput!]!
    ): Product!    # 戻り値
}
```

ただ、`addProduct()`が受け取るパラメータの数が多いので、ここでもinput型で渡す値をまとめます。
```diff
+input ProductInput {
+    name: String
+    price: Int,
+    size: Size,
+    hasGlutenFreeCrust: Boolean = False,
+    isVeganFriendly: Boolean = False,
+    description: [String!],
+    Ingredients: [RecipeInput!]
+}

type Mutation {
    addProduct(
        type: ProductType!,
+       input: ProductInput!
    ): Product!
}
```

input型でパラメータをまとめると、同じinput型を別のミューテーションで再利用することができます。これがinput型のもう一つの利点です。

ここでは、input型 `ProductInput` のパラメータを 更新用のミューテーション`updateProduct()` でも利用することにします。
```diff
input ProductInput {
    name: String
    price: Int,
    size: Size,
    hasGlutenFreeCrust: Boolean = False,
    isVeganFriendly: Boolean = False,
    description: [String!],
    Ingredients: [RecipeInput!]
}

type Mutation {
    addProduct(
        type: ProductType!,
        input: AddProductInput!
    ): Product!
    
+    updateProduct(id: ID!, input: ProductInput!): Product!
}
```

削除用のミューテーション `deleteProduct()` はIDのみ受け取り、返却値としては削除の成功 or 失敗を示すBoolean値にします。
```diff
type Mutation {
    addProduct(
        type: ProductType!,
        input: AddProductInput!
    ): Product!
    
    updateProduct(id: ID!, input: ProductInput!): Product!
+   deleteProduct(id: ID!): Boolean!
     
}
```
## 本記事で作成したGraphQLスキーマ

<details><summary>schema.graphqls</summary>

```
interface ProductInterface {
    id: ID!
    name: String!
    price: Int!
    available: Boolean!
    size: Size!
    lastupdated: Datetime!
    description: [String!]
    Ingredients: [IngredientRecipe!]!
}

type Pizza implements ProductInterface {
    id: ID!
    name: String!
    price: Int!
    available: Boolean!
    size: Size!
    hasGlutenFreeCrust: Boolean
    lastupdated: Datetime!
    description: [String!]
    Ingredients: [IngredientRecipe!]!
}

type Pasta implements ProductInterface {
    id: ID!
    name: String!
    price: Int!
    available: Boolean!
    size: Size!
    isVeganFriendly: Boolean
    lastupdated: Datetime!
    description: [String!]
    Ingredients: [IngredientRecipe!]!
}

union Product = Pizaa | Pasta

Type Ingredient {
    id: ID!
    name: String!
    supplier: Supplier!
    products: [Product!]!
}

Type Supplier {
    id: ID!
    name: String!
    address: String!
    ingredients: [Ingredient!]!
}

input ProductsFilter {
   maxPrice: Int
   minPrice: Int
   available: Boolean = true
}

type Query {
   products(input: ProductsFilter!): [Products!]!
}

input ProductInput {
    name: String
    price: Int,
    size: Size,
    hasGlutenFreeCrust: Boolean = False,
    isVeganFriendly: Boolean = False,
    description: [String!],
    Ingredients: [RecipeInput!]
}

type Mutation {
    addProduct(
        type: ProductType!,
        input: AddProductInput!
    ): Product!
    
    updateProduct(id: ID!, input: ProductInput!): Product!
    deleteProduct(id: ID!): Boolean!
}
```
</details>

※本来は`Ingredient`や`Supplier`のクエリ/ミューテーションも必要ですが、含めていません。

# さいごに
余力があれば、今回作ったスキーマを元にしたリゾルバの実装とクエリ/ミューテーションの具体的な動作について別記事にまとめたいと思います。

# 参考
https://www.amazon.co.jp/%E5%AE%9F%E8%B7%B5%E3%83%9E%E3%82%A4%E3%82%AF%E3%83%AD%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9API-Jos%C3%A9-Haro-Peralta/dp/4798179736