LoginSignup
112
85

More than 3 years have passed since last update.

GraphQLに入門する

Last updated at Posted at 2019-12-14

はじめに

前回の記事で GraphQL を使った開発環境が構築できたので、やれ開発だーと意気込んでいたところ、GraphQL のスキーマ定義で出鼻をくじかれました。
Prisma は DB マイグレーションツールって点だけ見てましたけど、GraphQL の知識はやっぱり必須ですよね……

というわけで、まずは GraphQL についての勉強と備忘録を兼ねて入門記事を書こうと思います。
本記事の目標は、GraphQL を完全に理解したと言えるようになることです。

そもそも GraphQL って?

GraphQL は、クエリ言語(QL)の一種です。で、クエリ言語って?

ちょっと私のグーグル力に問題があるようで、以下にはあやふやな情報が含まれます。マサカリは随時受付中です。
クエリ言語(Query Language)は、データの問い合わせを行うための言語です。現在、普遍的に使われている SQL もその一種ですね。最近ではAmazon も PartiQL というクエリ言語を発表したそうです
すなわち、今回取り上げる GraphQL もデータの問い合わせを行うための言語です。

GraphQL は、クライアント-サーバー間のデータ問い合わせに特化した言語です。
GraphQL には、データ問い合わせを行うクエリ言語とは別に、データの構造を定義するスキーマ言語をもち、このスキーマ言語によってデータ構造の型定義を行うことができます。定義された型を用いることにより、型安全なデータ通信が可能になります。

つまり、何がどう便利になるの?

Web API の運用が簡単になるそうです。
API というと、REST APIが真っ先に浮かぶ人は多いと思います。私もそうです。
REST API と GraphQL の比較はこちらでも行われていますが、一応ここでも軽く触れておきましょう。

REST API では、アクセスするリソースを URI で、リソースに対する操作をリクエストメソッドで規定していました。

例えば、以下のようなデータ構造が存在するとします。

qiita.png

このとき、REST API では以下のエンドポイントが必要になります。

- 飼い主情報問い合わせエンドポイント(/owners)
  - 飼い主情報確認(/owners/{id} - GET)
  - 飼い主新規登録(/owners/{id} - POST)
  - 飼い主情報更新(/owners/{id} - PUT)
  - 飼い主登録削除(/owners/{id} - DELETE)
  - ペット情報問い合わせエンドポイント(/owners/{id}/pets)
    - 飼い主情報確認(/owners/{id}/pets/{id} - GET)
    - 飼い主新規登録(/owners/{id}/pets/{id} - POST)
    - 飼い主情報更新(/owners/{id}/pets/{id} - PUT)
    - 飼い主登録削除(/owners/{id}/pets/{id} - DELETE)

この通り、リソースごとにエンドポイントが分割されており、その操作はリクエストメソッドという形で表現されます。
これは以下のメリットがあります。

  • アクセスする対象のリソースがわかりやすい
  • リソースに対する操作もリクエストメソッドという形で表現されているためわかりやすい

一方で、以下のようなデメリットもあります。

  • 1 度のリクエストにつき 1 つ(1 種類)のリソースしか取得できない
    • 複数のリソースを取得したい場合は何度もリクエストを送信する必要がある
  • リソースの種類が増えるのに伴い、エンドポイントも増えていく

プロジェクトが肥大化するほど、上記のデメリットは無視できない大きさになっていきます。

GraphQL は、この問題を解消することができるのです。
先ほどの例で言えば、 GraphQL で置き換えた場合に必要なエンドポイントは以下の通りです。

- 問い合わせエンドポイント(/graphql - POST)

この通り、エンドポイントもリクエストメソッドも一本化されます。同じエンドポイントで、複数のリソースにアクセスすることができるようになるわけですね。
ちなみに、上記図では全部 POST ですが、GET でも問題なく動作するようです。
REST と比較して、GraphQL を使うことによるメリットは以下の通りです。

  • 1 度のリクエストで複数のリソースを取得できる
    • 複数のリソースにまたがるデータを取得する場合でも、リクエストは 1 度で済む
  • リソースが増えてもエンドポイントは 1 つのまま

デメリットについてはまだよくわかっていないので、ここでは一旦横に置かせていただきます。
誰だってリクエストを何回も投げるなんてしたくないですし、GraphQL が騒がれる理由もわかってきました。

GraphQL でできること、できないこと

GraphQL の仕様書を一部抜粋すると、こんなことが書かれています。

GraphQL is a query language designed to build client applications by providing an intuitive and flexible syntax and system for describing their data requirements and interactions.
GraphQLは、データ要件と相互作用を記述するための直感的で柔軟な構文とシステムを提供することにより、クライアントアプリケーションを構築するために設計されたクエリ言語です。(Google翻訳)
GraphQL is not a programming language capable of arbitrary computation, but is instead a language used to query application servers that have capabilities defined in this specification. GraphQL does not mandate a particular programming language or storage system for application servers that implement it. Instead, application servers take their capabilities and map them to a uniform language, type system, and philosophy that GraphQL encodes. This provides a unified interface friendly to product development and a powerful platform for tool‐building.
GraphQLは、任意の計算が可能なプログラミング言語ではなく、この仕様で定義されている機能を持つアプリケーションサーバーのクエリに使用される言語です。GraphQLは、それを実装するアプリケーションサーバーに特定のプログラミング言語またはストレージシステムを強制しません。代わりに、アプリケーションサーバーはその機能を利用して、GraphQLがエンコードする統一言語、型システム、および哲学にそれらをマッピングします。これにより、製品開発に適した統合インターフェースと、ツール構築用の強力なプラットフォームが提供されます。(Google翻訳)

GraphQL はあくまで Web API へのデータ問い合わせを行うための言語です。すなわち、Web API を提供するサーバーの実装や、API サーバー-データベース間の処理等に関しては関与しないのです。
GraphQL の言語仕様を利用した DB マイグレーションツールなどもありますが、それらは例外と見ていいでしょう。
よって、本記事でもクライアント-サーバー間通信のみを想定した内容となります。

GraphQL を用いたリクエスト

ここで、基本的な GraphQL のリクエスト・レスポンスの流れについて確認していきます。参考資料は公式のこちら

GET リクエストの場合

ユーザーの一覧を取得する場合について考えてみましょう。このとき、ユーザー一覧を取得する GraphQL クエリは以下の文で表せるとします。

{
  users {
    id
    name
  }
}

GET リクエストの場合、上記を圧縮してクエリ文字列に突っ込むだけです。

http://myapi/graphql?query={users{id,name}}

GraphQL クエリをクエリ文字列の query パラメータに入れるわけですね。クエリとクエリと query でごっちゃになりそうです。

POST リクエストの場合

POST の場合、2 つの方法があります。
1 つ目は JSON 形式で送信する方法です。当然ですが、ヘッダのContent-Typeとしてapplication/jsonを指定します。

{
  "query": "...",
  "operationName": "...",
  "variables": { "myVariable": "someValue", ... }
}

それぞれのフィールドに関する説明は以下の通りです。

  • query: GraphQL クエリ本文(GET リクエストの query と同じ)
  • operationName: 実行するクエリの名前(クエリを複数含む場合は必須)
  • variables: query 中で使われる変数

常に必須なのは query のみであり、operationName と variables はオプショナルです。

JSON 形式とは異なる方法として、GraphQL のクエリ形式で送信する方法もあります。
Content-Typeapplication/graphqlを指定すればいいようです。

レスポンスについて

レスポンスは基本的に以下の JSON 形式文字列で返されます。

{
  "data": {...},
  "errors": [...]
}

言うまでもなく data はクエリ結果、errors はエラーです。
GraphQL の仕様書によると、正常に処理が実行できた場合は errors を含まないべきであり、また処理実行前にエラーが発生した場合は data を含まないべきであるそうです。

GraphQL 詳細

ここからは、実際に GraphQL の仕様について確認していきます。基本的に公式ページの焼き増しです。

スキーマ言語について

GraphQL には型があります。この型定義を行うことができるのがスキーマ言語です。
あらゆる GraphQL を介した通信は、このスキーマ言語による型定義を元にしています。

スカラー型

スカラー(scalar)とは、物理学において大きさのみを持つ量のことだそうです(Wikipedia 調べ)
QraphQL においては、数値や文字などの実データを表す型として用いられるようです。

まず、QraphQL においてデフォルトで定義されている型を紹介します。

  • Int: 符号付きの 32 ビット整数
  • Float: 符号付きの倍精度浮動小数点数
  • String: UTF-8 の文字列
  • Boolean: trueもしくはfalse
  • ID: オブジェクトの再取得やキャッシュのキーとして用いられる、一意な識別子
    • この型は同様の手段で文字列にシリアル化される
    • この型をフィールドに与えるということは、そのフィールドは人間が読むためのものではないことを意味する

ID だけちょっと特殊ですが、他の型はよく見る型ですね。
GraphQL は上記以外にも、ユーザー独自に型を定義することができます。Dateという型を定義したい場合はこんな感じ。

scalar Date

ただし、独自定義したスカラー型について、ユーザーは必要に応じてその変換処理を記述しなければなりません。上記Date型で言うのであれば、年月日までなのか、秒あるいはミリ秒までなのか、フォーマットはどうするかなどについて記述するべきでしょう。
ただし、このスカラー型の変換については GraphQL のスキーマ定義範囲外です。GraphQL ライブラリによって異なる記述になるので、利用するライブラリのドキュメントを確認してみましょう。たとえばgqlgenならこのページです。

オブジェクトの型とフィールド

ここでは基本的な型の記述について確認していきましょう。
GraphQL の型定義は、型名とフィールドからなります。

type Character {
  name: String!
  appearsIn: [Episode!]!
}

上記の例について確認してみましょう。

  • Characterというオブジェクトの型を定義している
  • nameappearsInの 2 つのフィールドを持つ
  • Stringは文字列のスカラー型
    • String!と最後に!をつけることで、そのフィールドが null にならないことを示す
  • Episodeはユーザー定義の型
    • [Episode][]で囲むことにより、配列であることを示す
    • !がついているので、このフィールドは null にならないことを示している

直感的でわかりやすいかと思います。
簡単なオブジェクトならこれまでの知識で十分記述できそうですね。

引数を持つフィールド

フィールドには引数を定義することができます。以下の例を見てみましょう。

type Starship {
  id: ID!
  name: String!
  length(unit: LengthUnit = METER): Float
}

lengthフィールドに引数としてLengthUnit型のunitが指定されていますね。
引数は必須および任意で指定することが可能です。必須としたい場合は、null 不可を示す!を引数の型につけましょう。任意とした場合、引数にデフォルト値を指定することも可能です。上記の例はまさしくデフォルト値を指定されていますね。
上記例だと、Float型の長さフィールドに、引数としてLengthUnitの単位を、デフォルトとしてMETER(メートル)として指定しています。おそらく、スターシップの長さがメートル単位で取得できるのでしょう。

Query 型と Mutation 型

スキーマ定義において、Query 型と Mutation 型は特殊な意味を持ちます。この 2 つは API におけるエンドポイントを示すことができるのです。

具体的に見ていきましょう。
例えば、以下のようなクエリ文を GraphQL サーバーに送り、データが取得できたとします。

クエリ文
query {
  hero {
    name
  }
  droid(id: "2000") {
    name
  }
}
レスポンスデータ
{
  "data": {
    "hero": {
      "name": "R2-D2"
    },
    "droid": {
      "name": "C-3PO"
    }
  }
}

リクエストしたクエリ文には、heroおよびdroidの 2 文が含まれていました。結果もそれぞれ取得できていますね。
これはすなわち、GraphQL のエンドポイントにheroおよびdroidが用意されていることを示しています。

GraphQL におけるエンドポイントの定義は、冒頭でも触れたように Query 型もしくは Mutation 型を利用します。
今回の例で言えば、例えば以下のような型定義が必要です。

type Query {
  hero(episode: Episode): Character
  droid(id: ID!): Droid
}

Query型にheroフィールドおよびdroidフィールドが定義されていますね。ちなみに、heroでは引数が任意となっていますが、droid型では必須です。
このように、Query 型に登録されたフィールドがエンドポイントとして登録されますが、型定義そのものは通常のオブジェクトと変わりません。覚えることが少なくてありがたいですね。

Query 型と Mutation 型の違いについてですが、この 2 つは行える処理が異なります。
GraphQL において、Query はデータの取得(GET)、Mutation はデータの変更(POST、PUT)を意味します。データの変更を許さず取得のみを可能とする API では、Query 型のみを定義すればいいわけですね。

列挙型

みんな大好き列挙型。あまり説明の必要はないでしょう。

enum Episode {
  NEWHOPE
  EMPIRE
  JEDI
}

Episode型として指定されたフィールドには、NEWHOPEEMPIREJEDIのいずれかしか入らないことを示すことができます。

インターフェース

様々なシステムで見られるインターフェースと似通ったものだそうです。
以下のようなインターフェースがあるとします。

interface Character {
  id: ID!
  name: String!
  friends: [Character]
  appearsIn: [Episode]!
}

このCharacterインターフェースを実装する型では、idnamefriendsappearsInの 4 フィールドを実装する必要があります。
例として、Character インターフェースを実装したHuman型とDroid型を見てみましょう。

type Human implements Character {
  id: ID!
  name: String!
  friends: [Character]
  appearsIn: [Episode]!
  starships: [Starship]
  totalCredits: Int
}

type Droid implements Character {
  id: ID!
  name: String!
  friends: [Character]
  appearsIn: [Episode]!
  primaryFunction: String
}

Character インターフェースで定義されていたフィールド以外については自由に実装することができます。Human型ではstarshipstotalCreditsDroid型ではprimaryFunctionが実装されていますね。
同じインターフェースを実装している場合でも、型名が異なれば別個の型です。例として、以下のクエリ文と結果を見てみましょう。

クエリ文
query HeroForEpisode($ep: Episode!) {
  hero(episode: $ep) {
    name
    primaryFunction
  }
}
変数
{
  "ep": "JEDI"
}
レスポンスデータ
{
  "errors": [
    {
      "message": "Cannot query field \"primaryFunction\" on type \"Character\". Did you mean to use an inline fragment on \"Droid\"?",
      "locations": [
        {
          "line": 4,
          "column": 5
        }
      ]
    }
  ]
}

heroは Character 型の値を取得するクエリです。すなわち、このクエリ文では"ep": "JEDI"に該当する値をもつ Character を取得しようとしているわけです。
一方で、primaryFunctionは Droid 型にのみ実装されたフィールドであり、Character 型のすべてに含まれているとは限りません。そのため、エラーが発生しました。

取得できたデータが Droid 型の場合だったとき、一緒にprimaryFunctionの値を取得したい、といったこともあるでしょう。
その場合、インラインフラグメントを使えば上手く取得することができます。

クエリ文
query HeroForEpisode($ep: Episode!) {
  hero(episode: $ep) {
    name
    ... on Droid {
      primaryFunction
    }
  }
}
変数
{
  "ep": "JEDI"
}
レスポンスデータ
{
  "data": {
    "hero": {
      "name": "R2-D2",
      "primaryFunction": "Astromech"
    }
  }
}

ユニオン型

ユニオン型は TypeScript にもありますね。基本的にはあんな感じです。

union SearchResult = Human | Droid | Starship

上記の場合、SearchResultとしてHumanDroidStarshipの 3 つを指定しています。これはSearchResult型を指定したフィールド等では、その実装としてHumanDroidStarshipのいずれかが入ることを意味します。
例えば、SearchResult 型の結果を返すsearchクエリが存在する場合に、以下のような処理が行えます。

クエリ文
{
  search(text: "an") {
    __typename
    ... on Human {
      name
      height
    }
    ... on Droid {
      name
      primaryFunction
    }
    ... on Starship {
      name
      length
    }
  }
}
レスポンスデータ
{
  "data": {
    "search": [
      {
        "__typename": "Human",
        "name": "Han Solo",
        "height": 1.8
      },
      {
        "__typename": "Human",
        "name": "Leia Organa",
        "height": 1.5
      },
      {
        "__typename": "Starship",
        "name": "TIE Advanced x1",
        "length": 9.2
      }
    ]
  }
}

まず、__typenameは型名を表す文字列ですね。クライアントはこの文字列をもとに、取得できたデータの型を判断できます。
次は...on XXX。先ほども出てきたインラインフラグメントですね。見ればそのままだと思いますが、型に応じて取得するフィールドを選択しています。
ちなみに、Human 型と Droid 型はどちらも Character 型を実装しているため、以下のように書くことも可能です。

{
  search(text: "an") {
    __typename
    ... on Character {
      name
    }
    ... on Human {
      height
    }
    ... on Droid {
      primaryFunction
    }
    ... on Starship {
      name
      length
    }
  }
}

入力型

入力型は、クエリの引数として用いられるフィールドの組み合わせを定義したものです。
例えば、ReviewInput型を以下のように定義します。

input ReviewInput {
  stars: Int!
  commentary: String
}

typeではなくinputを使っているのがミソです。
このとき、ReviewInput 型を用いるCreateReviewForEpisodeクエリが存在する場合、以下のような処理が行えます。

クエリ文
mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) {
  createReview(episode: $ep, review: $review) {
    stars
    commentary
  }
}
変数
{
  "ep": "JEDI",
  "review": {
    "stars": 5,
    "commentary": "This is a great movie!"
  }
}
レスポンスデータ
{
  "data": {
    "createReview": {
      "stars": 5,
      "commentary": "This is a great movie!"
    }
  }
}

実装を確認してはいませんが、これはエピソードごとの感想を登録するためのクエリだと推測することができると思います。すなわち、ReviewInput 型は感想を登録するために必要な情報を示している、と捉えることもできますね。
このように、「ある操作を行うために必要な情報」について、入力型という形式で表現することができるわけです。
ただし、入力型は通常の型と違い、フィールドに引数を設定することはできないので注意しましょう。

クエリ言語

ここからはクエリ言語についてです。クエリ言語を用いることにより、データの問い合わせやデータ変更依頼を行うことができます。

フィールド

ごくごくシンプルなクエリの例として、以下のクエリ文を見てみましょう。

クエリ文
{
  hero {
    name
  }
}
レスポンスデータ
{
  "data": {
    "hero": {
      "name": "R2-D2"
    }
  }
}

heroはスキーマ言語の項目で見たように、Query 型に登録されたフィールドを呼び出しています。
nameは上記 hero の持つフィールドのうち、取得したいフィールドを指定しているわけです。結果として、"name": "R2-D2"が取得できています。

hero では Character 型が取得できるため、name 以外にもappersInなどのフィールドを持っているはずです。しかし、ここでは指定したnameのみが返却されています。
このように、必要なフィールドのみを選択的に取得することができるため、無駄なデータでクライアントのメモリを圧迫しにくくなるわけですね。

ここで、例えば Character 型のフィールドとしてfriendsを持ち、かつfriendsはオブジェクトの配列だったとします。
その場合、以下のような処理を行うこともできます。

クエリ文
{
  hero {
    name
    # Queries can have comments!
    friends {
      name
    }
  }
}
レスポンスデータ
{
  "data": {
    "hero": {
      "name": "R2-D2",
      "friends": [
        {
          "name": "Luke Skywalker"
        },
        {
          "name": "Han Solo"
        },
        {
          "name": "Leia Organa"
        }
      ]
    }
  }
}

フィールドがオブジェクトであれば、上記のように取得するフィールドを指定することができるわけです。
Query 型もオブジェクトであると考えれば、どんなクエリ文も取得するフィールドを指定しているだけと考えられますね。

引数

クエリ文には引数を渡すことができます。例えば、ID が 1000 である人間を取得するクエリは以下の通りです。

クエリ文
{
  human(id: "1000") {
    name
    height
  }
}
レスポンスデータ
{
  "data": {
    "human": {
      "name": "Luke Skywalker",
      "height": 1.72
    }
  }
}

REST でデータを取得する場合、値をリクエストに含めたいときは URL もしくはクエリパラメータに入れる形になります。
一方で GraphQL の場合、ネストしたオブジェクトのフィールドそれぞれに引数を指定することができます。よって、取得するデータを柔軟に選択することができるようになるのです。
上記の例でいえば、例えば身長をメートル表記からフィート表記に変えたい場合は以下のようになります。

クエリ文
{
  human(id: "1000") {
    name
    height(unit: FOOT)
  }
}
レスポンスデータ
{
  "data": {
    "human": {
      "name": "Luke Skywalker",
      "height": 5.6430448
    }
  }
}

上記の例ですが、human に与える引数と height に与える引数では役割が異なります。
human に与える引数は取得するデータの選択、すなわち SQL でいうWHEREです。一方で、height の場合はデータ形式の指定をしており、おそらくこれはメートル => フィート変換処理のトリガーとなります。
このように、引数を用いた場合の振る舞いを柔軟に指定できることも、GraphQL の面白いところではないかと思います。

エイリアス

フィールドにはエイリアス(別名)をつけることができます。
例えば hero クエリを 2 つまとめて発行したい場合、そのままでは同じ hero なので両者の区別がつきません。
そこで、エイリアスをつけることで区別できるようになります。

クエリ文
{
  empireHero: hero(episode: EMPIRE) {
    name
  }
  jediHero: hero(episode: JEDI) {
    name
  }
}
レスポンスデータ
{
  "data": {
    "empireHero": {
      "name": "Luke Skywalker"
    },
    "jediHero": {
      "name": "R2-D2"
    }
  }
}

上記の場合、通常はどちらもheroフィールドとなってしまいますが、エイリアスによってこれを回避しています。

フラグメント

フラグメントは、フィールド取得構成を使いまわすことのできる機能です。スキーマ言語の章でも、インラインフラグメントが出てきましたね。
というわけで、以下の例を見てください。

クエリ文
{
  leftComparison: hero(episode: EMPIRE) {
    ...comparisonFields
  }
  rightComparison: hero(episode: JEDI) {
    ...comparisonFields
  }
}

fragment comparisonFields on Character {
  name
  appearsIn
  friends {
    name
  }
}
レスポンスデータ
{
  "data": {
    "leftComparison": {
      "name": "Luke Skywalker",
      "appearsIn": ["NEWHOPE", "EMPIRE", "JEDI"],
      "friends": [
        {
          "name": "Han Solo"
        },
        {
          "name": "Leia Organa"
        },
        {
          "name": "C-3PO"
        },
        {
          "name": "R2-D2"
        }
      ]
    },
    "rightComparison": {
      "name": "R2-D2",
      "appearsIn": ["NEWHOPE", "EMPIRE", "JEDI"],
      "friends": [
        {
          "name": "Luke Skywalker"
        },
        {
          "name": "Han Solo"
        },
        {
          "name": "Leia Organa"
        }
      ]
    }
  }
}

それぞれのフィールドとして..comparisonFieldsを指定しています。これは、取得するフィールドの指定をcomparisonFieldsフラグメントに委譲しています。
また、fragment comparisonFields on Characterは、取得できたデータ型が Character 型だった場合に取得するフィールドの指定を行っています。
hero では Character 型が取得できるので、どちらもデータを取得できているわけですね。

なお、フラグメントには引数が利用できます。

クエリ文
query HeroComparison($first: Int = 3) {
  leftComparison: hero(episode: EMPIRE) {
    ...comparisonFields
  }
  rightComparison: hero(episode: JEDI) {
    ...comparisonFields
  }
}

fragment comparisonFields on Character {
  name
  friendsConnection(first: $first) {
    totalCount
    edges {
      node {
        name
      }
    }
  }
}
レスポンスデータ
{
  "data": {
    "leftComparison": {
      "name": "Luke Skywalker",
      "friendsConnection": {
        "totalCount": 4,
        "edges": [
          {
            "node": {
              "name": "Han Solo"
            }
          },
          {
            "node": {
              "name": "Leia Organa"
            }
          },
          {
            "node": {
              "name": "C-3PO"
            }
          }
        ]
      }
    },
    "rightComparison": {
      "name": "R2-D2",
      "friendsConnection": {
        "totalCount": 3,
        "edges": [
          {
            "node": {
              "name": "Luke Skywalker"
            }
          },
          {
            "node": {
              "name": "Han Solo"
            }
          },
          {
            "node": {
              "name": "Leia Organa"
            }
          }
        ]
      }
    }
  }
}

Luke Skywalkerは友人(機?)が 4 人いるようですが、3 人までしか表示されていません。$firstがデフォルトでは 3 だからですね。
一方、R2-D2 は 3 人しか友人がいないのでピッタリ表示されています。C-3PO は友達じゃないようですね。

操作名

操作名は、発行するクエリに任意に指定することができる名称です。クエリ発行ごとに毎回指定できます。
これまで例として記述されたクエリ文は、クエリ操作(querymutation等)と操作名の指定を省略した記法になります。操作名を指定する場合、クエリ操作についても明記が必要です。
以下に例を示します。

クエリ文
query HeroNameAndFriends {
  hero {
    name
    friends {
      name
    }
  }
}
レスポンスデータ
{
  "data": {
    "hero": {
      "name": "R2-D2",
      "friends": [
        {
          "name": "Luke Skywalker"
        },
        {
          "name": "Han Solo"
        },
        {
          "name": "Leia Organa"
        }
      ]
    }
  }
}

省略記法を用いる場合、操作名は不要になります。不要な操作名をあえて明記する理由は、サーバーのログ記録です。
クエリを発行する際に操作名を記述し、GraphQL サーバーで記録することにより、不具合や問い合わせなどの際にログを追いかけることが容易になるのです。

変数

GraphQL では、クエリ文と同時に変数の指定も送信することができます。定型の処理であれば、変数の値だけ変更してあげれば済みますね。
例としては以下の通り。

クエリ文
query HeroNameAndFriends($episode: Episode) {
  hero(episode: $episode) {
    name
    friends {
      name
    }
  }
}
変数
{
  "episode": "JEDI"
}
レスポンスデータ
{
  "data": {
    "hero": {
      "name": "R2-D2",
      "friends": [
        {
          "name": "Luke Skywalker"
        },
        {
          "name": "Han Solo"
        },
        {
          "name": "Leia Organa"
        }
      ]
    }
  }
}

クエリ文には、Episode 型を指定できる$episode変数が引数として存在します。変数定義では、この episode にJEDIを代入していますね。
変数の指定は基本的に JSON で行います。よって、複数の変数も代入することが可能です。

変数は、スカラー型、列挙型、入力型のいずれかでなければなりません。よって、複雑なオブジェクトを変数として指定したい場合は、入力型を利用する必要があります。
また、既に何度か例示されていますが、初期値を設定することも可能です。例えば上記の例で初期値をJEDIにしたい場合は以下の通り。

query HeroNameAndFriends($episode: Episode = JEDI) {
  hero(episode: $episode) {
    name
    friends {
      name
    }
  }
}

ディレクティブ

ディレクティブ(Directives)とは、指令や命令といった意味があるそうです。
ディレクティブを使うことで、取得するデータの構造を動的に変化させることができます。動的なデータ構造変化としてはフラグメントも該当すると思いますが、こちらは変数を与えることで動作します。

クエリ文
query Hero($episode: Episode, $withFriends: Boolean!) {
  hero(episode: $episode) {
    name
    friends @include(if: $withFriends) {
      name
    }
  }
}
変数
{
  "episode": "JEDI",
  "withFriends": false
}
レスポンスデータ
{
  "data": {
    "hero": {
      "name": "R2-D2"
    }
  }
}

@includeディレクティブにより、friends フィールドを含むかどうか選択しています。今回は false を引数に与えたため、friends フィールドを除く結果となりました。

GraphQL には、上記の@include、および逆の操作を行える@skipの 2 種類のみが定義されています。
このディレクティブはライブラリやユーザー定義などで追加されることを想定した機能のようです。例えば、Apollo では@key や@provides 等のディレクティブが定義されています

Mutations

REST では、GET リクエストではデータの取得のみを行います。データの変更を行うには POST や PUT などのリクエストメソッドを使い、明示的に示すべきとされています。
QraphQL でもこの考え方を踏襲しています。データの変更を伴わない操作にはQueryを、データの変更を伴う操作にはMutationを利用するべきです。

以下は Mutation を明示的に示したクエリ文の例です。

クエリ文
mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) {
  createReview(episode: $ep, review: $review) {
    stars
    commentary
  }
}
変数
{
  "ep": "JEDI",
  "review": {
    "stars": 5,
    "commentary": "This is a great movie!"
  }
}
レスポンスデータ
{
  "data": {
    "createReview": {
      "stars": 5,
      "commentary": "This is a great movie!"
    }
  }
}

レビューを新規作成するクエリ文です。mutation CreateReviewForEpisodeと、これがデータ変更を行うことを示していますね。もちろんこの操作名はログとして保存されるべきです。
まだライブラリの実装は確認していませんが、実装をチョチョイとすれば Query でデータ変更を行うことも可能でしょう。ですが、REST におけるリクエストメソッドと同様に、規格に合わせることでよりわかりやすくバグの混入しにくいアプリケーションとすることが大事なのだと思います。

ちなみに、変数として定義できるオブジェクトは入力型のみなので、ここで用いられているReviewInput型は入力型です。

Query と同様に Mutation も一度のクエリ文に複数仕込むことが可能です。その場合、上から順番に連続して実行されるようです。

終わりに

以上でこの記事は終わりです。GraphQL を完全に理解できましたか? 私はできていません。
やはり細かいところは自分で作ってみなければ理解できないように感じます。幸い、各言語でライブラリが用意されているようなので、いろいろ試してみたいと思います。

参考資料

GraphQL
「GraphQL」徹底入門 ─ REST との比較、API・フロント双方の実装から学ぶ
GraphQL のクエリを基礎から整理してみた
0 から REST API について調べてみた
REST を採用して気づいた「GraphQL って結局何が良いの?」について
in between days
GraphQL 入門 - 使いたくなる GraphQL
GraphQL を最速でマスターするための意識改革3ヶ条
Amazon が SQL++となる新しいクエリ言語、PartiQL をオープンソースで発表

112
85
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
112
85