前書き
DMMグループ Advent Calendar 2020 6日目の記事です。
GraphQLは昨今、日の目を見る機会も増え "導入してみようかしら?"
と検討したことがある方もちらほら居られることかと思います。
GraphQLについてはまるで銀の弾丸かのようにメリットばかり着目され、
GraphQL関連の失敗や間違いについては教科書どおりのことしか語られることが少ないので、
やってみた後に "こんなはずじゃなかった!", "もっと考慮しておけばよかった!"
...とならないために、
GraphQLをやってて
- 見たことある
- 聞いたことある
- やったことある
- 勘違いしたことある
ようなあるある
(ないないも含む) を雑に紹介します。
(※多少大げさに書いている箇所がございますのでご注意ください)
GraphQLあるある
GraphQLの あるある
を下記の3つの区分で紹介していきます。
- スキーマ編
- バージョン管理・アップデート編
- パフォーマンス編
スキーマ編
1. GraphQLのスキーマファースト開発ではなく、DBスキーマファースト開発になる
GraphQLを用いる場合、多くの場合はスキーマファーストで設計していきます。
スキーマファーストの開発では、全てのチームメンバーが
データ型を共通言語にしてコミュニケーションを取ることができます。
しかしごく稀に次のようなケースが起きます。
謎の人< このSQLの結果が取得できるクエリがほしいんですが...(ry
私 < 🤔
2. フィールド名がDBと1対1になってよくわからないフィールドが生える (○○_id)
GraphQLのオブジェクト型のフィールド名はDBのテーブルのフィールド名と一致する必要はありません
また、不要なフィールドを公開する必要もありません。
よく見る形
{
user {
name
home {
id
country
prefecture
city
}
}
}
悩ましい形
{
human {
name
home_id
}
}
3. GraphQLのオブジェクト型にせず、フラットに大量のフィールドが生える。
DBのUserテーブルにその人の住んでいる場所に関わる情報が入っていたとします。
仮にそれが country, prefecture, city という全て住居に関する情報の場合、
Homeというオブジェクトにまとめてしまうこともできます。
特に1まとまりにできる場合や再利用する可能性がある場合はまとめてしまうほうが良い時があります。
例.
よく見る形
{
user {
name
home {
country
prefecture
city
}
}
}
悩ましい形
{
user {
name
country
prefecture
city
}
}
4. GraphQLならフィールドいらなければよばなければいいよね! という発想で無駄フィールドが生える。
GraphQLはオーバーフェッチング (データの過剰取得) を避けることができると考えられています。
とはいえ、全てのフィールドを生やす必要があるでしょうか? きっとありません。
例.
よく見る形
{
user {
name
home {
id
country
prefecture
city
}
}
}
悩ましい形
{
user {
home {
id
country
prefecture
city
}
deleted_at
}
}
5. REST likeなAPIのようにURLが生えまくることはないが、悩ましいクエリが大量に生える。
GraphQLはREST likeなAPIと比較して、ユースケースの数だけURLが生えまくるような事はありません。
ですが、悩ましいクエリは生えまくることはあります。
雑に例を出すと、
user情報の中から管理者を除いた情報がほしい場合があるかもしれません、
また、その日に作成したユーザーのみほしいかもしれません。
また、特定の条件のときのみ任意の引数を必須にしたいかもしれません。
それらの多くのユースケースの場合がでてきた時に、
userクエリの引数に追加していく方法もありますが、クエリを分けてしまう場合もあります。
スキーマを管理する人や部署がしっかり管理していないと、
すごい複雑な引数のクエリができあがったり、似たような違うクエリが大量に生えることはありえます。
例.
- user
- userExculdAdmin
- userCretedToday
6. オリジナルのカスタムスカラーをつくり、辛い思いをする (この型何が入るの…?)
GraphQLではスカラー型を、任意に作ることができ、これらはカスタムスカラーとも呼ばれます。
ですが多くのクライアントではカスタムスカラーを自動で解釈する術を持っておりません。
つまり、明らかに自明である型や極端にわかりやすい型を除いて、
カスタムスカラーは型の良さであるバリデーションがクライアント側では効かせづらい場面が起こりえます。
例えば、URLやJSONはある程度バリデーションの方法に共通の概念を持てます。
ではPasswordはどうでしょうか? Passwordの基本的なバリデーション...?
最悪の場合、これはString型であること以上のバリデーションを期待できません。
ある程度自明な形
- URL
- JSON
悩ましい形
- Password
- OfficialId
7. 実はGraphQLが必要なかった。
下記のようなケースで、GraphQLの良さによる恩恵を教授しにくい場合、
正直なところCRUD形式のREST likeなAPIで十分な時があります。
例.
-
オブジェクト間がGraph構造のようになんらかの繋がりをほぼ持っていない場合。
-
操作すべきオブジェクトが2~3しかなく、1つのオブジェクトのフィールドも5つとかである場合。
-
セキュア情報をやり取りしたいけど、キャッシュをバキバキに効かせたい場合
(GraphQLはREST likeなAPIと比較するとキャッシュしづらいです。GETでやる方法もありますが…) -
リクエストの送信元がスペックが十分にあり(サーバー間通信目的など)、
なおかつAPIを呼ぶ頻度も十分に少ないような場合。
バージョン管理・アップデート編
8. フィールドの削除はすぐできません。既存クライアントが死んでしまいます。
Q. GraphQLはフィールドすぐ消せるんでしょ?
A. 消せません、既存のクライアントが死んでしまうことがあります。
@deprecated
にして、呼ばれない期間を一定期間確認した後に消すことをおすすめします。
Q. GraphQLってdeprecatedあるんでしょ? それならクライアント呼ばなくなるよね?
A. @deprecated
として非推奨にすることはできますが、クライアントが呼ばない事を完全に強制することはできません。
9. 必須の引数の追加はすぐできません。既存クライアント死んでしまいます。
Q. ○○クエリに必須の引数を追加したいんだけど、GraphQLなら良いよね?
A. 必須の引数は基本的にすぐに追加することはできません。
既存のクライアントが漏れなく全てエラーを引く可能性があります。
10. GraphQLはフィールドを指定して呼び出すので、不要になったらすぐ気付けるという思い込み
GraphQLはフィールドを指定してているので、呼ばれなくなった時に不要なフィールドはたしかにある程度わかりやすいです。
ただし、優先度や、緊急性の類いでリファクタリングタスクの多くは後に回されることがあります。
よって、全てのクライアントが不要になったら即時に該当のフィールドを削除するわけではありません。
つまり、フィールドが呼ばれなったというログなどの結果で、フィールドが不要だと判断することはできますが、
不要だが呼び出し続けるクライアントがいる以上、クライアントの要望が不要になったタイミングを気づくことは難しいです。
11. バージョン管理不要?
GraphQLはバージョン管理が不要だという方も居られるかと思います。
GraphQLは新しい型とその新しい型のフィールドによって新規機能の追加などはしやすく、
基礎の設計がある程度しっかりしていると、ある程度バージョン管理は不要な時もあります。
ですが、上記の1,2,3 のケースを初め、GraphQLやスキーマの設計に関して知見が集まっていない間は、
大きなスキーマの変更が起こりうるので、バージョン管理をしたほうが良い場合もあると思っています。
パフォーマンス編
12. GraphQL = N+1問題を引く
GraphQLは型やフィールドに対して、その値を解決するための関数を指定することが多いのですが、
それぞれの関数でそれぞれDBやAPIにリクエストを送ってしまうことで、
N+1問題を含む、多くのパフォーマンス問題を引き起こすことがあります。
ですが、 どうあがいてもN+1問題を必ず引くわけではありません。
FacebookのDataLoaderのようなツールを用いたり、
任意のキャッシュやバッチの仕組みを実装することで、ある程度は問題を緩和することができます。
参考: graphql/dataloader https://github.com/graphql/dataloader
13. リクエストのサイズを小さくしたかったはずが、traceの結果をextensionに載せてレスポンスが肥大化
GraphQLはリクエストのURLベースではなく、指定されたフィールドで処理が決まります。
その結果、URLベースでのパフォーマンスのモニタリングができません。
そのような際には、
GraphQLサーバーのリゾルバで一意なリクエストのIDに紐付いて各処理の時間をログに吐き出したり、
GraphQLのレスポンスの extensions
の中に処理時間などを記載して返すことなどが考えられます。
後者の場合の例を1つ見てみましょう。これは apollo tracingの例です、
{
"data": <>,
"errors": <>,
"extensions": {
"tracing": {
"version": 1,
"startTime": <>,
"endTime": <>,
"duration": <>,
"parsing": {
"startOffset": <>,
"duration": <>,
},
"validation": {
"startOffset": <>,
"duration": <>,
},
"execution": {
"resolvers": [
{
"path": [<>, ...],
"parentType": <>,
"fieldName": <>,
"returnType": <>,
"startOffset": <>,
"duration": <>,
},
...
]
}
}
}
}
ご覧のように、レスポンスが肥大化してしまうことがあります。
ただし、このレスポンスの肥大化もHTTPレスポンスボディを圧縮することで、ある程度緩和することができます。
参考: apollographql/apollo-tracing https://github.com/apollographql/apollo-tracing
14. GraphQLに変えたら遅くなった! DBにクエリ送ったほうが早いんだけれど!
管理画面のシステムがDBなどを除いて1つのサーバーで完結されている場合に、
それを管理画面とGraphQLのAPIに切り分けるようなケースをよくお聞きします。
このような場合に、
謎の人 < 元の完結なシステムと比較して遅くなった!
私 < 🤔
負荷がさしてかかっていない状態で、速度面だけを気にする場合、
基本的にDBに1クエリを投げる処理よりは、GraphQLのAPIのリクエストのほうが遅くなることが多いと思います。
このような時は、GraphQLのメリットとトレードオフで考える必要があります。
15. パフォーマンスに影響する悪意あるクエリを止めづらい
GraphQLはパフォーマンスに影響を与える悪意あるクエリを止めるために、
- クエリの深さ制限
- クエリの複雑さの制限 (すべてのフィールドスコアの合計など)
- リクエスト自体のタイムアウトによる制限
などが一般的によく設定されます。
ですが、フィールドの深さ制限も複雑さの制限もどちらも
各フィールドがどれくらいパフォーマンスに影響があるのか(大きいのか、小さいのか)を区別していません。
また、これらの制限を小さく設定しすぎると イントロスペクションのクエリもエラーになってしまいます。
後書き
ここまで、多少大げさにGraphQLあるあるを紹介してきました。
これらの多くはコンテキスト次第なのでやむ終えないケースもあると思います。
また、実際どうあるべき? どう考えるべき? という所は書ききれず、
ざっくりの紹介にとどまってしまってはいることを平にご容赦ください。
(どこかの勉強会やLT会などでお会いした時にぜひ意見交換させてください)
"そんなことする(考える)わけないだろー"
と思う方も多々居られるとは思いますが、
これからGraphQLを採用しようという方に少しでも参考になれば幸いです。
DMMグループ Advent Calendar 2020 7日目は @mesh1nek0x0 さんです