PRODUCTION READY GRAPHQLを読む: 1章. GraphQL入門

仕事で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のドキュメントのサンプルがわかりやすかったので引用。

strawberry.rocks

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.org

まとめ

この章ではGraphQLのクエリの作り方について学んだ。
仕事ではGraphQLサーバー側を担当しており、クライアント側で呼び出し方を工夫する、試行錯誤するという経験がないため、クエリの作り方に関して知らないことも多かった。
エイリアスを定義できることなどカスタムのdirectiveを作れることなど)

次の章はスキーマ設計に関する章。
普段開発をしている中で、チーム内でも「設計がGraphQL的かどうか」という観点で議論がされることが多い。 RESTとの考え方の違いを学んでいきたい。

*1:GraphQLサーバーとの接続を保ちながら、リアルタイムにデータの変更をクライアントに通知できる。Subscriptionに関してはこの本にも公式ガイドにも記述がなく情報が少なそう。仕様: 6.2.3 Subscription | GraphQL