GraphQLとは
簡単に言うとモダンなREST APIといったところ。
「クライアント ー REST API / GraphQL ー サーバー」のようにどちらもクライアントとサーバーの橋渡しをしている点と、HTTPメソッドで動く点は同じ。HTTPメソッドで動くということは、GraphQLもRest APIと同様にどのような言語/データベース/クライアントにも対応できるということ。
Rest API との違いは、一つのエンドポイントで好きなデータを問い合わせできるという点。欲しいデータに対してその都度エンドポイントを変える必要が無く、リクエストに対してクライアント側で多様な GraphQL query なるものをくっつけてやれば、それで取得データを決定することができる。
例)
・REST API ⇒ GET/posts/123 とか GET/posts?author=3421 とか
・GraphQL ⇒ POST/graphql (w/ a graphQL query) オンリー
これはつまり以下のように言える
・REST API ⇒ エンドポイントをもとにサーバーが必要なデータを決定する
・GraphQL ⇒ GraphQL queryを利用してクライアントが必要なデータを決定する
GraphQLの利点
1. 速い
GraphQL query の記述を変えればHTTPリクエスト1回で必要なデータをごっそりと取得できる ⇒ 速い
2.柔軟
GraphQL query の記述を変えるだけでエンドポイントを増やさずに取得するデータの量や種類を決定できる(モバイルには少量のデータを送るとか)
3.簡単
一つのエンドポイントしか管理しなくてよいから
Hello GraphQL
サーバー ⇒ クライアント で既存のデータを取得してみる
1 ) プロジェクトの初期化とBabelのインスト-ル
> npm init
> npm install babel-cli babel-preset-env
※ babel-cli:Babelのコンパイルに必要なコマンドを実行するのに必要
※ babel-preset-env:Babelに何をパースすべきか伝える
2 ) package.json / .babelrc / index.js を以下のように作成
3 ) 「npm run start」 でコンパイルしてBabelの動作確認
> npm run start
> graphql@1.0.0 start C:\Users\xxx\Desktop\GraphQL\graphql
> babel-node src/index.js
hello
4 ) GraphQL Yogaのセットアップ
GraphQLはクエリがどのように働くかを決めているだけで実装そのものではない。
つまり、環境によってどのように実装するかを決めるのは開発者の仕事。
例えるなら、JavascriptがECMAスクリプトでどのように働くかを記述されており、そのJavaScriptを動かすためにChromeはV8エンジンを、MozillaはSpiderMonkeyを実装しているといった感じ。
ということで、今回はまずGraphQLがNodejs上で動くような環境作りから始める必要があり、そのために GraphQL Yoga を使用する。GraphQL Yoga は多機能&高性能&簡単で人気。
> npm i graphql-yoga
ダミーのデータをサーバーに準備しつつセットアップ。
import { GraphQLServer } from 'graphql-yoga';
// Type definition (Schema)
const typeDefs = `
type Query {
hello: String!
name: String!
}
`
// Resolvers
const resolvers = {
Query : {
hello() {
return 'Hello GraphQL'
},
name() {
return 'Hugo'
}
}
}
const server = new GraphQLServer({
typeDefs,
resolvers
})
server.start(() => {
console.log('server is up');
})
- typeDefs でサポートする型を定義する(スキーマの定義)
- resolvers でクエリに対する処理を定義する
- serverのインスタンスを作成する
- serverを立ち上げる
※ TypeDefsは「クエリ名: 型!」で定義する(末尾の「!」が無い場合は null 可能ということ)
5 ) 実行
>npm run start
> graphql@1.0.0 start C:\Users\xxx\Desktop\GraphQL\graphql
> babel-node src/index.js
server is up
この状態で「localhost:4000」につなぐと GraphQL の Playground につながるので、クエリの実行と Hello GraphQL を確認する。
6 ) ついでに Live Reload セットアップ
プロジェクトを保存する度にサーバーを自動でリロードするように設定する。
> npm install nodemon --save-dev
nodemonをインストールしたら package.json の scripts を以下のように書き変える。
"scripts": {
"start": "nodemon src/index.js --exec babel-node"
}
index.jsを自動で実行しなおす、babel-nodeを通して。という意味になる。
これで「npm run start」を実行することで自動リロード状態になる。
GraphQL Types
【Scalar Type】
5つのビルトイン type が存在する。
- ID: ユニークなid
- String: 文字列
- Int: 整数
- Float: 小数
- Boolean: true/false
// Type definition (Schema)
const typeDefs = `
type Query {
id: ID!
name: String!
age: Int!
height: Float!
isAdult: Boolean!
}
`
【Custom Type】
開発者が定義する type。
// Type definition (Schema)
const typeDefs = `
type Query {
me: User!
}
type User {
id: ID!
name: String!
email: String!
age: Int
}
`
// Resolvers
const resolvers = {
Query : {
me(){
return {
id: '12345',
name: 'Hugo',
email: 'hugo@example.com',
age: 27
}
}
}
}
`
Resolver Operation Arguments
// 引数に '!' が有る場合、その引数は required
const typeDefs = `
type Query {
greeting(name: String, position: String): String!
add(a:Float!, b:Float!): Float!
}
`
const resolvers = {
Query : {
greeting(parent, args, ctx, info) {
if(args.name && args.position) {
return `Hello, ${args.name}! You are my favorite ${args.positino}`;
} else {
return 'Hello!'
}
},
add(parent, args){
return args.a + args.b
}
}
}
Resolverには4つの引数が存在する
- parent : Type間にリレーションがある場合の親を指す
- args : operation arguments supplied
- ctx : contexutual data (ユーザの情報)
- info : サーバーに送信されたアクションの情報など
Array Type の扱い
【Arrays of Scalar types】
const typeDefs = `
type Query {
add(numbers: [Float!]!): Float!
}
`
const resolvers = {
Query : {
add(parent, args, ctx, info){
if(args.numbers.length === 0 ) {
return 0
}
return args.numbers.reduce( ( accumulator, currentValue ) => accumulator + currentValue )
}
}
【Arrays of Custom types】
// Demo data
const users = [{
id: '1',
name: 'Hugo',
email: 'hugo@ex.com',
age: 27
}, {
id: '2',
name: 'Taro',
email: 'taro@ex.com'
}, {
id: '3',
name: 'Aike',
email: 'mike@ex.com'
}]
// Type definition (Schema)
const typeDefs = `
type Query {
users(query: String): [User!]!
}
type User {
id: ID!
name: String!
email: String!
age: Int
}
`
const resolvers = {
Query : {
users(parent, args, ctx, info) {
if (!args.query){
return users
} else {
return users.filter(user => user.name.toLowerCase().includes(args.query))
}
}
}
Relation
Type の ID を追加することでリレーションが生まれる。
// Demo data
const users = [{
id: '1',
name: 'Hugo',
email: 'hugo@ex.com',
age: 27
}, {
id: '2',
name: 'Taro',
email: 'taro@ex.com'
}, {
id: '3',
name: 'Aike',
email: 'aike@ex.com'
}]
const posts = [{
id: '4',
title: 'aaa',
body: 'asdtfhaer',
published: true,
author: '1' /// 注目
}, {
id: '5',
title: 'fff',
body: 'hrtea',
published: true,
author: '1' /// 注目
}, {
id: '6',
title: 'Ahwerike',
body: 'aikreshe',
published: true,
author: '2' /// 注目
}]
const typeDefs = `
type Query {
users(query: String): [User!]!
posts(query: String): [Post!]!
}
type User {
id: ID!
name: String!
email: String!
age: Int
posts : [Post!]! /// 注目
}
type Post {
id: ID!
title: String!
body: String!
published: Boolean!
author: User! /// 注目
}
`
そして、Resolverにオブジェクトを追加することでクエリ時にリレーションをもつTypeを引きずってくることができる
const resolvers = {
Query : {
users(parent, args, ctx, info) {
return users // call posts() coded below (userの数だけcallする)
},
posts(parent, args, ctx, info) {
return posts // call author() coded below (postの数だけcallする)
}
},
Post: {
author(parent, args, ctx, info) {
return users.find(user => user.id === parent.author) // parentはPost
// 自分(Post)のauthor(UserのID)とマッチするUserを返却
}
},
User: {
posts(parent, args, ctx, info) {
return posts.filter(post => post.author === parent.id) // parentはUser
// 自分(User)のIDとマッチするauthorをもつPostを返却
}
}
}
まとめ
import { GraphQLServer } from 'graphql-yoga';
// Demo data
// Arrayをフィールドに持ったりなんかしないですよ
const users = [{
id: '1',
name: 'Hugo',
email: 'hugo@ex.com',
age: 27,
}, {
id: '2',
name: 'Taro',
email: 'taro@ex.com'
}, {
id: '3',
name: 'Aike',
email: 'aike@ex.com'
}]
const posts = [{
id: '4',
title: 'About React',
body: 'intriguing',
published: true,
author: '1' // userのID
}, {
id: '5',
title: 'About GraphQL',
body: 'Im now studying',
published: true,
author: '1'
}, {
id: '6',
title: 'Money',
body: 'very little',
published: true,
author: '2'
}]
const comments = [{
id: '11',
text: 'oh my god!',
author: '1', // userのID
post: '4' // postのID
}, {
id: '21',
text: 'jesus christ',
author: '1',
post: '4'
}, {
id: '31',
text: 'goo goo dloo l doo',
author: '2',
post: '5'
}, {
id: '41',
text: 'ironman',
author: '3',
post: '6'
}]
// Type definition (Schema)
const typeDefs = `
type Query {
users(query: String): [User!]!
posts(query: String): [Post!]!
comments: [Comment!]!
}
type User {
id: ID!
name: String!
email: String!
age: Int
posts : [Post!]!
comments: [Comment!]!
}
type Post {
id: ID!
title: String!
body: String!
published: Boolean!
author: User!
comments: [Comment!]!
}
type Comment {
id: ID!
text: String!
author: User!
posts: Post!
}
`
// Resolvers
const resolvers = {
Query : {
users(parent, args, ctx, info) {
if (!args.query){
return users
} else {
return users.filter(user => user.name.toLowerCase().includes(args.query.toLowerCase()))
}
},
posts(parent, args, ctx, info) {
if (!args.query){
return posts // call Post() coded below
} else {
return posts.filter(post => post.title.toLowerCase().includes(args.query))
}
},
comments(parent, args, ctx, info){
return comments
}
},
Post: {
author(parent, args, ctx, info) {
return users.find(user => user.id === parent.author) // parentはPost
},
comments(parent, args, ctx, info){
return comments.filter(comment => comment.post === parent.id)
}
},
User: {
posts(parent, args, ctx, info) {
return posts.filter(post => post.author === parent.id) // parentはUser
},
comments(parent, args, ctx, info){
return comments.filter(comment => comment.author === parent.id)
}
},
Comment: {
author(parent, args, ctx, info){
return users.find(user => user.id === parent.author) // parentはComment
},
posts(parent, args, ctx, info){
return posts.filter(post => post.id === parent.post);
}
}
}
const server = new GraphQLServer({
typeDefs,
resolvers
})
server.start(() => {
console.log('server is up');
})