1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

GraphQL入門メモ 概要編

Posted at

はじめに

この記事では、GraphQL仕様やドキュメントをもとにGraphQLの基本について解説し、さらに、Go言語のライブラリであるgqlgenを利用して実際にGraphQLサーバーを構築する方法について解説します。

この記事は、GraphQLの簡単な概要を把握したい方や、Go言語でGraphQLサーバーを構築したい方を対象とした入門記事になります。記事を通じて、GraphQLの基本的な概念や、gqlgenを利用したGraphQLサーバーの構築方法を学ぶことができます。

(という予定でしたがいきあたりばったりで書いていたら長くなってしまったため分割します...)

今回の記事ではGraphQLの基本について解説を行ないます。

  • GraphQL入門メモ 概要編(当記事)
  • GraphQL入門メモ gqlgenでGraphQLサーバー構築編

環境

  • Go: go version go1.20.5 linux/amd64
  • gqlgen: v0.17.36

GraphQLとは

GraphQLはAPI用のクエリ言語であり、定義した型システムを使用してクエリを実行するためのランタイムです。GraphQLはクライアントに必要なものだけを要求する能力を与えます。またGraphQLが特定のデータベースやストレージエンジンに縛られることはありません。

GraphQLには多くの設計原則があり、その一部を紹介します。

  • 階層的: GraphQLは階層的なクエリ構造を採用しています。この設計は、フロントエンドのビューやコンポーネントの構造に直感的に合わせられるため、クエリの作成やデータの理解が容易になります。また、データのアンダーフェッチやオーバーフェッチを防ぐ助けとなります。
  • 強い型付け: GraphQLはアプリケーション固有の型システムを採用しています。何らかのオペレーションが与えられると、ツールは実行前にその型システムのなかで構文的に正しく、かつ有効であることを保証できます。サービスはレスポンスの形状と性質について一定の保証を行なえます。
  • クライアント指定レスポンス: GraphQLではクライアントが具体的にどのデータを必要としているかを明示的に指定できます。GraphQLを使用せずに記述されたクライアントサーバーアプリケーションの大半では、サービスがさまざまなエンドポイントから返されるデータの形状を決定します。一方、GraphQLレスポンスには、クライアントが要求したものが正確に含まれ、それ以上は含まれません。

GraphQLは規模を問わず、さまざまなプロジェクト、環境、言語で使用されています。

gqlgenとは

gqlgenはGo言語で簡単にGraphQLサーバーを構築できるライブラリです。

gqlgenはスキーマファーストのアプローチに基づいており、GraphQLスキーマ定義言語を使用することで、GraphQLサーバーのコードを自動生成します。またgqlgenは型の安全性を優先しています。

GraphQL document

クライアントはGraphQLクエリ言語を使用してGraphQLサービスにリクエストを行ないます。これらのリクエストソースをドキュメントと呼びます。

GraphQL Specification Versions October 2021 - Overviewに掲載されているドキュメントの例を見てみましょう。

{
  user(id: 4) {
    name
  }
}

このドキュメント(クエリ)は、idが4のユーザーのnameを取得する要求を表しています。

Comments

ドキュメント内でコメントを記述するには、#を使用します。この記号に続くテキストはすべてコメントとして扱われ、実際のクエリ実行時には無視されます。

# idが4のユーザーの名前を取得する
{
  user(id: 4) {
    name
  }
}

Operations

GraphQLには3つの操作タイプ(以降、オペレーション)があります。それが、querymutationsubscriptionです。

オペレーションは以下のような形式として定義されています。

# OperationDefinition
OperationType [Name] [VariableDefinitions] [Directives] {
}

# OperationType
query, mutation, subscription

Query・Mutationオペレーションについては後の章で解説します。
Subscriptionオペレーションはデータの変更をリアルタイムに監視するためのオペレーションです。今回の記事では扱いません。

オペレーション名の記述は任意ですが、本番アプリケーションではコードを曖昧にしないためにも使用するべきとされています。

以下はQueryオペレーションに対してHeroNameAndFriendsというオペレーション名を与えた場合のサンプルです。

query HeroNameAndFriends {
  hero {
    name
    friends {
      name
    }
  }
}

出典: Queries and Mutations | GraphQL - Operation name

オペレーション名を使用することでデバッグやサーバーサイドのロギングに役立ちます。

ドキュメントに含まれるオペレーションが1つだけで、変数を定義せず、ディレクティブを含まないクエリである場合、そのオペレーションはqueryキーワードとオペレーション名を省略したショートハンド形式で表すことができます。

{
  user(id: 4) {
    name
  }
}

Fields

At its simplest, GraphQL is about asking for specific fields on objects.

出典: Queries and Mutations | GraphQL - Fields

フィールドの中には複雑なデータや他のデータとの関係を記述しているものもあります。このデータをネストしてリクエストすることでさらに探索できます。すべてのGraphQLオペレーションは明確な形状のレスポンスを保証するために、スカラー値を返すフィールドまでのSelectionを明示的に指定する必要があります。

GraphQL Specification Versions October 2021 - Fieldsに掲載されている複雑なオペレーションを見てみましょう。

{
  me {
    id
    firstName
    lastName
    birthday {
      month
      day
    }
    friends {
      name
    }
  }
}

Arguments

フィールドは概念的には値を返す関数とされており、その動作を変更する引数を受け入れることもあります。これらの引数は(name: value)の形式で指定されます。

この記事ではこれまでに、userフィールドに対してid引数を指定している例が登場しています。

{
  user(id: 4) {
    name
  }
}

引数はどのような順序で指定しても構いません。以下2つの例は意味的には同一です。

{
  picture(width: 200, height: 100)
}
{
  picture(height: 100, width: 200)
}

出典: GraphQL Specification Versions October 2021 - Arguments are unordered

Fragments

GraphQLにはドキュメント内の重複を減らすことができるフラグメントという機能があります。

GraphQL Specification Versions October 2021 - Fragmentsには、あるユーザーの共通の友人や友人に関する情報を取得するためのフラグメントの例が掲載されています。

query noFragments {
  user(id: 4) {
    friends(first: 10) {
      id
      name
      profilePic(size: 50)
    }
    mutualFriends(first: 10) {
      id
      name
      profilePic(size: 50)
    }
  }
}

繰り返されているフィールドをフラグメントとして抽出します。抽出したフラグメントはスプレッド演算子(...)を用いることで使用できます。

query withFragments {
  user(id: 4) {
    friends(first: 10) {
      ...friendFields
    }
    mutualFriends(first: 10) {
      ...friendFields
    }
  }
}

fragment friendFields on User {
  id
  name
  profilePic(size: 50)
}

フラグメントはQueries and Mutations | GraphQL - Fragmentsによると、異なるフラグメントを持つ多くのUIコンポーネントを1度のデータフェッチに組み合わせる必要がある場合に役立つとされています。

Inline Fragments

GraphQLにはインラインフラグメントという機能もあります。これは、実行時の型にもとづいてフィールドを条件付きで決定するために使用されます。

query inlineFragmentTyping {
  profiles(handles: ["zuck", "coca-cola"]) {
    handle
    ... on User {
      friends {
        count
      }
    }
    ... on Page {
      likers {
        count
      }
    }
  }
}

出典: GraphQL Specification Versions October 2021 - Inline Fragments

適用する型を省略すると、インラインフラグメントは囲んでいるコンテキストと同じ型であるとみなされます。

Variables

変数はオペレーションの先頭で定義する必要があります。変数はどの引数が動的に決定されるのかを示すのに役立ちます。

以下のコードは、JEDIという初期値を初期値を持つEpisode型の変数$episodeを宣言しています。

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

出典: Queries and Mutations | GraphQL - Default variables

変数には省略可能なものと必須のものがあり、上記はEpisode型の隣に!がないため省略可能です。

逆に、上記のコードを改変し、型の部分をEpisode!に変更すると以下のようなエラーが発生します。

query HeroNameAndFriends($episode: Episode!) {
  hero(episode: $episode) {
    name
    friends {
      name
    }
  }
}
{
  // "episode": "JEDI"
}
{
  "errors": [
    {
      "message": "Variable \"$episode\" of required type \"Episode!\" was not provided.",
      "locations": [
        {
          "line": 1,
          "column": 26
        }
      ]
    }
  ]
}

Directives

フィールドを条件付きで含めたりスキップしたりなど、GraphQLの実行動作を変更するオプションを提供したいことがあるかもしれません。ディレクティブを使用することでこれらを提供できます。

query Hero($episode: Episode, $withFriends: Boolean!) {
  hero(episode: $episode) {
    name
    friends @include(if: $withFriends) {
      name
    }
  }
}
{
  "episode": "JEDI",
  "withFriends": true
}
{
  "data": {
    "hero": {
      "name": "R2-D2",
      "friends": [
        {
          "name": "Luke Skywalker"
        },
        {
          "name": "Han Solo"
        },
        {
          "name": "Leia Organa"
        }
      ]
    }
  }
}

出典: Queries and Mutations | GraphQL - Directives

GraphQLの将来のバージョンで新しい設定可能な実行機能が採用されると、それらはディレクティブを介して公開される可能性があります。また、GraphQLサービスやツールはカスタムディレクティブを提供できます。

QueryとMutation

ここではQueryオペレーションとMutationオペレーションについて解説を行ないます。

Queryオペレーション

GraphQLはデータ・フェッチにもっとも焦点を当てており、そのためにQueryオペレーションが用意されています。

GraphQLの特徴の1つとして、Queryオペレーションのリクエストフォーマットと、それに対するレスポンスのフォーマットが一致していることが挙げられます。
これにより、クライアントはどのようなデータが返ってくるのかを予測することが容易になります。

Mutationオペレーション

GraphQLにはサーバー側のデータを変更する方法が用意されています。それがMutationオペレーションです。

技術的にはQueryオペレーションでデータの書き込みを引き起こす実装が可能です。しかし、データの作成、更新、削除のようなデータを変更するためのリクエストはMutationを通じて明示的に送信されるべきとされています。

Mutationがオブジェクト型を返す場合、ネストしたフィールドを要求できます。これは更新後のデータを取得するために役立ちます。

Queries and Mutations | GraphQL - Mutationsに掲載されている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!"
    }
  }
}

createReviewフィールドが更新後のstarscommentaryを返していることがわかります。

MutationフィールドもQueryフィールドと同様に複数のフィールドを含めることができます。
この際、Queryフィールドは並列に実行されるのに対して、Mutationフィールドは直列に実行されます。これはMutationが副作用を持つ可能性があるためです。提供されたMutationを直列で実行することで、副作用間の競合関係を確実に防ぐことができます。

SchemaとType System

ここではGraphQLの型システムについて見ていきたいと思います。

Schema

スキーマはサポートする型、ディレクティブ、および各種ルートオペレーションタイプで定義されます。

スキーマ内のすべての型は一意の名前を持ち、Scalar型やIntrospection型など組み込みの型と競合する名前をつけることはできません。
また、すべてのディレクティブも同様に一意の名前を持つ必要があります。
スキーマ内のすべての型とディレクティブは__(アンダースコア2つ)ではじまる名前をつけることはできません。

Scalars

スカラー値は文字列や整数のようなプリミティブな値です。
GraphQLのレスポンスは階層的なツリー構造を取りますが、このツリーの葉の部分は通常、Scalar型になります。(列挙型やnullの場合もあります)

GraphQLはいくつかの組み込みScalar型を提供しています。

  • Int: 符号付き32ビット整数
  • Float: 符号付き倍精度浮動小数点数
  • String: 人間が読める自由形式のテキストデータ
  • Boolean: truefalse
  • ID: オブジェクトのリフェッチやキャッシュのキーに使用される一意の識別子

GraphQLは組み込みのScalar型以外にも、独自のカスタムスカラー型を導入できます。

Objects

Scalar型がツリー構造の葉に当たる部分に対して、Object型はノード(節)に相当します。

Schemas and Types | GraphQL - Object types and fieldsに掲載されている例を見てみましょう。

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

nameはNon-NullのString型フィールドで、[Episode!]!は常に配列ですべての要素がEpisodeオブジェクトであるフィールドです。

Object型のフィールドは、Scalar、列挙型、別のObject型、InterfaceUnionのいずれかです。これら5つの型を基本型とした任意のラッパー型の可能性もあります。

Input Objects

引数に対してスカラー値や列挙型を渡すことが多いかもしれませんが、より複雑な値を渡したいこともあると思います。
そのような際、とくに作成されるオブジェクト全体を渡したいようなMutationオペレーションに役立つInput Object型があります。

Input Object型はinputキーワードで定義できます。

input Point2D {
  x: Float
  y: Float
}

出典: GraphQL Specification Versions October 2021 - Input Objects

Input Object型は与えられたリテラルや変数に応じてコアーション(ここでは別の型への変換と解釈しました)が発生します。

GraphQL Specification Versions October 2021 - Input Coercionにその一例が掲載されているので確認してみましょう。
以下は任意のaフィールドと必須のbフィールドを持った型のコアーションの例です。

input ExampleInputObject {
  a: String
  b: Int!
}
リテラル値 変数 コアーション値
{ a: "abc", b: 123 } {} { a: "abc", b: 123 }
{ a: null, b: 123 } {} { a: null, b: 123 }
{ b: 123 } {} { b: 123 }
{ a: $var, b: 123 } { var: null } { a: null, b: 123 }
{ a: $var, b: 123 } {} { b: 123 }
{ b: $var } { var: 123 } { b: 123 }
$var { var: { b: 123 } } { b: 123 }
"abc123" {} Error: Incorrect value
$var { var: "abc123" } Error: Incorrect value
{ a: "abc", b: "123" } {} Error: Incorrect value for field b
{ a: "abc" } {} Error: Missing required field b
{ b: $var } {} Error: Missing required field b.
$var { var: { a: "abc" } } Error: Missing required field b
{ a: "abc", b: null } {} Error: b must be non-null.
{ b: $var } { var: null } Error: b must be non-null.
{ b: 123, c: "xyz" } {} Error: Unexpected field c

List

List型はリスト内の各アイテムの型(アイテム型)を宣言するコレクション型です。

フィールドがList型を使っていることを示すには、pets: [Pet]のようにアイテム型を角括弧で括ります。matrix: [[Int]]のように入れ子にもできます。

Non-Null

デフォルトではGraphQLのすべての型はnullを許容します。
nullを許容しない型を宣言するにはNon-Null型を使用します。この型はもととなる型をラップし、そのラップされた型と同じように動作します。name: String!のように、末尾に感嘆符を追加することでNon-Null型を使用できます。

List型とNon-Null型を組み合わせることでより複雑な型を表現できます。
GraphQL Specification Versions October 2021 - Combining List and Non-Nullにさまざまな組み合わせの例が掲載されているため確認してみましょう。

期待する型 内部の値 コアーションの結果
[Int] [1, 2, 3] [1, 2, 3]
[Int] null null
[Int] [1, 2, null] [1, 2, null]
[Int] [1, 2, Error] [1, 2, null] (With logged error)
[Int]! [1, 2, 3] [1, 2, 3]
[Int]! null Error: Value cannot be null
[Int]! [1, 2, null] [1, 2, null]
[Int]! [1, 2, Error] [1, 2, null] (With logged error)
[Int!] [1, 2, 3] [1, 2, 3]
[Int!] null null
[Int!] [1, 2, null] null (With logged coercion error)
[Int!] [1, 2, Error] null (With logged error)
[Int!]! [1, 2, 3] [1, 2, 3]
[Int!]! null Error: Value cannot be null
[Int!]! [1, 2, null] Error: Item cannot be null
[Int!]! [1, 2, Error] Error: Error occurred in item

Validation

Operation Name Uniqueness

ドキュメント内のオペレーション名は一意である必要があります。

たとえば以下のドキュメントは無効です。

query getName {
  dog {
    name
  }
}

query getName {
  dog {
    owner {
      name
    }
  }
}

異なるオペレーションの場合でも同様です。

query dogOperation {
  dog {
    name
  }
}

mutation dogOperation {
  mutateDog {
    id
  }
}

出典: GraphQL Specification Versions October 2021 - Operation Name Uniqueness

Leaf Field Selections

選択したフィールドがScalar型または列挙型の場合、そのサブフィールドは空でなければなりません。

たとえば以下のフィールドは無効です。

fragment scalarSelectionsNotAllowedOnInt on Dog {
  barkVolume { # Int
    sinceWhen
  }
}

逆に、インターフェイス、ユニオン、Object型の場合は空であってはなりません。

query directQueryOnObjectWithoutSubFields {
  human # Human
}

query directQueryOnInterfaceWithoutSubFields {
  pet # Pet
}

query directQueryOnUnionWithoutSubFields {
  catOrDog # CatOrDog
}

出典: GraphQL Specification Versions October 2021 - Leaf Field Selections

All Variables Used

とあるオペレーションで定義されている変数は、そのオペレーションまたはそこに含まれるフラグメントで使用されていなければなりません。
未使用の変数はバリデーションエラーになります。

まとめ

この記事ではGraphQL仕様やドキュメントをもとにGraphQLの基本について解説を行ないました。

1
1
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
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?