はじめに
GraphQLに関する情報は英語圏の資料が圧倒的に多く、日本語のドキュメントやリソースがまだ十分に充実していません。そのため、個人的にGraghQLの設計思想などが好きなこともあり、復讐も兼ねて本記事では日本語で分かりやすくGraphQLスキーマ設計を解説し、これから学び始める方に役立つようにまとめました。
スキーマは「型の集合」
GraphQLを使うと、APIは従来のRESTのような「エンドポイントの集合」ではなく、「型の集合」として捉えられます。つまり、どんなデータが存在していて、どのようにやり取りされるかを型で定義するのです。
この型定義の集合こそが「スキーマ」であり、バックエンド・フロントエンド双方の開発者が共通言語として利用しやすくなります。
スキーマファーストによるメリット
スキーマファーストとは、まずアプリケーションで扱うデータ型をしっかり設計したうえで実装を進める方法論です。これによって、
- バックエンドはどんな型が必要かを明確に把握し、適切な永続化処理やリクエストの実装が可能
- フロントエンドはどんなデータを使えるか把握しやすく、UIに反映させるときに迷わない
- 全メンバーが同じ型定義を見ながらコミュニケーションを取れるといったメリットが得られます。
4.1 型定義の基本
Photo型の例
写真共有アプリケーションを例に考えます。まずは、Photo
型を定義してみましょう。
type Photo {
id: ID!
name: String!
url: String!
description: String
}
-
id
は全てのPhoto
を一意に識別するためのID。GraphQLのID
型はユニークな文字列を表します。 -
name
とurl
は必須なのでString!
。 -
description
は省略(null)される場合もあるためエクスクラメーションマークを付けていません。
このように、フィールドの型とnullの可否を明確に示すことで、データのやり取りがわかりやすくなります。
組み込みスカラー型とカスタムスカラー型
GraphQLにはあらかじめInt
, Float
, String
, Boolean
, ID
の5つのスカラー型が組み込まれています。ただし、日時など特別なバリデーションが必要な場合は、独自のカスタムスカラー型を定義できます。
scalar DateTime
type Photo {
...
created: DateTime!
}
このようにcreated
フィールドを日時として扱いたい場合、サーバー側で正しい形式か検証しつつレスポンスを返せるようになります。
Enum(列挙型)
フィールドの値が限られた選択肢の中から必ず1つを選ぶようなケースでは、enum
を利用すると便利です。
enum PhotoCategory {
SELFIE
PORTRAIT
ACTION
LANDSCAPE
GRAPHIC
}
type Photo {
...
category: PhotoCategory!
}
これにより、category
がSELFIE
・PORTRAIT
・ACTION
などの決まった値からのみ選ばれることが保証されます。
4.2 接続(リレーション)とリスト
リスト型とnull制約
GraphQLでは、[String]
のように角括弧を用いて型のリストを表せます。
また、!
を付ける場所で「リスト全体がnullにならない」「要素がnullにならない」という制約を設定できます。よほどの理由がない限り、実用上は[Type!]!
のように「リスト自体も、要素もnull不可」の指定をすることが多いでしょう。
一対一の接続
たとえば、Photo
がUser
によって投稿される場合は、Photo
側に「投稿者を示すフィールド」を持たせればOKです。
type Photo {
...
postedBy: User!
}
ここでのUser!
は、必ずユーザーオブジェクトが存在するという意味になります。
一対多の接続
User
から見て、1人のユーザーが複数の写真を投稿しうるなら、フィールドをリストにすれば一対多を表現できます。
type User {
githubLogin: ID!
name: String
avatar: String
postedPhotos: [Photo!]!
}
このように双方向の接続を定義しておくと、任意のノードから関連データをたどれる無向グラフが実現できるため、クライアントの自由度が上がります。
多対多の接続
「写真に複数のユーザーがタグ付けされ、ユーザーも複数の写真にタグ付けされる」といった多対多の場合は、双方にリストフィールドを定義します。
type User {
...
inPhotos: [Photo!]!
}
type Photo {
...
taggedUsers: [User!]!
}
多対多の接続自体に「何かしら情報(いつタグ付けされたか等)」を付与したければ、スルー型という中間オブジェクト型を作る方法が考えられます。
4.3 フィールドへの引数
GraphQLの強みの一つとして、どのフィールドにも引数を定義できる点が挙げられます。
特定の1件を取得する例
User
やPhoto
を特定のIDで問い合わせたいときは、フィールドにID用の引数を設定します。
type Query {
User(githubLogin: ID!): User!
Photo(id: ID!): Photo!
}
クエリ例:
query {
User(githubLogin: "MoonTahoe") {
name
avatar
}
}
フィルタリング・ページング・ソート
リストを返すフィールドに対して、以下のような引数を追加すると便利です。
-
カテゴリなどで絞り込む:
category: PhotoCategory
-
ページング:
first
やstart
引数を使って取得数や開始位置を指定 - ソート: 昇順か降順か、どのフィールドでソートするかをenumで指定
例として、allPhotos
クエリにソート引数を足す場合は以下のように書けます。
enum SortDirection {
ASCENDING
DESCENDING
}
enum SortablePhotoField {
name
description
category
created
}
type Query {
allPhotos(
sort: SortDirection = DESCENDING
sortBy: SortablePhotoField = created
): [Photo!]!
}
4.4 ミューテーション
Mutation型
クエリ(読み取り)とは異なり、アプリケーションの状態を変更する操作はミューテーションとして定義します。
写真共有アプリでは「写真を投稿する」「GitHubでサインインする」「写真にタグを付ける」などが該当します。
type Mutation {
postPhoto(
name: String!
description: String
category: PhotoCategory = PORTRAIT
): Photo!
}
schema {
query: Query
mutation: Mutation
}
たとえば上記のpostPhoto
では、
- 引数として
name
やdescription
、category
(デフォルトPORTRAIT)などを受け取る - それをもとに新しい写真オブジェクトが生成され、投稿処理が行われる
- 結果として投稿された
Photo
オブジェクト(新たなIDやURLなど)を返す
という流れになります。
入力型(Input Type)
引数が多くなってきた場合は、入力型を用いるのがおすすめです。
input PostPhotoInput {
name: String!
description: String
category: PhotoCategory = PORTRAIT
}
type Mutation {
postPhoto(input: PostPhotoInput!): Photo!
}
これにより、クエリ変数としてオブジェクトをまとめて渡せるため、実装時の可読性が上がります。
4.5 入力型の活用
入力型はクエリやミューテーションで使う引数を整理し、再利用しやすくする仕組みです。
たとえば、写真のフィルタリングに関連する項目をひとまとめにしたPhotoFilter
、ページングに関するDataPage
、ソートに関するDataSort
などを作っておけば、多数のフィールドで使い回すことができます。
graphql
コピーする編集する
input PhotoFilter {
category: PhotoCategory
createdBetween: DateRange
taggedUsers: [ID!]
searchText: String
}
input DateRange {
start: DateTime!
end: DateTime!
}
これらを用いて、allPhotos
やpostedPhotos
などへ引数として与えれば、複雑な検索にも対応しやすくなります。
4.6 返却型
GraphQLのスキーマ上では、フィールドは通常User
やPhoto
などの主要オブジェクト型を返しますが、追加のメタ情報を一緒に返したいケースがあります。
たとえば、認証を行った際に「ユーザーオブジェクトだけでなく、トークン文字列もまとめて返したい」などです。そういった場合、専用の返却用カスタムオブジェクトを用意します。
graphql
コピーする編集する
type AuthPayload {
user: User!
token: String!
}
type Mutation {
githubAuth(code: String!): AuthPayload!
}
これにより、GitHub認証の処理で成功した場合は、認証済みユーザー情報と認可トークンを同時に取得できます。
4.7 サブスクリプション
リアルタイム通信
GraphQLでは、リアルタイムでの更新を受け取る機能としてサブスクリプションを定義できます。
新しい写真が投稿された場合や、新しいユーザーが登録された場合など、特定のイベントに応じてクライアントへプッシュ配信する仕組みです。
graphql
コピーする編集する
type Subscription {
newPhoto: Photo!
newUser: User!
}
schema {
query: Query
mutation: Mutation
subscription: Subscription
}
これをクライアント側から購読(subscribe)すると、該当するイベントが起こるたびにサーバーからデータを受け取れます。フィルタリングが必要な場合は、category
などの引数を付けることもできます。
4.8 スキーマのドキュメント化
GraphQLにはイントロスペクション機能があり、さらにスキーマ定義に対してトリプルクォート(""" ... """
)でコメントを書くと、そのままドキュメントとして表示できるメリットがあります。
graphql
コピーする編集する
"""
最低一度はGitHubで認可されたユーザー
"""
type User {
"""
ユーザーの一意のGitHubログインID
"""
githubLogin: ID!
...
}
- 型全体やフィールド, 引数, 入力型など、任意の部分にコメントを追加可能
- GraphiQLやGraphQL Playgroundでのスキーマ閲覧画面に、これらのコメントがそのまま説明として表示される
ドキュメントを別途用意しなくても、自動で分かりやすいAPIリファレンスが生成されるのは大きな利点としてあげられます。
まとめ
GraphQLプロジェクトでは、優れたスキーマを設計することが最重要課題のひとつと言えます。質の高いスキーマはフロントエンドとバックエンドの共通言語・ロードマップとして機能し、開発チームが常に同じ指針を共有できるようにする(らしいです)