こんにちは。FLINTERS エンジニアの山下です。この記事はFLINTERS設立10周年ブログリレーの108日目の記事になります。 今回は弊チームで利用しているGraphQLを「Production Ready GraphQL」と照らし合わせて振り返っていこうと思います。 book.productionreadygraphql.com
はじめに
弊チームではWebアプリにGraphQLを利用しています。 しかし自分は前のチームではGraphQLは一切触っておらず、GraphQLについての知識はありませんでした。 そこでGraphQLへの理解を深めるためにGraphQLの概要や設計について書かれている著書「Production Ready GraphQL」を読んで時間が経ったので 本の復習を兼ねて、本の内容の要点と弊チームでの設計・実装を振り返っていこうと思います。 今回はスキーマ設計・サーバー実装について掘り下げていこうと思います。
スキーマ設計
本では、スキーマ設計について抑えるべきポイントとして以下の4つが挙げられていました。 (引用: Production Ready GraphQL p.83)
デザインファースト
First, use a design-first approach to schema development. Discuss design with teammates that know the domain best and ignore implementation details.
(いきなり実装を始めるのではなく、早い段階で設計を検討することが大切である。 )
「Design First」 (Production Ready GraphQL p. 27-28) の節で、実装を最初にやってしまうとスキーマの設計が実装に密接に結びついてしまう問題があるということが説明されていました。
クライアントファースト
Second, design in terms of client use cases. Don’t think in terms of data, types, or fields.
(クライアントのユースケースを念頭に置いて設計することが大切である。データ・タイプ・フィールドで考えてはいけない。 )
「Client First」(Production Ready GraphQL p. 28-29) の節で、クライアントがAPIから欲しいデータを取得する際に大量のドキュメントを読むのを避けることにつながること・そのためには設計の早い段階で「最初のクライアント」と協力することが重要であることが説明されていました。
表現豊かなスキーマ
Third, make your schema as expressive as possible. The schema should guide clients towards good usage. Documentation should be the icing on the cake.
(スキーマはクライアントにとって使い方が分かりやすいものになっているべきで、ドキュメントは補足程度になるべきである。)
あらゆるユースケースに対応するようなスキーマを作らない
Finally, avoid the temptation of a very generic and clever schema. Build specific fields and types that clearly answer client use cases.
( 最後に、非常に一般的で巧妙なスキーマの誘惑を避けよう。クライアントのユースケースに明確に答える特定のフィールドとタイプを構築しよう。)
振り返り(スキーマ設計)
上記の4つについて弊チームでの状況と照らし合わせていこうと思います。
1つ目の「デザインファースト」についてですが、ドメインモデリングを行ったのちにバックエンドの実装を行い、コードを元にスキーマを生成しました。(ライブラリは Caliban を利用)
ドメイン定義・Query/Mutation定義 -> usecase, repositoryをダミーで実装 -> 具体的な実装 の手順で実装しておりScalaコードでのドメインの定義の段階でスキーマを設計しているのに等しいため、デザインファーストと言えるかと思います。
2つ目の「クライアントファースト」についてですが、
クライアントを弊チームで開発している都合上、クライアントのユースケースを考慮しながらドメインモデリングを進めていたためクライアントのユースケースを念頭に置くことは無意識にできていました。
設計の早い段階で「最初のクライアント」と協力することも弊チームで開発しているクライアントなので自然にそうなっていました。
3つ目の「表現豊かなスキーマ」についてですが
挙げられる工夫としては、
Query名に関しては「データ名(s)」の名前にして特定のデータが取得できることを示し、
Mutation名に関しては「動詞 + データ名(s)」としています。
そのため、Mutationに関してはユースケースごとにmutationが実装されており、Queryに関してはデータごとにqueryが実装されています。
また、クライアント側で欲しい値については、バックエンドから得た値を元にクライアントで計算するのではなく、バックエンド側で予め計算するようにしています。
そうすることで、スキーマの変更がなされても、フロントエンド側で誤ったデータの使い方をしないようなスキーマになっています。
最後に4つ目の「あらゆるユースケースに対応するようなスキーマを作らない」ですが
前述したQueryやMutationに関する工夫に加えて
スキーマのフィールド名や型については、バックエンドの実装時にレビューをするので
不自然なものになりにくくなっています。もし、後から不自然だと感じたものに関してはリファクタリングを行なって修正します。
つぎにサーバー実装について振り返っていきます。
サーバー実装
サーバー実装の抑えるべき点としては以下の6つの点が挙げられていました。(引用: Production Ready GraphQL p.103)
コードファーストのフレームワークを選択しよう
Prefer code-first frameworks with high extendability (metadata, plugin and middlewares, etc)
(拡張性の高いコードファーストのフレームワーク (メタデータ、プラグイン、ミドルウェアなど) を選択しよう。)
GraphQL層は可能な限り薄く保つ
Keep your GraphQL layer as thin as possible, and refactor logic to its own domain layer if not already the case.
(GraphQL層は可能な限り薄く保ちましょう。そして、すでにそのようになっていない場合はドメイン層にひもづくロジックをリファクタリングしよう。)
「Resolver Design」 (Production Ready GraphQL p. 93-94) の節では「GraphQLはAPIインターフェースであり、ドメインやビジネスロジックへのインターフェースでもあるため、GraphQL自身がビジネスロジックを持つべきではない。」と説明されていました。
リゾルバをシンプルに
Keep resolvers as simple as possible, and don't rely on global mutable state.
(リゾルバを可能な限りシンプルに保ち、グローバルな変更可能な状態変数に依存しないようにしよう。)
具体的な例ではcontextオブジェクトに依存しすぎないこと・contextオブジェクトを不変に保つことが説明されていました。
モジュール化を実現しよう
Modularize when it starts hurting, you don't need a magical or specific framework. Use your programming language to achieve modularity.
(痛くなったらモジュール化しよう。魔法のようなあるいは特定のフレームワークは必要ない。プログラミング言語を用いてモジュール化を実現しよう。)
統合テストはGraphQLサーバーにとって最高のアプローチ
Test most of the domain logic at the domain layer. Integration tests are the best "bang for buck" approach for GraphQL servers.
(ドメインロジックのほとんどをドメインレイヤーでテストしよう。統合テストはGraphQLサーバーにとって最高の「費用対効果」のあるアプローチです。)
実行時の条件に基づく小さなスキーマの変種には可視性フィルターを使おう
Use visibility filters for small schema variations based on runtime conditions, but don't hesitate to build completely different servers at build time when dealing with wildly different schemas.
(実行時の条件に基づく小さなスキーマの変種には、可視性フィルター*1を使用しよう。しかし、大きく異なるスキーマを扱う場合はビルド時に全く異なるサーバーを構築することをためらわないようにしましょう。)
上記の6つについて弊チームでの状況と照らし合わせていこうと思います。
振り返り(サーバー実装)
1つ目の「コードファーストのフレームワークを選択しよう」についてですが チームで使用しているCalibanではWrapperというミドルウェアが使えて、クエリの処理に対してさまざまなレベルで追加アクションを実行することができ、この機能をプロジェクトでも利用しています。 GraphQLリクエストやレスポンスのエラーをログに出力したり、クエリの処理に時間がかかった場合、ログにかかった時間を出力するWrapperを自前で実装し利用しています。
2つ目の「GraphQL層は可能な限り薄く保つ」についてですが usecase層・domain層がビジネスロジックを持っています。GraphQL層にはAPI用のケースクラスとリゾルバが存在する構成なので薄く保たれていると思います。
3つ目の「リゾルバをシンプルに」についてですが リゾルバではusecase層のメソッドを呼び出してデータ取得を行っています。そしてAPI用のケースクラスに変換しています。それ以外の役割は担っていないのでシンプルな作りになっています。 また、contextオブジェクトは不変で利用しており、ほとんど認可の部分でのみ利用しています。
4つ目の「モジュール化を実現しよう」についてですが コードを元にスキーマを生成するコードファーストなアプローチをとってスキーマを生成しておりコードとスキーマの対応関係の把握が容易なため、我々のプロジェクトでは気にしなくても良さそうです。
5つ目の「統合テストはGraphQLサーバーにとって最高のアプローチ」についてですが ドメインロジックに関してはロジックが複雑なものはテストを用意しています。また、統合テストとして、PlayGroundで実行予定のクエリを実行しテストしたり、クライアントとセットで結合テストしています。
最後に6つ目の「実行時の条件に基づく小さなスキーマの変種には可視性フィルターを使おう」についてですがユーザの認可に応じてアクセスできるデータやアプリの機能に制限を施しており、新機能についてはGraphQLではない部分でフィーチャーフラグによる制御を行っているため、現状はマスキングしていません。
まとめ
以上が スキーマ定義・サーバー実装についての振り返りになります。書籍の一部分の内容との比較にはなりますがプロダクトのスキーマ定義・サーバー設計についての理解を深めることができました。
*1:特定条件下においてスキーマをマスキングするフィルター。特定のクライアントにのみ共有したいスキーマが存在したり、フィーチャーフラグによって新しい機能を公開する際に使うことが推奨される。