仕事でGraphQLを使うようになり、会社の人が紹介していたProduction Ready GraphQLという書籍を購入しました。
book.productionreadygraphql.com
普段こういう読書メモはNotionに雑に書くんですが、洋書ということもありしっかり読みつつ記事にしていくことで理解を深めていきたいと思っています。
目次
今回読んだ章
Preface, Ackowledgmentsは飛ばして、An Introduction to GraphQLの章から読んでいきます。
この章ではGraphQLの基本的な仕様について説明しています。
GraphQLの公式ガイドのQueries and Mutations, Schemas and Typesのページと内容が重複するところも多く、両者は交互に見ながら読み進めました。
読み進める
Hello World
まずは基本的なクエリの投げ方から。
クエリを投げるとよく似た構造でレスポンスが取得できる。data
というキーの値に取得した値が入るfriends
フィールドのfirst
のように、引数を取ることができる。
query { me { name friends(first: 2) { name age } } }
{ "data": { "me": { "name": "Marc", "friends": [{ "name": "Robert", "age": 30 }, { "name": "Andrew", "age": 40 }] } } }
型システム
型システムはGraphQLはスキーマとも呼ばれ、一般的にGraphQL Schema Definition Language(SDL)で表現される。
どの言語でGraphQL APIを実行していてもスキーマはSDLで表現されるため、言語に依存せずにスキーマを共有できる。
クライアントが参照するエントリポイントはQuery, Mutation, Subscription*1のいずれかで定義される。
GraphQLの定義済みスカラー型(String
など)か、ユーザー定義の型を使用できる。
スキーマの例:
type Shop { name: String! # Where the shop is located, null if online only. location: Location products: [Product!]! } type Location { address: String } type Product { name: String! price: Price! } type Query { shop(id: ID): Shop! }
引数
フィールドは引数を持つことができる。↑の例のshop
クエリもid
という引数を持っている。
引数は、定義済みスカラー型か入力型(Input Type)で定義される。(入力型は前章の型とは別物。input
キーワードで定義される)
type Product { price(format: PriceFormat): Int! } input PriceFormat { displayCents: Boolean! currency: String! }
変数
クエリ文字列と変数を一緒に送信し、GraphQLサーバー側で処理させることができる。変数を一緒に送信する場合はオペレーション名は省略できない。
query FetchProduct($id: ID!, $format: PriceFormat!) { product(id: $id) { price(format: $format) { name } } }
以下のように送信
{ "id": "abc", "format": { "displayCents": true, "currency": "USD" } }
エイリアス
エイリアスの例はGraphQL公式ドキュメントがわかりやすかったので引用。
{ empireHero: hero(episode: EMPIRE) { name } jediHero: hero(episode: JEDI) { name } }
{ "data": { "empireHero": { "name": "Luke Skywalker" }, "jediHero": { "name": "R2-D2" } } }
https://graphql.org/learn/queries/#aliases
hero型のオブジェクトを2件習得する際に、このようなエイリアスをつけることで両者を区別できる。
ミューテーション
ここまでの例はいずれもクエリでのデータ読み取り処理だった。
データの書き込み・変更にはミューテーションを使う。クエリをtype Query
配下に定義するように、ミューテーションのエントリーポイントはtype Mutation
配下に定義。
type Mutation { addProduct(name: String!, price: Price!): AddProductPayload } type AddProductPayload { product: Product! }
以下のようにmutation
キーワードをつけて呼び出す
mutation { addProduct(name: String!, price: Price!) { product { id } } }
ミューテーションには、クエリと違い以下のような特徴があります。
- ミューテーションは副作用を持ったり、修正を加えることが許される
- ミューテーションはサーバーによってシリアルに実行されなければならない。Query, Subscriptionは並列に実行することができる
Enum
いくつかの決まった値のいずれかを返すフィールドについて、Enum型で値のセットを定義することができる
type Shop { # The type of products the shop specializes in type: ShopType! } enum ShopType { APPAREL FOOD ELECTRONICS }
抽象型
抽象型には、InterfaceとUnionがある。
Interface
interface Discountable { priceWithDiscounts: Price! priceWithoutDiscounts: Price! } type Product implements Discountable { name: String! priceWithDiscounts: Price! priceWithoutDiscounts: Price! } type GiftCard implements Discountable { code: String! priceWithDiscounts: Price! priceWithoutDiscounts: Price! }
フィールドの型をDiscountable
とすることで、Product
またはGiftCard
いずれかを返すことができる
type Cart { discountedItems: [Discountable!]! }
query { cart { discountedItems { priceWithDiscounts priceWithoutDiscounts } } }
interfaceで定義されていない型を取得したい場合、型を明示する必要がある
query { cart { discountedItems { priceWithDiscounts priceWithoutDiscounts ... on Product { name } ... on GiftCard { code } } } }
Union
Union型は、フィールドが返す可能性のあるオブジェクトのセット。union
キーワードで定義する
union CartItem = Product | GiftCard type Cart { items: [CartItem] }
Unionに含まれる型は必ずしも同じフィールドを持つとは限らない。 返される具象型(interfaceでないもの)に対して、それぞれ明示的に取得する値を定義する必要がある。
query { cart { items { ... on Product { name } ... on GiftCard { code } } } }
Fragment
↑で型指定のために使われている… on Product
はinline fragment。
fragmentは、部分的なクエリを定義して再利用できる。 以下はGraphQL公式ドキュメントから引用。
{ leftComparison: hero(episode: EMPIRE) { ...comparisonFields } rightComparison: hero(episode: JEDI) { ...comparisonFields } } fragment comparisonFields on Character { name appearsIn friends { name } }
https://graphql.org/learn/queries/#fragments
Directives
GraphQLでは組み込みで@skip
と @include
というディレクティブが定義されている。
@include
は、以下のように <フィールド> @include(if <変数>)
という形で指定しできる。
この例ではshouldInclude
パラメータがtrue
の場合のみmyField
がクエリされる
query MyQuery($shouldInclude: Boolean) { myField @include(if: $shouldInclude) }
(@skip
はその逆。true
が指定されたらそのフィールドを取得しない)
また、カスタムのディレクティブも定義することができる。
PythonのGraphqQLライブラリStrawberryのドキュメントのサンプルがわかりやすかったので引用。
query People($identified: Boolean!) { person { name @turnUppercase } jess: person { name @replace(old: "Jess", new: "Jessica") } johnDoe: person { name @replace(old: "Jess", new: "John") @include(if: $identified) } }
ディレクティブを付けたフィールドに対し、任意の処理をさせることができる。
Introspection
GraphQLの特徴的な機能がイントロスペクション。クライアントはスキーマに関する情報を取得することができる。 以下は、スキーマで定義されている型名を取得するクエリ。
query { __schema { types { name } } }
{ "data": { "__schema": { "types": [ { "name": "Query" }, { "name": "Product" }, ] } } }
その他の取得方法については、公式ドキュメントにクエリ例があった。
まとめ
この章ではGraphQLのクエリの作り方について学んだ。
仕事ではGraphQLサーバー側を担当しており、クライアント側で呼び出し方を工夫する、試行錯誤するという経験がないため、クエリの作り方に関して知らないことも多かった。
(エイリアスを定義できることなどカスタムのdirectiveを作れることなど)
次の章はスキーマ設計に関する章。
普段開発をしている中で、チーム内でも「設計がGraphQL的かどうか」という観点で議論がされることが多い。
RESTとの考え方の違いを学んでいきたい。
*1:GraphQLサーバーとの接続を保ちながら、リアルタイムにデータの変更をクライアントに通知できる。Subscriptionに関してはこの本にも公式ガイドにも記述がなく情報が少なそう。仕様: 6.2.3 Subscription | GraphQL