2019年11月に発売された「初めての GraphQL」を読んだ.1度ザッと読んだ後に,気になっていた Apollo Server と Apollo Client の実装を写経しながら理解を深めていたため,書評をまとめるのに少し遅れてしまった.
タイトルに「初めての」とある通り,GraphQL 初学者をターゲットに網羅的に学ぶことができる1冊だった.特に「背景 → クエリ → スキーマ → リゾルバ → クライアント → 実戦投入」という流れは素晴らしく,一言で表現すると「知りたい!を知れる本」かなと!5章と6章は時間を取って写経するのが良いと思う.
目次
- 1章 : GraphQLへようこそ
- 2章 : グラフ理論
- 3章 : GraphQLの問い合わせ言語
- 4章 : スキーマの設計
- 5章 : GraphQLサーバーの実装
- 6章 : GraphQLクライアントの実装
- 7章 : GraphQLの実戦投入にあたって
- 付録A : Relay各仕様解説
以下のサイトに誤植は公開されていなかった.正直言って「初版第1刷」は誤植がそこそこある.記事の最後にメモ程度に残しておく.
GraphQL API とクエリ環境
本書を読みながら理解度を高めるため,気軽に試せる GraphQL API とクエリ環境を準備しておくと良いと思う.本書では「Snowtooth GraphQL API」をメインで使うけど,他にも「GitHub GraphQL API」や「Star Wars API」もある.
また,個人的に好きな「GraphQL Pokémon」もあり,詳しくは前回の記事にまとめてある.
クエリ環境は「GraphQL Playground (Web / App)」や「GraphiQL (Web / App)」など,慣れたもので良いと思う.個人的には「GraphQL Playground」の Mac App を使っている.
GraphQL オペレーション
3章「GraphQLの問い合わせ言語」では,GraphQL オペレーション(query
と mutation
と subscription
)を学ぶ.本書で使う「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 } }
クエリを実行すると,以下のように結果が返ってくる.
2. mutation
次に mutation
オペレーションでは,データ更新を実行するミューテーションクエリを学べる.構文だけではなく,実際に「Snowtooth GraphQL API」を使って試すこともできる.公開 API だけど,実はリフトとトレイルのステータスを更新する setLiftStatus()
と setTrailStatus()
は使えるようになっている.ビックリ!
実際にリフトのステータスを CLOSED
に更新するミューテーションクエリは以下となる.
mutation closeLift {
setLiftStatus(id: "jazz-cat", status: CLOSED) {
name
status
}
}
3. subscription
最後に subscription
オペレーションでは,GraphQL のポイントとも言える「WebSocket を使ったデータのリアルタイム反映」を学べる.以下のようなサブスクリプションクエリを実行すると,データの更新待ちになるため,別途ミューテーションクエリを実行すると,すぐに反映される.REST のように定期的に API を実行する必要もなく,データによっては便利な機能だと思う.
subscription { liftStatusChange { name capacity status } }
なお,「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 に公開されているので,動作確認から先に進めても良いと思う.
まず最初にプロジェクトを作成する.Apollo 関連とホットリロードをするための nodemon
もインストールしておく.
$ npm init -y
$ npm install apollo-server graphql nodemon
さっそく index.js
を作成する.構成としてはクエリを定義した typeDefs (型定義)
と resolvers (リゾルバ実装)
となる.そして,最後に typeDefs
と resolvers
を指定した 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 }
次にミューテーションを実装する.名前は写真を登録するため postPhoto()
とする. パラメータとしては name
と description
を定義する.登録後は 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 } }
実際に使おうとすると,写真の一覧が欲しかったり,ミューテーションクエリから 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 の完成形を見るだけでも雰囲気は伝わるはず.
typeDefs
とresolvers
を別ファイルに分割してindex.js
をリファクタリングする- MongoDB を使ってデータを永続保存する
- GitHub API を使って認証と認可を実装する
GraphQL サーバの実装を写経する : apollo-client
6章「GraphQLクライアントの実装」では,React から GraphQL を扱うために graphql-request
と apollo-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
GrapghQL
→GraphQL
- 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
DataTime
→DateTime
- P.82 「エラーが帰ってきます」→「エラーが返ってきます」
- P.97
type Mutation {
のインデント誤り - P.138「写真共有サービス」→「写真共有アプリケーション」
- P.149「写真管理サービス」→「写真共有アプリケーション」
- P.213
Amazon Web Service
→Amazon Web Services
なお,誤植ではないけど,P.7の「状態機械」はシンプルに「ステートマシン」で良さそうな気がする.
まとめ
- 「初めての GraphQL」を読んだ
- GraphQL 初学者をターゲットに網羅的に学ぶことができる1冊だった
- 特に「背景 → クエリ → スキーマ → リゾルバ → クライアント → 実戦投入」という流れは素晴らしい!
- 1周目は全体をザッと読みつつ,2周目で手を動かしながら読み直すのが,1番学習効率が高そう