🧱

GraphQL設計入門してみた

2024/05/29に公開

ゼロからGraphQLのスキーマ設計をしたことがなかったので、調べた内容をアウトプットしておきます。以下の参考資料から、気になったポイントを抽出したものです。

参考:
Shopify GraphQL Design Tutorial
GraphQLスキーマ設計の勘所

実装の詳細を含めない

Productと、その集合のCollectionというエンティティを考えます。両者がM対Nの関係をもつとき、RDBでは CollectionMembershipといった中間テーブルを利用しますが、これは実装詳細であり、GraphQLスキーマに含めるべきではありません。

Bad
interface Collection {
  Image
  [CollectionMembership]
}

type ManualCollection implements Collection {
  Image
  [CollectionMembership]
}

type CollectionMembership {
  Collection
  Product
}

単純に、ProductとCollectionの関係を表現します。

Good
interface Collection {
  Image
  [Product]
}

type ManualCollection implements Collection {
  Image
  [Product]
}

また、上では省略していましたが、DBモデルのプロパティとして存在するフィールドも、GraphQLスキーマとして公開するかどうかはユースケースによって検討します。

Nodeの利用

一般的によく利用されるインターフェース。GraphQLクライアントに対して、そのオブジェクトが永続化されておりIDを用いて特定できることを知らせるヒントになります。

GraphQLクライアントはIDを用いてデータをキャッシュするため、主要なビジネスオブジェクトはNodeインターフェースを実装するべきです。

Node
interface Node {
  id: ID!
}

type Collection implements Node {
  id: ID!
}

IDよりオブジェクトの参照を直接含める

REST APIでは、オブジェクトの関連を示す方法としてほかのオブジェクトのIDをレスポンスに含めるのが一般的ですが、ネットワークリクエストが複数必要になるデメリットがあります。GraphQLではIDではなくオブジェクトの参照を直接レスポンスに含めます。

クライアントが明示的にそのフィールドを要求した場合のみサーバー側で処理が実行されるため、無駄な処理が発生することはありません。

type Collection implements Node {
  id: ID!
  title: String!
  image: Image
}

type Image {
  id: ID!
}

ビジネスロジックをフィールドで表現する

たとえばクライアントが「ある商品があるコレクションに所属しているか知りたい」場合、それを判定する専用のフィールドを用意します。

type Collection implements Node {
  # ...
  hasProduct(id: ID!): Boolean!
}

クライアント側でCollectionに含まれるProductを探索すれば同様の結果を得ることはできますが、「サーバーが唯一の正しい情報源であるべき」という思想に基づいています。

CRUD動詞の命名を避ける

REST APIに対するGraphQLのメリットの1つは、こちらの記事にもあるように、CRUD(HTTPメソッド)にとらわれずにAPIを命名できることです。

ユースケースごとに分解し、それぞれの操作に適した命名をします。
例:updateCollectionpublishCollection, unpublishCollection
例:createCollectionaddCollection, registerCollection

また、GraphQLのMutationを整理する手段として、対象となる型名を接頭辞とするワークアラウンドがあります(自然な命名である<action>Collectionをあえてcollection<action>とする)。

type Mutation {
  collectionDelete(collectionId: ID!)
  collectionPublish(collectionId: ID!)
  collectionUnpublish(collectionId: ID!)
  collectionAddProducts(collectionId: ID!, productIds: [ID!]!)
  collectionRemoveProducts(collectionId: ID!, productIds: [ID!]!)
  collectionCreate(title: String!, ruleSet: CollectionRuleSetInput, image: ImageInput, description: HTML!)
}

Mutationの出力はPayloadをつかう

Mutationは成功と失敗が存在するため、両者をラップしたPayloadを定義します。これには、IFの変更があったときに影響範囲を小さくするメリットもあります。

type CollectionCreatePayload {
  userErrors: [UserError!]!
  collection: Collection
}

type UserError {
  message: String!
  field: [String!]
}

考えたこと

REST APIでの一般的な設計がアンチパターンになることもあり、GraphQLの特徴を理解してスキーマ設計をすることが大切だと感じました。とくにGraphQLスキーマはサーバー・クライアントの両方に影響があるので、初期設計でミスると大きな負債になりそうです。

GraphQLの開発はコードファーストとスキーマファーストの2パターンがあります。どちらのアプローチも一長一短あることは確かなのですが、スキーマ設計という観点で考えると、コードファーストは純粋にGraphQLスキーマを検討しづらい印象を受けました。

現在私はPothosとPrismaを利用してGraphQLサーバーを構築していますが、PothosはGraphQLスキーマを明示的に出力しないこともあり、どうしてもDBモデルに引っ張られる傾向が強いように感じています(習熟度の問題かもしれません)。このあたり、良いアプローチをご存知の方がいらっしゃいましたら、ぜひ教えていただけると嬉しいです。

Discussion