Web系エンジニアのアウトプット練習場

エンジニアリングと書評が中心。たまに全然関係無い話もします。

GraphQLでマイクロサービスアーキテクチャを構築する際に有効なApollo Federationを採用する際に注意すべきこと

Apollo Federationとは?

過去記事にApollo Federation(以下、単にFederationとも)の紹介記事があるので、Federationが何かご存じない方は御覧ください!

blog.h-sakano.dev

Apollo Federationを採用する際の注意点

上記の記事のように、Apollo FederationはGraphQLでマイクロサービスを構築する際にとても有効な選択肢のうちの1つですが、採用にあたって注意すべき点もあります。

それはずばり、「extendして追加したフィールドに引数を指定することはできない」ということです。

ちょっとこれだけではわかりにくいと思うので、以下のような構成のブログサービスの例で考えてみましょう。

ブログサービスの例

例として使用するブログサービスの構成
ブログサービスの構成

ユーザー管理サービスとブログ管理サービスがそれぞれ定義したスキーマをFederation Gatewayで統合してユーザーに単一のGraphQL APIを提供します。

ユーザー管理サービスのスキーマ定義

まずは、ユーザー管理サービスから、ユーザー情報をGraphQLで取り扱うことができるように、以下のようなスキーマを定義します

extend type Query {
  users: [User]
  user(id: ID!): User
}

type User {
  id: ID!
  email: String!
  firstName: String!
  lastName: String!
  nickname: String
}

このスキーマに対してリゾルバを定義すると、ユーザー情報の一覧や、idを指定することで特定のユーザー情報を取得することができるようになるでしょう。

記事管理サービスのスキーマ定義

そして同様に、記事管理サービスにも記事に関するスキーマを定義します。

extend type Query {
  posts: [Post]
  post(id: ID!): Post
}

type Post {
  id: ID!
  title: String!
  body: String!
  authorId: ID!
}

サービス間のデータ参照

さて、ここでユーザーごとに記事一覧を取得できるようになれば便利です。

記事の管理は記事管理サービスの責務ですから、記事管理サービスでUserタイプを拡張します。

extend type User @key(fields: "id") {
  id: ID! @external
  posts: [Post]
}

問題が起きるのはここからです。

ユーザー1人あたりの記事の投稿数が多い場合、ページネーションを実装したり、記事の並び順を変更したりするため、上記のpostsフィールドに以下のように引数を取ることができれば便利です。

例えば、以下のような感じです。

extend type User @key(fields: "id") {
  id: ID! @external
  posts(after: String, before: String, first: Int, last: Int, order: String, orderBy: String): [Post]
}

ただ、上記のように、Apollo Federationを利用してextendで追加したフィールドに引数を指定することは現状はできません。

ソース: Federation relation filtering and arguments problems (federation design concerns / feature request) · Issue #3420 · apollographql/apollo-server · GitHub

2つの妥協策

妥協策としては、2通り考えられます。

フィールドを分ける

ソート順程度のパターン数がある程度限られているものであれば、以下のようにソート順ごとに別のフィールドを用意することもできるでしょう。

extend type User @key(fields: "id") {
  id: ID! @external
  postsOrderedById: [Post]
  postsOrderedByTitle: [Post]
}

柔軟性には欠けますが、引数の組み合わせに限りがある場合は有効な手ではあります。

extendを諦める

ただ、ページネーションのようにパターン数が膨大な場合は、extendしてフィールドを追加することを諦めて、別の方法でユーザーごとの記事一覧を取得するしかありません。

具体的には、ルートクエリに定義したpostsフィールドでユーザー検索ができるように、author引数を指定します。

extend type Query {
  # authorで特定ユーザーによる記事を検索できるようにする
  posts(after: String, before: String, first: Int, last: Int, author: ID!): [Post]
}

この場合、複数ユーザーの記事一覧を1回のクエリで取得できず、ユーザーごとにAPIを叩く必要があります。

また、以下のようにauthorsを引数で指定できるようにし、複数ユーザーの記事を一気に取ってきて、フロント側で表示を出し分けするという方法も考えられるかもしれません。

extend type Query {
  # authorsで特定ユーザーによる記事を検索できるようにする
  posts(after: String, before: String, first: Int, last: Int, authosr: [ID!]): [Post]
}

まとめ

以下の条件ならびに妥協策を許容できない場合、現状はApollo Federationを採用することはできませんのでご注意ください。

  • extendして追加したフィールドに引数を指定することはできない
  • 妥協策1: 引数の組み合わせが少ない場合は、組み合わせごとにフィールドを定義する
  • 妥協策2: extendを諦めて、ルートクエリで検索できるようにする