kakakakakku blog

Weekly Tech Blog: Keep on Learning!

Apollo Server と Apollo Client を写経しながら GraphQL を学べる「初めての GraphQL」を読んだ

2019年11月に発売された「初めての GraphQL」を読んだ.1度ザッと読んだ後に,気になっていた Apollo ServerApollo Client の実装を写経しながら理解を深めていたため,書評をまとめるのに少し遅れてしまった.

タイトルに「初めての」とある通り,GraphQL 初学者をターゲットに網羅的に学ぶことができる1冊だった.特に「背景 → クエリ → スキーマ → リゾルバ → クライアント → 実戦投入」という流れは素晴らしく,一言で表現すると「知りたい!を知れる本」かなと!5章と6章は時間を取って写経するのが良いと思う.

目次

  • 1章 : GraphQLへようこそ
  • 2章 : グラフ理論
  • 3章 : GraphQLの問い合わせ言語
  • 4章 : スキーマの設計
  • 5章 : GraphQLサーバーの実装
  • 6章 : GraphQLクライアントの実装
  • 7章 : GraphQLの実戦投入にあたって
  • 付録A : Relay各仕様解説

以下のサイトに誤植は公開されていなかった.正直言って「初版第1刷」は誤植がそこそこある.記事の最後にメモ程度に残しておく.

www.oreilly.co.jp

GraphQL API とクエリ環境

本書を読みながら理解度を高めるため,気軽に試せる GraphQL API とクエリ環境を準備しておくと良いと思う.本書では「Snowtooth GraphQL API」をメインで使うけど,他にも「GitHub GraphQL API」「Star Wars API」もある.

また,個人的に好きな「GraphQL Pokémon」もあり,詳しくは前回の記事にまとめてある.

kakakakakku.hatenablog.com

クエリ環境は「GraphQL Playground (Web / App)」「GraphiQL (Web / App)」など,慣れたもので良いと思う.個人的には「GraphQL Playground」の Mac App を使っている.

GraphQL オペレーション

3章「GraphQLの問い合わせ言語」では,GraphQL オペレーション(querymutationsubscription)を学ぶ.本書で使う「Snowtooth GraphQL API」「スノートゥース山」という名前のゲレンデ(架空)のリフト情報とトレイル(コース)情報を管理する.

1. query

まず query オペレーションでは,シンプルなクエリを実行したり,複数クエリを実行したり,条件付きのクエリを実行したり,ステップバイステップにクエリを学べる.また「GraphQL Pokémon」の記事でも紹介した「フラグメント」も本書で紹介されている.以下のクエリは liftCount()allLifts()allTrails() の3種類のクエリを実行し,リフト件数は status: OPEN の条件付きとなる.

query liftsAndTrails {
  liftCount(status: OPEN)
  allLifts {
    name
    status
  }
  allTrails {
    name
    difficulty
  }
}

クエリを実行すると,以下のように結果が返ってくる.

f:id:kakku22:20200105051036p:plain

2. mutation

次に mutation オペレーションでは,データ更新を実行するミューテーションクエリを学べる.構文だけではなく,実際に「Snowtooth GraphQL API」を使って試すこともできる.公開 API だけど,実はリフトとトレイルのステータスを更新する setLiftStatus()setTrailStatus() は使えるようになっている.ビックリ!

f:id:kakku22:20200105051053p:plain

実際にリフトのステータスを CLOSED に更新するミューテーションクエリは以下となる.

mutation closeLift {
  setLiftStatus(id: "jazz-cat", status: CLOSED) {
    name
    status
  }
}

3. subscription

最後に subscription オペレーションでは,GraphQL のポイントとも言える「WebSocket を使ったデータのリアルタイム反映」を学べる.以下のようなサブスクリプションクエリを実行すると,データの更新待ちになるため,別途ミューテーションクエリを実行すると,すぐに反映される.REST のように定期的に API を実行する必要もなく,データによっては便利な機能だと思う.

subscription {
  liftStatusChange {
    name
    capacity
    status
  }
}

f:id:kakku22:20200105051110p:plain

なお,「GraphQL Playground」の Mac Appだと WebSocket をローカルホストに接続するため,うまく動かなかった.設定変更をすることもできず,今回は Web で確認した.

{
  "error": "Could not connect to websocket endpoint ws://localhost:4000/. Please check if the endpoint url is correct."
}

GraphQL スキーマ : 多対多

4章「スキーマの設計」では,写真共有アプリケーションをテーマとし,GraphQL スキーマの仕様と「スキーマファースト」と呼ばれる設計思想を学べる.そして,シンプルな Photo 型だけではなく,User 型と多対多の関係を作るために中間テーブルを用意したりする「よくある設計」に関してもまとまっていて良かった.

例えば,よくある「タグ付け」という機能を実装する場合,以下のように Photo 型と User 型に「タグ付け」を表現するフィールドを追加する.!「null ではない」を意味するため,[Photo!]!「null ではない配列に,null ではない Photo が入っている」となる.

type User {
  (中略)
  inPhotos: [Photo!]!
}

type Photo {
  (中略)
  taggedUsers: [User!]!
}

さらに,型同士に関係だけではなく,追加情報(例えば「知り合ってからの期間」)も持たせたい場合は,新しく型を作ることになる.これを「スルー型」と呼ぶ.以下のように Friendship 型を定義し,追加情報は Friendship 型に持たせられる.

type User {
  friendship: [Friendship!]!
}

type Friendship {
  friend_a: User!
  friend_b: User!
  howLong: Int!
  whereWeMet: Location
}

GraphQL サーバの実装を写経する : apollo-server

5章「GraphQLサーバーの実装」では,apollo-server を使って,実際に GraphQL サーバを実装していく.GraphQL のクエリを実行するためには,リゾルバ(特定のデータを返す関数)が必要となり,実際に実装するのは大変だと思う.

github.com

本書を流し読みするだけだと理解が浅くなりそうだったので,時間を取って写経をしてみた.是非写経をオススメするけど,今回はその一部を載せておこうと思う.なお,完成形は GitHub に公開されているので,動作確認から先に進めても良いと思う.

github.com

まず最初にプロジェクトを作成する.Apollo 関連とホットリロードをするための nodemon もインストールしておく.

$ npm init -y
$ npm install apollo-server graphql nodemon

さっそく index.js を作成する.構成としてはクエリを定義した typeDefs (型定義)resolvers (リゾルバ実装) となる.そして,最後に typeDefsresolvers を指定した Apollo Server を起動する.totalPhotos() を実行すると,固定値 42 を返す.

const {
    ApolloServer
} = require(`apollo-server`)

const typeDefs = `
    type Query {
        totalPhotos: Int!
    }
`

const resolvers = {
    Query: {
        totalPhotos: () => 42
    }
}

const server = new ApolloServer({
    typeDefs,
    resolvers
})

server
    .listen()
    .then(({
        url
    }) => console.log(`GraphQL Service running on ${url}`))

さっそく npm start で Apollo Server を起動する.

$ npm start
(中略)
GraphQL Service running on http://localhost:4000/

うまく起動できていると,GraphQL Playground でクエリを実行できる.以下のようになれば OK!

{
  totalPhotos
}

f:id:kakku22:20200105051336p:plain

次にミューテーションを実装する.名前は写真を登録するため postPhoto() とする. パラメータとしては namedescription を定義する.登録後は Boolean を返す定義となり,今回は固定値 true 返す.リゾルバとしては,配列 photos にデータを追加する実装になっている.コードの差分を中心に以下に載せた.

// 中略

const typeDefs = `
    type Query {
        totalPhotos: Int!
    }

    type Mutation {
        postPhoto(name: String! description: String): Boolean!
    }
`

var photos = []

const resolvers = {
    Query: {
        totalPhotos: () => photos.length
    },
    Mutation: {
        postPhoto(parent, args) {
            photos.push(args)
            return true
        }
    }
}

// 中略

動作確認のために,まず Query Variables に以下の JSON を定義する.

{
  "name": "sample photo A",
  "description": "A sample photo for our dataset"
}

そして,ミューテーションクエリを実行する.

mutation newPhoto($name: String!, $description: String) {
  postPhoto(name: $name, description: $description)
}

すると,リゾルバの実装通りに true が返ってくる.

{
  "data": {
    "postPhoto": true
  }
}

f:id:kakku22:20200105051355p:plain

実際に使おうとすると,写真の一覧が欲しかったり,ミューテーションクエリから true が返ってくるのは微妙だったりする.次に allPhotos() クエリを追加したり,ミューテーションクエリから追加した写真を返せるようにする.そのために Photo 型を定義したり,postPhoto() の定義で Photo を返すように修正したり,ID を連番で採番するように修正している.コードの差分を中心に以下に載せた.

// 中略

const typeDefs = `
    type Photo {
        id: ID!
        url: String!
        name: String!
        description: String
    }

    type Query {
        totalPhotos: Int!
        allPhotos: [Photo!]!
    }

    type Mutation {
        postPhoto(name: String! description: String): Photo!
    }
`

var _id = 0
var photos = []

const resolvers = {
    Query: {
        totalPhotos: () => photos.length,
        allPhotos: () => photos
    },
    Mutation: {
        postPhoto(parent, args) {
            var newPhoto = {
                id: _id++,
                ...args
            }
            photos.push(newPhoto)
            return newPhoto
        }
    },
    Photo: {
        url: parent => `http://yoursite.com/img/${parent.id}.jpg`
    }
}

// 中略

フィールドを指定したミューテーションクエリを実行する.

mutation newPhoto($name: String!, $description: String) {
  postPhoto(name: $name, description: $description) {
    id
    name
    description
  }
}

すると,ちゃんと Photo 型の結果が返ってきた.

{
  "data": {
    "postPhoto": {
      "id": "3",
      "name": "sample photo A",
      "description": "A sample photo for our dataset"
    }
  }
}

ミューテーションクエリを数回実行した後に追加した allPhotos() クエリを実行する.

query listPhotos {
  allPhotos {
    id
    name
    description
    url
  }
}

写真の一覧を取得できる.

{
  "data": {
    "allPhotos": [
      {
        "id": "0",
        "name": "sample photo A",
        "description": "A sample photo for our dataset",
        "url": "http://yoursite.com/img/0.jpg"
      },
      {
        "id": "1",
        "name": "sample photo A",
        "description": "A sample photo for our dataset",
        "url": "http://yoursite.com/img/1.jpg"
      },
      {
        "id": "2",
        "name": "sample photo A",
        "description": "A sample photo for our dataset",
        "url": "http://yoursite.com/img/2.jpg"
      },
      {
        "id": "3",
        "name": "sample photo A",
        "description": "A sample photo for our dataset",
        "url": "http://yoursite.com/img/3.jpg"
      }
    ]
  }
}

残りは以下の項目などを実装していくことになる.

  • enum 型 と input 型を使って使って型定義をモデル化する(デフォルトインプットを指定する)
  • Photo 型 と User 型を連携する

GraphQL サーバの実装を写経する : apollo-server-express

5章「GraphQLサーバーの実装」にはまだ続きがある.Apollo Server を既存のアプリケーションに追加したり,より細かな機能を Express ミドルウェアとして利用したり,様々な用途を考えて apollo-server-express を使ったリファクタリングをする.試す場合は,以下のように apollo-server-express などをインストールしておく.

$ npm remove apollo-server
$ npm install apollo-server-express express
$ npm install graphql-playground-middleware-express

Express を使う場合は,以下のような実装になる.ウェブページを表示したり,GraphQL Playground を表示したり,必要に応じてミドルウェアを追加できる.

const {
    ApolloServer
} = require(`apollo-server-express`)
const express = require(`express`)
const expressPlayground = require(`graphql-playground-middleware-express`).default

// 中略

var app = express()

const server = new ApolloServer({
    typeDefs,
    resolvers
})

server.applyMiddleware({
    app
})

app.get(`/`, (req, res) => res.end(`Welcome to the PhotoShare API`))
app.get(`/playground`, expressPlayground({
    endpoint: `/graphql`
}))

app
    .listen({
            port: 4000
        }, () => console.log(`GraphQL Service running on @ http://localhost:4000${server.graphqlPath}`)
    )

残りは以下の項目などを実装していくことになる.コード量が多く,今回は割愛するけど,より実践的な実装を学べるため,試しておくと良いかと!GitHub の完成形を見るだけでも雰囲気は伝わるはず.

  • typeDefsresolvers を別ファイルに分割して index.js をリファクタリングする
  • MongoDB を使ってデータを永続保存する
  • GitHub API を使って認証と認可を実装する

GraphQL サーバの実装を写経する : apollo-client

6章「GraphQLクライアントの実装」では,React から GraphQL を扱うために graphql-requestapollo-client を学ぶ.今回は個人的に興味のあった apollo-client を写経した.React 自体はあまり難しい点はなく,apollo-boost (apollo-client などを含む)react-apollo を使うことにより,実装がシンプルになることを体験できた.以下は実装した User.js の中で GraphQL Server にクエリを実行している部分を抜粋している.

// 中略

const Users = () => 
    <Query query={ROOT_QUERY} fetchPolicy="cache-and-network">
        {({ data, loading, refetch }) => loading ?
            <p>loading users...</p> :
            <UserList count={data.totalUsers} 
                users={data.allUsers} 
                refetch={refetch} />
        }
    </Query>

// 中略

本書を読んでいて参考になったのはキャッシュ実装の仕組みで,REST だとエンドポイントごとにキャッシュできるけど,GraphQL だと固定のエンドポイントになるため,どうキャッシュを実現するの?という話だった.react-apollo を使うと options.fetchPolicy という設定があり,以下の5種類から選べる.また apollo-cache-persist と組み合わせると,キャッシュを localStorage に保存することもできる.このあたりはプロダクションコードを実装するときに改めて検討したいと思う.

  • cache-first
  • cache-and-network
  • network-only
  • cache-only
  • no-cache

https://www.apollographql.com/docs/react/api/react-apollo/www.apollographql.com

とは言え,まだまだ apollo-client のメリットを学べてなく,引き続き調査をしていく.

実践投入

7章「GraphQLの実戦投入にあたって」では,これから本番環境に GraphQL を導入したい人と既に導入している人に最適な内容になっている.僕自身もまだ GraphQL はプロトタイプでしか使ってなく,知らない内容も多かった.

特に「漸進的なマイグレーション」という解説は良かった.どのように既存のアプリケーションを GraphQL に移行するか?という点で,計5種類の戦略が紹介されていた.GraphQL をゲートウェイのように使って,リゾルバから REST API にアクセスするパターンは,並行稼動を前提とした「移行のしやすさ」もあり,現場でも使う機会がありそうだった.

  • REST からリゾルバにデータをフェッチする
  • もしくは GraphQL リクエストを使用する
  • 1つか2つのコンポーネントに GraphQL を組み込む
  • 新しい REST エンドポイントを作成しない
  • 現在の REST エンドポイントをメンテナンスしない

誤植 : 初版第1刷

  • P.vii GrapghQLGraphQL
  • P.48 List 型のLift 型の
  • P.51 https://www.graphqlbin.com/v2/ANgjtr → 既に Server cannot be reached になっている
  • P.53 https://www.graphqlbin.com/v2/yoyPfz → 既に Server cannot be reached になっている
  • P.71 「構成されるでデータになり」→「構成されるデータになり」
  • P.75 DataTimeDateTime
  • P.82 「エラーが帰ってきます」→「エラーが返ってきます」
  • P.97 type Mutation { のインデント誤り
  • P.138「写真共有サービス」→「写真共有アプリケーション」
  • P.149「写真管理サービス」→「写真共有アプリケーション」
  • P.213 Amazon Web ServiceAmazon Web Services

なお,誤植ではないけど,P.7の「状態機械」はシンプルに「ステートマシン」で良さそうな気がする.

まとめ

  • 「初めての GraphQL」を読んだ
  • GraphQL 初学者をターゲットに網羅的に学ぶことができる1冊だった
    • 特に「背景 → クエリ → スキーマ → リゾルバ → クライアント → 実戦投入」という流れは素晴らしい!
  • 1周目は全体をザッと読みつつ,2周目で手を動かしながら読み直すのが,1番学習効率が高そう