はじめに
この記事では、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つの操作タイプ(以降、オペレーション)があります。それが、query
・mutation
・subscription
です。
オペレーションは以下のような形式として定義されています。
# 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.
フィールドの中には複雑なデータや他のデータとの関係を記述しているものもあります。このデータをネストしてリクエストすることでさらに探索できます。すべての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
フィールドが更新後のstars
とcommentary
を返していることがわかります。
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:
true
かfalse
- 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
型、Interface
、Union
のいずれかです。これら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の基本について解説を行ないました。