GraphQL入門したいのだけどなかなか理解できずに四苦八苦した記録。もしGraphQL入門でつまづく人がいたら参考になればいいと思う。対象は完全に0からプログラミングを初めて今まさに勉強中というレベル。基礎知識のない状態で参考記事を読んでいるので、理解できるまでの解釈を独り言のように記録しているので無駄が多いかもしれないけど、ご勘弁ください。
一応具体的な目標はあって、Next.jsでブログシステムを自作したくて、記事情報を取得する処理を実装する方法を勉強中。GraphQLなら、記事をmdファイルで作成してfrontbatterの記事情報を取得できると聞いて入門することに。だいたいのイメージは分かるのだけど、自作でカスタマイズして動作させるには知識が足りない。まずはGraphQLの処理をコピペでなく自作できるレベルを目指す。
↓初心者には怪文書でしかない。これをどうにかして読めるレベルになりたい。
#GraphQLとは
とりあえず基本情報に触れておく。
GraphQLとは、2015年にFacebookが発表したAPIのクエリ言語であ、データ用に定義した型システムを使用してクエリを実行するためのサーバー側のランタイム。ウェブAPIの開発においてRESTやその他のWebサービスと比較して、効率的、堅牢、フレキシブルなアプローチを提供する。
GraphQLがどういう位置付けの技術なのかというと、GitHubのエンジニアのブログを読むと大体わかる。Node.jsなどを採用したWebアプリではサイト内のデータを扱うのにJSONを採用するのが一般的なイメージだけど、かつてはXMLが使われていたらしい(多分)。XMLはHTMLに似た構文で記述するものなのでコード量が半端なく多くて面倒で、しかも読みにくい。< >
でタグを付けないといけないので煩わしいしタイピングも面倒い。なのでJSONの方が嬉しい、という挨拶に続けて、APIはRESTfulで設計されているが、RESTでは痒いところに手が届かない。一方でGraphQLはデータリクエストの柔軟性やデータ型チェックに長けているし、通信の無駄が解消される。みたいなことが書かれていた。
###REST に 比べて GraphQL のクエリ発行が効率的という話
クエリで指定されるフィールドごとにresolver
という関数を定義し、対応するデータをDBなどのデータストア等から取得して返すようにする。
REST APIとの違い。例えば/post/a
と/post/b
と/post/c
というリソースをリクエストしたいとする。REST APIではソース毎にHTTPリクエストを発行する必要がある。
- get /post/a
- get /post/b
- get /post/c
しかし、GraphQLでは/graphql
に問い合わせてJSONファイルとして一括でリクエストできる。
- /graphql {a, b, c}
必要なデータだけを単純なクエリ発行で取得できるので効率的。
###気になった用語
エンドポイント
APIにアクセスするためのURIを指す。基本的にはURIがリソースを指し、URIとHTTPメソッドの組み合わせで処理の内容を表すのが良い設計であるとされている。REST APIではリソース毎にURLを指定してリクエストする必要があるが、GraphQL APIではエンドポイントが一つしかない上に一回のリクエストで複雑なデータ取得要求が可能。
クエリ Query
データの取得要求。GETメソッドのようなもの。
ミューテーション Mutation
データの更新要求。POSTメソッドのようなもの。
###type Query
と type Mutation
の違い
データの取得にはQuery、データの更新にはMutationを使う。スキーマの定義の仕方は下記コード。
const typeDefs = `
type Query {
フィールド名(引数): 返却データ型
}
type Mutation {
フィールド名(引数): 返却データ型
}
}
`
// resolvers
const resolvers = {
Query: {
フィールド名: (引数) => 返却データ,
},
Mutation: {
フィールド名: (引数) => 返却データ,
},
}
データの基本型。
Int
32bit整数型
Float
浮動小数型
String
UTF-8 文字列型
Boolean
true もしくは false
ID
ユニークなスカラー値、キャッシュに使われる。Stringをシリアライズ(直列化)したデータで保存される。
type Query
やtype Mutation
のブロック内にメソッドを追加。複数メソッドを定義できる。コメントアウトは"""
で囲む。
const typeDefs = `
type Query {
books: [Book],
items: [Item],
}
"""
返却するデータ構造
"""
type Book { title: String, author: String }
type Item { title: String }
`
// resolvers
const resolvers = {
Query: {
books: () => books,
items: () => items,
},
}
#とりあえずここまで情報収集したけどさっぱり理解できてない
ここまではGraphQLの雰囲気を掴んでおこうという趣旨。他にも公式ドキュメントや多くの記事を参考にしたけど、素人が理解できるほど優しい世界ではなかった。できればシンプルなコードで順を追ってコーディングしていけば動作するアプリが作れる日本語の教材が欲しいところ。いくらググって考えても理解できないから贅沢言うしかないのである。
#謎が解けていったときの勉強過程の覚書
しばらくはGraphQLに入門したくてもなかなか理解できずに入り口でウロウロさせられていた。一体何が悩みかと言うと、Query
でデータ要求とか、Mutation
でデータ更新とか、どこかにあるオブジェクトデータ群に対してリクエストするんだということは何となく分かっていたけど、GraphQLを介してリクエストを出すというイメージが全然浮かんでこなくて、結局アプリの中でGraphQLを起動するにはどうすんの、と思っていた。オブジェクトデータ群は外部ファイルやDBに書いておいてインポートしたりすればいいし、Queryの出し方も普通に途中にコードを書けばいいし、スキーマもそのそばに書けばいい。しかし、GraphQLの起動はどのコードが発端なんだ、Expressがないといけないのか、どうなんだ。
しばらくは下記の記事を参考に模索。apollo-server-express
というモジュールを使ってるらしい。ググってると「apollo」という単語は何度も見かけたけど、これが必須なのかいわゆる便利ツールなのかはまだよく分からん。プレーンな状態でGraphQLを動かす方法とかはそのうち全貌が分かってくることに期待。
この記事では以下のモジュールを使っている。
- apollo-server-express
- graphql-tools
- graphql
- express
- body-parser
$ npm install --save apollo-server-express graphql-tools graphql express body-parser
この記事のコードは下記。コメントアウトのおかげでどの部分が何を意味するのかわかりやすかった。しかしGraphQLへの接続のあたりがやっぱりよく分からなかった。
const express = require('express')
const bodyParser = require('body-parser')
const { graphqlExpress, graphiqlExpress } = require('apollo-server-express')
const { makeExecutableSchema } = require('graphql-tools')
const app = express()
process.on('uncaughtException', (err) => console.error(err))
process.on('unhandledRejection', (err) => console.error(err))
app.use(bodyParser.urlencoded({extended: true}))
app.use(bodyParser.json())
// GraphQLスキーマ定義
const typeDefs = `
"""
type Query (必須)
"""
type Query { books: [Book] }
"""
返却するデータ構造
"""
type Book { title: String, author: String }
`
// ダミーデータ
const books = [
{
title: 'Harry Potter and the Sorcerer\'s stone',
author: 'J.K. Rowling',
},
{
title: 'Jurassic Park',
author: 'Michael Crichton',
},
]
// resolvers
const resolvers = {
Query: { books: () => books },
}
// スキーマ生成
const schema = makeExecutableSchema({
typeDefs,
resolvers,
})
// GraphQLエンドポイント
app.use('/graphql', bodyParser.json(), graphqlExpress({ schema }))
// GraphiQL:GraphQLクエリのvisual editor
// TODO: 本番デプロイ時はアクセス出来ないようにする
app.use('/graphiql', graphiqlExpress({ endpointURL: '/graphql' }))
app.listen(5000, () => {
console.log('Access to http://localhost:5000')
})
###STEP 1 もう少しシンプルなサンプルコードで勉強したいと思った
このコードを解読しようといろいろ調べたけど決定打はなかった。そこでapolloとexpressに絞ってググるとこの記事に行き着いた。以降、この2つの記事のコードを比較しながら勉強していく。
この記事で使うモジュールは以下。さきほどの記事よりインストールする数が少なくてうれしい。
- express
- apollo-server-express
- graphql
$ $ npm i --save express apollo-server-express graphql
この記事のコードを参考にする。
const express = require('express');
const { ApolloServer, gql } = require('apollo-server-express');
// Construct a schema, using GraphQL schema language
const typeDefs = gql`
type Query {
hello: String
}
`;
// Provide resolver functions for your schema fields
const resolvers = {
Query: {
hello: () => 'Hello world!',
},
};
const server = new ApolloServer({ typeDefs, resolvers });
const app = express();
server.applyMiddleware({ app });
app.listen({ port: 4000 }, () =>
console.log(`🚀 Server ready at http://localhost:4000${server.graphqlPath}`)
);
とりあえずコピペしてサーバー起動すれば動く状態ではある。
$ node app.js
そしてここであることに気づく。
###STEP 2 エンドポイント/graphql
ってどゆこと?
REST APIはリソースをリクエストするたびにそのURL(エンドポイント)にアクセスする必要があるというのはどこを見ても書かれていることだけど、GraphQLのエンドポイントは/graphql
の一つしかないという意味がよく分からなかった。でもテストアプリを走らせてhttp://localhost:4000/graphql
にアクセスするとGraphQLのビジュアルエディタが開かれた。なるほど、GraphQLのAPIが動作するURLが/graphql
で、リクエストをここのアプリで捌いていっただけだったのか。プログラムとして動作するのは当たり前だけど、ビジュアルエディタとして手動で操作することもできるというわけだった。
###STE 3 ちなみにbody-parserについて
一つ目の参考記事と、二つ目の参考記事では、インストールするモジュールの数に差があった。一つ目の記事では以下のモジュールを多くインストールしていた。
- graphql-tools
- body-parser
一つ目の記事のサンプルコード中にあるbodyParser.json()
という記述は、Express4.xでは外部ミドルウェアだったものを標準搭載してexpress.json()
で仕様できるらしい。ググっていろんなバージョンやいろんな方針でコーディングしている記事があるけど、一応どちらも使えるし同じ機能を実装しているということを把握しておきたい。ちなみに標準機能として実装しておけばconst bodyParser = require('body-parser')
を宣言する必要がなくなって効率的。
というわけで二つ目の記事のやり方の方が最新情報に近いと判断して、そっちを参考に勉強してくことに。
###STEP 4 ビジュアルエディタを使ってみる
いろいろスッキリしたところでGraphQlエディタを動かしてみた。{hello}
を要求するとHello World!
が返ってきた。
このHello Worldはapp.js
でresolvers
に書いておいたHello Worldが渡されてるだけ。試しに適当な名前のデータを要求したら「そんな名前のデータはねえ」と言われる。
ちなみにエディタの右端についている「SCHEMA」タブを開くと要求したデータの型を定義したスキーマを自動生成だか読み込みだかしてくれている。
###STEP 5 オブジェクトデータ群を用意する方法
ではGraphQLでデータ要求する相手となるオブジェクトデータ群はどこに存在するのかと言うと、DBを実装するのはまだ難しいので、外部ファイルにオブジェクトを記述しておいてexports
すればいいとのこと。なるほどこれがデータ保管庫か。
とりあえず手本通りにusers
というオブジェクトを作り、id
name
age
created_date
という4つのkeyを定義した。このオブジェクトデータの構造は作りたいアプリによって自由に定義できる、と思う。
exports.users = [
{
id: 1,
name: 'Zonomaa',
age: 25,
created_date: '2019-04-05 01:35:25.000'
},
{
id: 2,
name: 'Masa',
age: 28,
created_date: '2019-04-10 08:23:51.000'
},
{
id: 3,
name: 'Ms',
age: 30,
created_date: '2019-04-10 08:32:09.000'
}
// ... 適当にいくつか作ってみましょう
];
メインのapp.js
のファイルの冒頭でrequireを追記し、オブジェクトデータ群を記述したファイルを読み込んでおく。メインのapp.js
でこのファイルを読み込んでおき、app.js
の中でエンドポイントの/graphql
にリクエストすればデータを引っ張ってこれるというわけか。
const express = require('express'); // いつもの
const { ApolloServer, gql } = require('apollo-server-express'); // ここまだよく分からん
const { users } = require('./users'); // ここでオブジェクトデータ群を読み込み
へぇ、importじゃないんだ。importとrequireの使い分けまだよく分からん。Reactコンポーネントでよくimportを見かけるなぁ程度の知識しかない。クラスやメソッドはimportとexport? そしてファイルやモジュールはrequireとexports? あとで勉強しとこう。
###STE 6 スキーマを定義してデータ要求をカスタマイズ
データの用意の仕方、GraphQLの起動の仕方、この2つのイメージが掴めた。ここでGraphQLでどのようにデータを要求するかを考えていく。ここで要求するデータは先ほど作成したusers
なので、そのオブジェクトデータのvalueの型を定義していく。app.js
のtypeDefs
を編集する(この時点ではhelloがはいっているだけなので)。
// Construct a schema, using GraphQL schema language
const typeDefs = gql`
type User {
id: Int
name: String
age: Int
created_date: String
}
type Query {
users: [User]
}
`;
型の定義はtype User{}
、データ要求はtype Query{}
に書かれているのが分かる(よく分からんが)。ええと、つまりusers.js
に盛り込んでいるオブジェクトデータの型に合わせてtypeDefs
の中で新たにtype User{}
というオブジェクトの型を定義することでusers.js
から要求通りのデータの受け渡し時の作法を決定した、ということっぽい。そしてデータを要求するには「さっき定義した型の[User]
で頼んます」とやっているっぽい。
これで要求するデータの型の定義のtypeDefs
と、どのデータを要求するかのQuery
が完成した。これでアプリが動くと思いきや、動かないらしい。参考記事の著者も驚いて演出してくれている。
###STEP 7 resolvers
でデータ取得のロジックを定義する
一つ目の参考記事の中でもresolvers
とあって「これ何やねん」と悩んでいたけど、2つ目の参考記事でも登場した。どちらでもresolvers
について触れているけど、1つ目の記事では具体的な仕様が、2つ目の記事では具体的なコーディングの順序が分かるような内容になっている。今はイメージが掴めてない状態なので2つ目の記事がありがたい。
GraphQLでは、データ取得のロジック(実際に処理を発火させる関数のこと?)は手動で記述するのがことになっているとのこと。普通は自動でやるものなのか? とにかくここでやるべきは、resolvers
の中のQuery
でusers
をreturnする関数書く。このusers
はさっきtype Query
で定義したやつ。
const resolvers = {
Query: {
users: () => users
},
};
つまり、typeDefs
ではtype User
でデータの型を定義し、type Query
で要求するデータを指定する。そして、resolvers
ではQuery
でデータ取得の処理を記述する。これがGraphQLによるデータ要求の一連の流れになる。
###STEP 8 いろいろ自作したアプリでGraphQLの動作を確認する方法
ここでアプリ起動。
$ node app.js
そしてhttp://localhost:4000/graphqlにアクセスしてエディタを開く。そこでクエリを発行する。要は欲しいデータを書いて要求するということ。例えばこう。
{
users {
id
name
}
}
さっき定義してexportsしておいたusers
というデータ群から、User
で定義したデータ型で、id
name
の2つのkeyのデータを要求する、という意味になる。
動いた。お見事!!めっちゃスッキリした。
今回の勉強で分かったことはこんな感じ。
- インストールが必要なnodeモジュールの種類はとりあえず3つでいい
- GraphQLにアクセスする「エンドポイント」とはエディタが起動するURLだった
- オブジェクトデータ群は外部ファイルに書いてrequireしとけばGraphQLのエンドポイントからアクセスできる
-
typeDefs
の中で、データ型のtype objName
と、要求するデータのtype Query
を定義 -
resolvers
の中で、データ取得処理のQuery
を定義 - 今回はエディタでリクエスト出したけど実際のアプリではどうすんの?
#基礎をすっ飛ばして知識0からの勉強は大変すぎるという愚痴
こういう外部データの読み込みとか、APIへの接続とか、普通のエンジニアなら当たり前のように理解して、説明を省略した記事でも読み解くことができるのだろうけど、全くの素人からするとコードの1行1行の相関関係がわからないので理解に到ることのできる記事というのがなかなか見つからない。今回参考にした記事はとてもわかりやすくて助けられた。おかげでWebアプリでモジュールやらJSONデータを活用する作法というものが少し見えてきた。
今回学んだことを取り入れて自分が作りたいものに活かすにはどうすべきか。Next.jsでブログシステムを作ろうとしているが、記事情報の取得はGraphQLでどうにかできそうだ。記事はmdファイルで管理し、記事情報はfrontmatterに記述しているので、おそらく記事のmdファイルが入ったディレクトリ以下をrequireしておき、/graphql
からアクセスしてfrontmatter.id
とかで取得できる。うん、多分いける。
次回
Gatsby.js(Next.js)のテーマ制作から学ぶ【React.js × GraphQL】のブログシステムでの投稿記事情報取得