WEB+DB PRESS Vol.125のGraphQL備忘録
概要
WEB+DB PRESS Vol.125のGraphQL章の備忘録
GraphQLの概要
- Facebook社が作ったWeb APIの仕様
- RESTでのツラミを解決したかった
- 1度に複数のリソースを取得する
- オーバーフェッチングやアンダーフェッチングしない(必要以上なデータをフェッチしない。その反対も)
- RESTでのツラミを解決したかった
- GitHubやTwitter、Netflix、Shopify、はてななど多くのテック企業での利用実績あり
- GraphQL自体はFB社からGraphQL Funcationに移管された(2018年)
- RelayはFB社が作成したReactやReact Native向けのクライアントライブラリ
- クライアントが使いやすいようにGraphQLのスキーマに一部制約を導入しているのが特徴
- interface Node導入するなどのアレ
- クライアントが使いやすいようにGraphQLのスキーマに一部制約を導入しているのが特徴
- GraphQL over HTTP
- 本来HTTP以外のプロトコルでも利用できるが普及している大半がHTTP
- 標準化に向けて準備中
- レスポンスにdataとerrorを含めるルール
- リクエストの単位は「オペレーション」と呼ぶ
オペレーション
-
種類は3つ
- Query
- Mutation
- Subscription
-
スキーマの書き方
- オペレーション毎に以下のように書くのが一般的
QueryやMuatation、Subscriptionはデフォルト型名なので省略可能とのこと# QeuryやMuationの型をルートオペレーション型という schema { query: Query mutation: Mutation } # Query型を定義して対応する中身を書く type Query { posts: [Post!]! post(id: ID!): Post! }
- オペレーション毎に以下のように書くのが一般的
-
クライアント側からのQueryオペレーションの例(オペレーション構文)
基本の構文は種別 名前(引数) セレクションセット
# 詳細 query getPost($id: ID!) { post(id: $id) { title body } } # 一覧 query { posts { title } }
ディレクティブ
@include、@skip、@deprecatedがある動的に振る舞いを変えたいときに利用する
例えば、フラグによって記事のタイトルのみ or タイトル+本文を切り替えたいとき
query listPosts($isBody: boolean!) {
posts {
title
body @include(if: $isBody)
}
}
サーバー側
RESTの以下のツラミをGraphQLでは解決できる
- クライアントに特化させたAPIを作らないといけなかったりする(メンテナンス性低い)
- 1つ画面でAPIを何度もリクエストしないといけなかったりする
GraphQLフェデレーション
マイクロサービスのAPI Gatewayパターンと相性が良い
要は複数のAPIを束ねる技術
ドメインごとのチームで並列でAPI開発できるので効率化できる
GraphQL Federationという仕様書が公開されている
サーバーとスキーマとリゾルバの関係
多くのサーバーサイド用のライブラリがあるがGraphQL.jsを参考にしたものが大半
ライブラリ(GraphQLサーバー)から呼び出される関数のことを「リゾルバ」という
リゾルバはスキーマで定義されている個々のフィールドの値を解決する役割を担う
スキーマとリゾルバの関連性の管理
コードファースト・スキーマファーストの2つのアプローチがある
種別名 | 例 |
---|---|
コードファースト | GraphQL.jsやNexus |
スキーマファースト | Apollo Server |
# コードファースト: スキーマとリゾルバ一体化
query: new GraphQLObjectType({
name: 'Query',
fields: {
hello: {
type: GraphQLString,
resolve: () => 'hello world',
}
}
})
# スキーマファースト:スキーマとリゾルバが分離
const typeDefs = gql`
type Query {
hello: String
}
`;
const resolvers = { # リゾルバがスキーマ型と一致しているか保証されてないのでフォローが必要(GraphQL Code Generatorなど)
Query: {
hello: () => 'hello world',
}
};
const server = new ApolloServer({
typeDefs,
resolvers,
});
N+1問題対策
データローダーという仕組みを導入する
1つ1つのデータの取得を 遅延評価 して一括で処理できるようにするもの
dataloaderパッケージを追加してリゾルバを跨いで共有できるcontextにdataloaderインスタンを保存しておくのが良い
prismaはDataloader対応が良しなにされている様子(参考)
実践パターン
Relayの追加仕様で参考になるもの
Viewerパターン
TBD
Mutationの設計
- セマンティクスに合わせて複数のミューテーションに分割をおすすめ
- 記事公開はpublishPostにする(updatePostとは別にする)
-
戻り値の設計
- クライアントのキャッシュを意識した設計にするためのもの
エラー表現
基本はレスポンスにerorrsフィールドを持ち、中身のフィールドは以下
- message
- locations
- paths
GraphQLはエラーのフィールド拡張を認めている
extensionsフィールドを追加して、httpのステータスコードなどを持たせるのが良い
{
"errors": {
"message": "登録が失敗しました",
"locations": [{"line": 2, "column": 3}],
"path": ["node"],
"extensions": {
"code": "NOT_FOUND"
}
}
}
ミューテーションの入力に対するバリデーション結果などはスキーマ自体にエラーオブジェクトを組み込むのが良い
type Mutation {
createPost(
blogId: ID!
input: PostInput!
): CreatePostPayload!
}
type CreatePostPayload {
userErrors: [UserError!]!
post: Post
}
type UserError {
message: String!
field: [String!]
}
APIのバージョニング
RESTのように明示的にバージョニングする必要はない
フィールドを失効させたい場合は@deprecatedディレクティブを使ってクライアントに教えてあげれば良い
GraphQLの運用
クエリのコスト制限
ページネーションで一度に取得できる件数を制限する
クエリの深さにも制約をかける
クエリのコストを計算し一定の範囲内に収まるようにする
GraphQLのライブラリの一部にはクエリコストに対するヘルパー用意しているものがあるので確認する
なぜ気にしないといけないかというと、 以下のように最初のブログ記事を100件取得し
そのブログを介して記事を100件取得となると10000万件のデータを取得するクエリを簡単にかけてしまい凄い負荷がかかるから
query {
blog(id: "abc") {
posts(first: 100) {
edges {
node {
blog {
posts(first: 100) {
}
}
}
}
}
}
}
Persisted Query
予め登録されたクエリのみ許可するリストを作っておく
事前に検証されたクエリしか実行されないので上記のような負荷のかかるクエリの発行を防げる
RelayやApolloなどはPersisted Queryをサポートしている
トレーシング
GraphQLはエンドポイントが1つなので、どのオペレーションが負荷かかっているかが観測できない
AWSならばX-Ray、Google Cloudの場合はCloud Trace。Apollo使っているのであればApollo Tracingがある
Discussion