0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GraphQLスキーマ設計とスキーマファーストの思想

Posted at

はじめに

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型はユニークな文字列を表します。
  • nameurlは必須なので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!
}

これにより、categorySELFIEPORTRAITACTIONなどの決まった値からのみ選ばれることが保証されます。


4.2 接続(リレーション)とリスト

リスト型とnull制約

GraphQLでは、[String]のように角括弧を用いて型のリストを表せます。

また、!を付ける場所で「リスト全体がnullにならない」「要素がnullにならない」という制約を設定できます。よほどの理由がない限り、実用上は[Type!]!のように「リスト自体も、要素もnull不可」の指定をすることが多いでしょう。

一対一の接続

たとえば、PhotoUserによって投稿される場合は、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件を取得する例

UserPhotoを特定のIDで問い合わせたいときは、フィールドにID用の引数を設定します。


type Query {
  User(githubLogin: ID!): User!
  Photo(id: ID!): Photo!
}

クエリ例:


query {
  User(githubLogin: "MoonTahoe") {
    name
    avatar
  }
}

フィルタリング・ページング・ソート

リストを返すフィールドに対して、以下のような引数を追加すると便利です。

  • カテゴリなどで絞り込む: category: PhotoCategory
  • ページング: firststart引数を使って取得数や開始位置を指定
  • ソート: 昇順か降順か、どのフィールドでソートするかを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では、

  • 引数としてnamedescriptioncategory(デフォルト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!
}

これらを用いて、allPhotospostedPhotosなどへ引数として与えれば、複雑な検索にも対応しやすくなります。


4.6 返却型

GraphQLのスキーマ上では、フィールドは通常UserPhotoなどの主要オブジェクト型を返しますが、追加のメタ情報を一緒に返したいケースがあります。

たとえば、認証を行った際に「ユーザーオブジェクトだけでなく、トークン文字列もまとめて返したい」などです。そういった場合、専用の返却用カスタムオブジェクトを用意します。

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プロジェクトでは、優れたスキーマを設計することが最重要課題のひとつと言えます。質の高いスキーマはフロントエンドとバックエンドの共通言語・ロードマップとして機能し、開発チームが常に同じ指針を共有できるようにする(らしいです)

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?