GraphQL設計入門してみた
ゼロからGraphQLのスキーマ設計をしたことがなかったので、調べた内容をアウトプットしておきます。以下の参考資料から、気になったポイントを抽出したものです。
参考:
Shopify GraphQL Design Tutorial
GraphQLスキーマ設計の勘所
実装の詳細を含めない
Productと、その集合のCollectionというエンティティを考えます。両者がM対Nの関係をもつとき、RDBでは CollectionMembership
といった中間テーブルを利用しますが、これは実装詳細であり、GraphQLスキーマに含めるべきではありません。
interface Collection {
Image
[CollectionMembership]
}
type ManualCollection implements Collection {
Image
[CollectionMembership]
}
type CollectionMembership {
Collection
Product
}
単純に、ProductとCollectionの関係を表現します。
interface Collection {
Image
[Product]
}
type ManualCollection implements Collection {
Image
[Product]
}
また、上では省略していましたが、DBモデルのプロパティとして存在するフィールドも、GraphQLスキーマとして公開するかどうかはユースケースによって検討します。
Nodeの利用
一般的によく利用されるインターフェース。GraphQLクライアントに対して、そのオブジェクトが永続化されておりIDを用いて特定できることを知らせるヒントになります。
GraphQLクライアントはIDを用いてデータをキャッシュするため、主要なビジネスオブジェクトは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を命名できることです。
ユースケースごとに分解し、それぞれの操作に適した命名をします。
例:updateCollection
→ publishCollection
, unpublishCollection
例:createCollection
→ addCollection
, 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