初めての GraphQL 読書メモ

オライリーから出ている はじめての GraphQL の自分向け読書メモ

GraphQLとは

  • GraphQL: API のための問い合わせ言語
  • プロトコルの指定はないが一般的に HTTP プロトコル が使われる

Hello, GraphQL

SWAPI で気軽に試せるのでまずは触ってみる。以下のようにデータのやりとりができる。

query {
    film(filmID: 1) {
    title,
    director
  }
}


// returns
{
  "data": {
    "film": {
      "title": "A New Hope",
      "director": "George Lucas"
    }
  }
}

設計原則

  • 階層構造: クエリは階層構造になっており、レスポンスと同じ構造を取る
  • プロダクト中心: クライアントの言語およびランタイムに従って実装される
  • 強い型付け: それぞれのフィールドは固有の型を持ち、バリデーションされることが GraphQL の型システムに保証されている
  • クライアントごとのクエリ: クライアントが必要とするクエリに対するレスポンスを提供する
  • 自己参照: GraphQL サーバー自身の型システムを問い合わせられる

なぜ GraphQL を使うのか

  • REST
    • リソースをアクションで状態変化させられるリソース志向アーキテクチャ
    • URI は情報に対応する
  • REST の課題
    • アプリケーションが必要としない余分なデータが得られる場合が多々ある(過剰な取得)
    • アプリケーションが必要とするデータが不足している場合も多々あり、再リクエストを行うといったことが多々発生する(過小な取得)
    • しばしばアプリケーションに最適化したエンドポイントが作られるが変更に弱く、管理するエンドポイントが増える傾向にある(エンドポイントの管理)
  • GraphQL の解決策
    • GraphQL 言語により必要なフィールドだけを指定してクエリすることにより過小・過剰な取得を防ぐ
    • 単一のエンドポイントに対して GraphQL 言語によるクエリを行う形にすることにより管理するエンドポイントは一つとなる
  • GraphQL の実際
    • 多くの組織では GraphQL と REST を併用している
    • 漸進的に取り入れていこう
    • GraphQL は単なる仕様でしかなく、様々な環境から利用可能

GraphQL 関連のツール

  • GraphiQL: ブラウザから利用できる GraphQL API の統合開発環境
    • 入力補完、シンタックスハイライト、構文エラー表示、クエリ実行結果確認などの機能
  • GraphQL Playground: GraphiQL と似ているが、HTTP ヘッダの書き換えが可能などいくつかの GraphiQL にない機能がある
  • 公開されている GraphQL API
    • SWAPI(スターウォーズ API)
    • GitHub API
    • Yelp

グラフ理論のさわり

  • グラフ: データを含むオブジェクト(ノード, 頂点)とコネクション(エッジ)から構成される
    • グラフ G = (頂点 V, エッジ E )
    • V = {1, 2, 3}, E ={ {1,2}, {2, 3} } のように表現できる
  • 無向グラフ は、エッジのリストの順番の入れ替えに対して不変
  • 有向グラフ は、エッジに向きがある、この場合 E = ( {1,2}, {2, 3} ) のように表現する
  • ノード A と B がエッジでつながっているとき、隣接している という
  • ノード A につながるエッジの数のことを、エッジの次数 という
    • ノードの次数がすべて奇数だと、すべてのエッジを一度だけ通ってノードを巡回する方法がないことをオイラーが気づいた
    • すべてのエッジを一度だけ通ってノードを巡回できるようなグラフを オイラー路 とよぶ
  • 閉路: 開始と終了のノードが同一になるグラフの経路
  • オイラー閉路: 閉路のうち、すべてのエッジを一度だけ通ってノードを巡回できるような経路
  • : 根や開始ノードがあるグラフ
    • ノードを根から辿る際に根側にあるノードを 、そうでないノードを とよぶ
    • 特に子を持たないノードを とよぶ
    • 根からのノードまでの距離を 深さ とよぶ
  • 二分木: ノードが高々 2 つの子しか持たない木
  • 二分探索木: ノードが特別な順番で並んでいる二分木

実世界との比較

  • Twitter のフォロー/フォロワー関係: 有向グラフ
  • Facebook の友人関係: 無向グラフ
    • あるユーザーを起点にした友人関係のデータのリクエストは木構造になる
    • これは GraphQL のクエリによく似ている
- person
  - name
  - location
  - friends
    - friend name
    - friend location

GraphQL 言語

  • GraphQL は SQL と同じく問い合わせ言語(Query Language)
  • データの取得には query コマンド 、データの操作には mutation コマンドを使う
  • データの変更を監視する subscription コマンドもある
  • GraphQL には スカラー型オブジェクト型 が存在する
    • スカラー型: Int, Float, String, Boolean, ID
    • オブジェクト型: 一つ以上のスキーマで定義されているフィールドの集合

query の実例

$ # succeeded
$ curl http://snowtooth.herokuapp.com/ \
    -H 'Content-Type: application/json' \
    --data '{"query": "{ allLifts { name } }"}'
{"data":{"allLifts":[{"name":"Astra Express"},{"name":"Jazz Cat"},{"name":"Jolly Roger"},{"name":"Neptune Rope"},{"name":"Panorama"},{"name":"Prickly Peak"},{"name":"Snowtooth Express"},{"name":"Summit"},{"name":"Wally's"},{"name":"Western States"},{"name":"Whirlybird"}]}}

$ # error
$ curl http://snowtooth.herokuapp.com/ \
    -H 'Content-Type: application/json' \
    --data '{"query": "{ allLifts { hoge } }"}'
{"errors":[{"message":"Syntax Error: Expected Name, found <EOF>","locations":[{"line":1,"column":21}],"extensions":{"code":"GRAPHQL_PARSE_FAILED","exception":{"stacktrace":["GraphQLError: Syntax Error: Expected Name, found <EOF>","    at syntaxError (/app/node_modules/graphql/error/syntaxError.js:15:10)","    at Parser.expectToken (/app/node_modules/graphql/language/parser.js:1404:40)","    at Parser.parseName (/app/node_modules/graphql/language/parser.js:94:22)","    at Parser.parseField (/app/node_modules/graphql/language/parser.js:291:28)","    at Parser.parseSelection (/app/node_modules/graphql/language/parser.js:280:81)","    at Parser.many (/app/node_modules/graphql/language/parser.js:1518:26)","    at Parser.parseSelectionSet (/app/node_modules/graphql/language/parser.js:267:24)","    at Parser.parseField (/app/node_modules/graphql/language/parser.js:308:68)","    at Parser.parseSelection (/app/node_modules/graphql/language/parser.js:280:81)","    at Parser.many (/app/node_modules/graphql/language/parser.js:1518:26)"]}}}]}

mutation の実例

$ curl http://snowtooth.herokuapp.com/ \
    -H 'Content-Type: application/json' \
    --data '{"query": "{ Lift(id: \"panorama\") { name status } }"}'
{"data":{"Lift":{"name":"Panorama","status":"OPEN"}}}

$ curl http://snowtooth.herokuapp.com/ \
     -H 'Content-Type: application/json' \
     --data '{"query": "mutation { setLiftStatus(id: \"panorama\" status: CLOSED) { name status } }"}'
{"data":{"setLiftStatus":{"name":"Panorama","status":"CLOSED"}}}

$ curl http://snowtooth.herokuapp.com/ \
    -H 'Content-Type: application/json' \
    --data '{"query": "{ Lift(id: \"panorama\") { name status } }"}'
{"data":{"Lift":{"name":"Panorama","status":"CLOSED"}}}

クエリ

  • 一度に送信できるクエリはひとつまでなので、複数のクエリを行いたい場合はひとつのクエリにまとめる
  • 指定したフィールドのことを 選択セット とよぶ
// これは NG
query {
  allLifts {
    name
    status
  }
  liftCount
}

query {
  allTrails {
    name
  }
  trailCount
}

// 以下のようにmatomeru
query {
  allLifts {
    name
    status
  }
  liftCount
  allTrails {
    name
  }
  trailCount
}

クエリ引数

結果をフィルタリングできる

query {
    allLifts(status: CLOSED) {
    name
  }
}

エイリアス

返却される際のフィールドの名前を指定できる

query {
  Lift(id: "jazz-cat") {
    liftname: name
    status
  }
}

フラグメント

複数の場所で使いまわせる選択セットのことをフラグメントとよぶ

// Trail に関する情報が繰り返し登場している
query {
  Lift(id: "summit") {
    name
    status
    trailAccess {
      name
      difficulty
    }
  }
  Trail(id: "fish-bowl") {
    name
    difficulty
  }
}

// fragment を用いると以下のようにまとめられる
query {
  Lift(id: "summit") {
    name
    status
    trailAccess {
      ...trailInfo
    }
  }
  Trail(id: "fish-bowl") {
    ...trailInfo
  }
}

fragment trailInfo on Trail {
  name
  difficulty
}

ユニオン型

Either みたいなやつ

query {
  entry {
    ...on Directory {
        path
    }
    ...on File {
        path
        content
    }
  }
}

// 以下のようにも書ける
query {
  entry {
    ...dir
    ...file
  }
}

fragment dir on Directory {
  path
}

fragment file on File {
  path
  content
}

インターフェース

よくあるインターフェースのようなやつ(雑)

query {
  entry {
    path
  }
}

ミューテーション

データの操作を行うコマンド、操作後の返り値としてほしいものをフィールドとして指定できる

mutation closeLift {
  setLiftStatus(id: "jazz-cat" status: CLOSED) {
    name
    status
  }
}

クエリ変数

クエリ内の値を置き換えられる

mutation closeLift($liftId: ID!) {
  setLiftStatus(id: $liftId, status: CLOSED) {
    name
    status
  }
}

// 変数設定は以下のようにする
{
  "liftId": "jazz-cat"
}

サブスクリプション

変更を通知として受け取れる

subscription {
  liftStatusChange {
    name
    status
  }
}

イントロスペクション

API スキーマの詳細を取得できる機能

query {
  __schema {
    types {
      name
      kind
    }
  }
  __type(name: "Lift") {
    name
    kind
    description
  }
}

GraphQL SDL

  • GraphQL のスキーマを定義する言語として GraphQL SDL(Schema Definition Language) がある
  • スキーマファイルの拡張子は .graphql

スカラー型

以下のように ID, String, Int, Float, Boolean といったスカラー型を定義でき、! は non-null を表す

type User {
  id: ID!
  name: String!
  age: Int!
  verified: Boolean!
  score: Float
}

カスタムスカラー型

以下のように scalar キーワードでカスタムスカラー型を定義できる

scalar DateTime

type Record {
  id: ID!
  created: DateTime!
}

Enum

以下のように enum キーワードで Enum 型を定義できる

enum Status {
  OPEN
  CLOSED
}

type Store {
  status: Status!
}

リスト

以下のように [] を使ってリストを定義できるが、要素とリスト自体の null 許容を両方していできる点、結構考えられている

enum Topping {
  NINNIKU
  YASAI
  ABURA
  KARAME
}

type Store {
  toppings: [Topping!]!
}

オブジェクトの一対一の接続

単純にある型が他の型に内包されているパターン

type User {
    id: ID!
    name: String!
}

type Post {
    id: ID!
    postedBy: User!
}

オブジェクトの一対多の接続

単純にある型のリストが他の型に内包されているパターン

type Character {
    id: ID!
    name: String!
}

type VoiceActor {
    id: ID!
    characters: [Character!]!
}

また Query ルート型には次のようにフィールドの追加ができる

type Query {
  allCharcters: [Character!]!
  totalCharacters: Int!
}

schema {
  query: Query
}

オブジェクトの多対多の接続

双方向の一対多の関係

type User {
    id: ID!
    name: String!
    posts: [Post!]!
}

type Post {
    id: ID!
    title: String!
    content: String!
    editedBy: [User!]!
}

ユニオン型

あるあるな表記で Good!

union Entry = Directory | File

type Directory {
  path: String!
}
type File {
  path: String!
  content: String!
}

インターフェース

あるあるな表記で Good!

interface Entry {
  path: String!
}

type Directory implements Entry {
}

type File implements Entry {
  content: String!
}

引数

Query に定義してやればよい

type Query {
  // User 型を返す
  User(id: ID!): User!
  // Post 型の title, content だけを要求
  Post(id: ID!) {
    title
    content
  }
}

データのフィルター、ページング、ソートなども引数を使って表現できる

enum Category {
  ...
}
enum Sort {
  ...
}

type Query {
  allPosts(category: Category!)
  allPosts(limit: Int=50, offset: Int=0, sort: Sort=CREATED_DESC): [Post!]!  
}

ミューテーション

ルート型の Mutation に追加していく

type Mutation {
  createPost(
    title: String!
    content: String!
    postedBy: User!
  ): Post!
}

schema {
  query: Query
  mutation: Mutation
}

入力型

多数の引数をまとめられる型で引数にのみ利用できる

input CreatePostInput {
    title: String!
    content: String!
    postedBy: User!
}

type Mutation {
  createPost(input: CreatePostInput): Post!
}

サブスクリプション

subscription ルート型に定義する

type Subscription {
    newPost: Post!
}

schema {
    query: Query
    mutation: Mutation
    subscription: Subscription
}

ドキュメンテーション

" で囲うらしい

"""
ユーザー
"""
type User {
    id: ID!
    """
    ユーザー名
    """
    name: String!
    posts: [Post!]!
}

type Query {
  User(
    "ユーザーID"
    id: ID!
  ): User!
}

GraphQL サーバーの実装

Apollo Server を使って実際に GraphQL サーバーを実装してみる

Hello, GraphQL

次のようなコードを実装する

const { ApolloServer } = require(`apollo-server`);

const typeDefs = `
  type Query {
    totalEvents: Int!
  }
`;

const resolvers = {
  Query: {
    totalEvents: () => 100
  }
};

const server = new ApolloServer({
  typeDefs,
  resolvers
});

server.listen().then(({ url }) => console.log(`Running on ${url}`));

npm start でサーバーを起動すると以下のようにクエリできるようになっている

{
  totalEvents
}

// return
{
  "data": {
    "totalEvents": 100
  }
}

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください