「初めてのGraphQL」お勉強ノート
こんにちは。
レバウェル開発部アドベントカレンダー 9日目担当者です。
業務でGraphQLを触る機会があったので、その時にお勉強した内容を投稿します。
👉 書籍はこちら
基本的な知識をキャッチアップするのに役に立った1章, 3章について掻い摘んで記載します。
1章 GraphQLへようこそ
1.1 GraphQLとは
- APIのための問い合わせ言語
- 一般的にはHTTPプロトコルが使用される
- GraphQLでは必要なデータをクエリで指定して要求する
👉 https://graphql.org/swapi-graphql
さくっと試すならこちら(スターウォーズの情報を提供するAPIサーバらしい)
● personID = 5(レイア姫)の名前、誕生年、作成日を取得
画像左がリクエスト内容、右がレスポンス結果です。
- クエリが入れ子になっている場合、紐づくオブジェクトを全て取得することができる
- レスポンスのデータを使ってさらにリクエストを送り...ということを繰り返す必要がない
● レイア姫と出演してる映画とそれに出演してるキャラクター一覧を取得
図の場合、filmConnection, films, characterConnection, charactersが入れ子になっています。
xxxConnectionが中間テーブル的なものと思われる。
- GraphQLサーバではクエリ実行のたび型システムに基づいてバリデーションされる
- GraphQLのサービスはGraphQLスキーマに則って型が定義されている
GraphQLの言語仕様
- GraphQLはクライアント/サーバ通信のための言語仕様
- クエリを書くための言語と文法、型システムに基づいたクエリの実行とバリデーションを規定している
- が、それ以外には何も規定していない
- 実際のプロダクトの設計は実装者に委ねられている
GraphQLの設計原則
- 階層構造
- クエリは階層構造になっている
- フィールドは他のフィールドの入れ子になり、クエリはレスポンスと同じ構造をとる
- プロダクト中心
- データを必要とするクライアントの言語やランタイムに従って実装される
- 強い型付け
- GraphQLサーバはGraphQLの型システムに保証される
- それぞれのフィールドは固有の型を持ちバリデーションされる
- クライアントごとのクエリ
- GraphQLサーバはクライアントが必要とする機能を提供する
- 自己参照
- GraphQL言語はGraphQL自身の型システムに問い合わせができる
1.2 GraphQLの誕生
- Facebookが開発
- クライアント/サーバアプリケーションの性能上の課題とデータ構造の要件を満たすための解決策として開発を始めた
1.4 RESTの課題
- RESTは現在最も馴染み深いAPIパラダイム
- GET/PUT/POST/DELETEといった操作でWeb上のリソースを操作するリソース指向アーキテクチャ
過剰な取得
- 登場人物の名前、身長、体重の情報が欲しいだけなのに関連する全てのデータを取得しなければならない例
- GraphQLでは欲しいデータだけをクエリに指定し、望んだ構造のレスポンスを受け取ることができる
過少の取得
- ルークスカイウォーカーが出演する映画の一覧が欲しいだけなのに6回のリクエストが必要になる例
- GraphQLでは入れ子になったクエリを定義し、1回のリクエストで必要なデータを全て取得することができる
自由度の低さ
- RESTではクライアントに変更が加わる度に新しいエンドポイントを作成する必要があり、エンドポイントの数が膨れ上がっていく
- GraphQLは基本的に単一のエンドポイントしか存在しない
- 単一のエンドポイントに送られるクエリを元に複数のデータを統合して返却できる
3章 GraphQLの問い合わせ言語
3.2 GraphQLのクエリ
👉 https://snowtooth.moonhighway.com/
教材はこちら(スノートゥース山という架空のスキー場で働いている設定)
query, field
- GraphQLではqueryオペレーションを使用してAPIからデータを取得する
- 取得したいデータをfieldで指定する
- サーバから返却されるデータの構造はqueryで指定したフィールドと同一になる
● name, statusをフィールドに指定してallLiftsのqueryを実行する
- 複数のクエリを書いても実行できるのは一つだけ
GraphQL Playgroundにどっちか選択するよう求められる
- クエリをまとめる
- 1回のリクエストで両方のデータが欲しい場合、一つのクエリを用意する必要がある
● ひとつのクエリにまとめた場合
query liftsAndTrails {
liftCount(status: OPEN)
allLifts {
name
status
}
allTrails {
name
difficulty
}
}
{
"data": {
"liftCount": 6,
"allLifts": [
{
"name": "Astra Express",
"status": "OPEN"
},
{
"name": "Jazz Cat",
"status": "OPEN"
},
{
"name": "Jolly Roger",
"status": "OPEN"
},
{
"name": "Neptune Rope",
"status": "OPEN"
},
{
"name": "Panorama",
"status": "HOLD"
},
],
"allTrails": [
{
"name": "Blue Bird",
"difficulty": "intermediate"
},
{
"name": "Blackhawk",
"difficulty": "intermediate"
},
{
"name": "Duck's Revenge",
"difficulty": "intermediate"
},
{
"name": "Ice Streak",
"difficulty": "intermediate"
},
{
"name": "Parachute",
"difficulty": "intermediate"
},
]
}
}
一つのクエリであらゆる種類のデータをうけとることができる
ルート型, 選択セット
- QueryはGraphQLの型の一つで「ルート型」と呼ばれる
- ルート型はオペレーションに対するGraphQLの型で、クエリドキュメントの一番上位には必ずルート型が指定される
- 取得するフィールドを指定する{}で囲まれたブロックを「選択セット」と呼ぶ
- 選択セットは入れ子にすることができる
query # ルート型
liftsAndTrails
{ # 選択セットここから
liftCount(status: OPEN)
allLifts {
name
status
}
allTrails {
name
difficulty
}
} # 選択セットここまで
JSONレスポンス
- JSONレスポンスはクエリで要求したすべてのデータが含まれる
- フィールド名は選択セットで指定したフィールド名と同じ名前になる
- フィールド名にエイリアスを指定することも可能
● フィールド名にエイリアスを指定した場合
query liftsAndTrails {
liftCount(status: OPEN)
chairLifts: allLifts { # エイリアスの設定
liftName: name
status
}
skiSlopes: allTrails {
name
difficulty
}
}
{
"data": {
"liftCount": 6,
"chairLifts": [
{
"liftName": "Astra Express",
"status": "OPEN"
},
{
"liftName": "Jazz Cat",
"status": "OPEN"
},
{
"liftName": "Jolly Roger",
"status": "OPEN"
},
{
"liftName": "Neptune Rope",
"status": "OPEN"
},
],
"skiSlopes": [
{
"name": "Blue Bird",
"difficulty": "intermediate"
},
{
"name": "Blackhawk",
"difficulty": "intermediate"
},
{
"name": "Duck's Revenge",
"difficulty": "intermediate"
},
{
"name": "Ice Streak",
"difficulty": "intermediate"
},
{
"name": "Parachute",
"difficulty": "intermediate"
},
]
}
}
クエリ引数
- 結果をフィルタリングしたい場合はクエリ引数を利用する
query closedLifts {
allLifts(status: CLOSED) {
name
status
}
}
サービス側で実装は必要
- 選択セットにも引数を指定することが可能
query jazzCatStatus {
Lift(id: "jazz-cat") {
id
name
status
night
elevationGain
}
}
サービス側で実装は必要
GraphQLの型
GraphQLにはスカラー型とオブジェクト型が存在する
スカラー型
プリミティブ型に近い概念
- 整数型(Int)
- 浮動小数点数型(Float)
- 文字列型(String)
- 論理型(Boolean)
- ID型(ID) ※GraphQLの仕様上ユニーク制約がかかる
オブジェクト型
- 1つ以上のスキーマで定義されているフィールドの集合
- 返却されるJSONオブジェクトの構造を規定する
query {
Lift(id: "jazz-cat") {
capacity # 整数型のフィールド
trailAccess { # オブジェクト型のフィールド
name
difficulty
}
}
}
{
"data": {
"Lift": {
"capacity": 2,
"trailAccess": [
{
"name": "Goosebumps",
"difficulty": "advanced"
},
{
"name": "River Run",
"difficulty": "intermediate"
},
{
"name": "Duck's Revenge",
"difficulty": "intermediate"
},
{
"name": "Cape Cod",
"difficulty": "intermediate"
},
{
"name": "Grandma",
"difficulty": "expert"
},
{
"name": "Wild Child",
"difficulty": "advanced"
},
{
"name": "Old Witch",
"difficulty": "expert"
}
]
}
}
}
この例ではリフトとトレイルという2つデータに対する一対多の接続を表現している。
3.3 ミューテーション
- 書き込み操作(作成/更新/削除)を担当する
- 引数で書き込み内容を指定する
- レスポンスにオブジェクトが指定されている場合、何らかのフィールドを指定する必要がある
mutation {
setLiftStatus(id: "jazz-cat", status: HOLD) {
name
status
}
}
{
"data": {
"setLiftStatus": {
"name": "Jazz Cat",
"status": "HOLD"
}
}
}
クエリ変数
- 静的な値だけでなく、変数を使用することでMutationに動的な値を渡すことができる
mutation createSong($title:String! $numberOne:Int $by:String!) {
addSong(title:$title, numberOne:$numberOne, performerName:$by) {
id
title
numberOne
}
}
この例ではcreateSongに渡された$titleがaddSongのtitleに渡される
3.4 サブスクリプション
GraphQLサーバからリアルタイムにデータの更新情報をうけとることができる
● リフトのステータスの変化をサブスクリプションで受け取る
subscription {
liftStatusChange {
name
capacity
status
}
}
リフトの状態が変化したことの通知をWebSocketを通じてリッスンできる。
別タブから下記のMutationを実行し、特定のリフトのステータスを変更してみる。
mutation {
setLiftStatus(id: "jazz-cat", status: CLOSED) {
name
status
}
}
以下のようなデータがサブスクリプションで受け取ることができる
{
"data": {
"liftStatusChange": {
"name": "Jazz Cat",
"capacity": 2,
"status": "CLOSED"
}
}
}
イントロスペクション
- APIスキーマの詳細を取得できる機能
- 以下のようにクエリで__schemaを指定することでAPIスキーマを取得できる
query {
__schema {
types {
name
}
}
}
{
"data": {
"__schema": {
"types": [
{
"name": "Lift"
},
{
"name": "ID"
},
{
"name": "String"
},
{
"name": "Int"
},
{
"name": "Boolean"
},
{
"name": "Trail"
},
{
"name": "LiftStatus"
},
{
"name": "TrailStatus"
},
{
"name": "SearchResult"
},
{
"name": "Query"
},
{
"name": "Mutation"
},
{
"name": "Subscription"
},
{
"name": "CacheControlScope"
},
{
"name": "Upload"
},
{
"name": "__Schema"
},
{
"name": "__Type"
},
{
"name": "__TypeKind"
},
{
"name": "__Field"
},
{
"name": "__InputValue"
},
{
"name": "__EnumValue"
},
{
"name": "__Directive"
},
{
"name": "__DirectiveLocation"
}
]
}
}
}
- 特定の方の詳細が知りたいときは__typeを引数とともに指定してクエリを発行する
query liftDetails {
__type(name:"Lift") {
name
fields {
name
description
type {
name
}
}
}
}
{
"data": {
"__type": {
"name": "Lift",
"fields": [
{
"name": "id",
"description": "The unique identifier for a `Lift` (id: \"panorama\")",
"type": {
"name": null
}
},
{
"name": "name",
"description": "The name of a `Lift`",
"type": {
"name": null
}
},
{
"name": "status",
"description": "The current status for a `Lift`: `OPEN`, `CLOSED`, `HOLD`",
"type": {
"name": "LiftStatus"
}
},
{
"name": "capacity",
"description": "The number of people that a `Lift` can hold",
"type": {
"name": null
}
},
{
"name": "night",
"description": "A boolean describing whether a `Lift` is open for night skiing",
"type": {
"name": null
}
},
{
"name": "elevationGain",
"description": "The number of feet in elevation that a `Lift` ascends",
"type": {
"name": null
}
},
{
"name": "trailAccess",
"description": "A list of trails that this `Lift` serves",
"type": {
"name": null
}
}
]
}
}
}
- ルート型で指定できるフィールドを確認できるクエリ
- どんなqueryやmutationがあるかを返却してくれる
query root {
__schema {
queryType {
...typeFields
}
mutationType {
...typeFields
}
subscriptionType {
...typeFields
}
}
}
fragment typeFields on __Type {
name
fields {
name
}
}
{
"data": {
"__schema": {
"queryType": {
"name": "Query",
"fields": [
{
"name": "allLifts"
},
{
"name": "allTrails"
},
{
"name": "Lift"
},
{
"name": "Trail"
},
{
"name": "liftCount"
},
{
"name": "trailCount"
},
{
"name": "search"
}
]
},
"mutationType": {
"name": "Mutation",
"fields": [
{
"name": "setLiftStatus"
},
{
"name": "setTrailStatus"
}
]
},
"subscriptionType": {
"name": "Subscription",
"fields": [
{
"name": "liftStatusChange"
},
{
"name": "trailStatusChange"
}
]
}
}
}
}
queryにallLifts, allTrails, searchそのほか、
mutationにsetLiftStatus, setTrailStatus、
subscriptionにliftStatusChange, trailStatusChange
があることがわかる。
所感
「初めてのGraphQL」1章, 3章について掻い摘んでお伝えしました。
以下、取り止めもない所感です。
RESTと比較した場合、GraphQLはデフォルトで型安全なAPI通信を実現できる点が強いなあと思いました。
バックエンドとフロントエンドでチームが分かれている場合や
マイクロサービスアーキテクチャを採用している場合は
型定義がそのままチーム間/サービス間のインターフェースになるので意思疎通や実装がしやすそうです。
また、クライアント側で取得するデータ構造を柔軟に決められるのも魅力的だなと思った反面、
サーバ側の実装に気をつけないと簡単にN+1問題などが発生することもありそうだなあと思いました(対策のためにDataLoaderなる仕組みをFacebookが提供したりしていますが)。
APIに限った話ではありませんが、
何かしら技術スタックを採用するときは
「どんな選択肢があるのか」
「それぞれのメリデメは何か」
「プロダクトやPJメンバーの状況に即しているか」
などを考えるのが結局は大事だよな〜という所感です。
長々と書いた割には「GraphQLはいいぞ」という〆にならず消化不良を起こしてしまった方には申し訳ありません。
読んでいただきありがとうございました。