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刷」は誤植がそこそこある.記事の最後にメモ程度に残しておく.
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 オペレーション(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.com
本書を流し読みするだけだと理解が浅くなりそうだったので,時間を取って写経をしてみた.是非写経をオススメするけど,今回はその一部を載せておこうと思う.なお,完成形は GitHub に公開されているので,動作確認から先に進めても良いと思う.
github.com
まず最初にプロジェクトを作成する.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:
}
}
フィールドを指定したミューテーションクエリを実行する.
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:
)
残りは以下の項目などを実装していくことになる.コード量が多く,今回は割愛するけど,より実践的な実装を学べるため,試しておくと良いかと!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番学習効率が高そう