🌙

【REST/gRPC経験者向け】GraphQLの三大操作を学ぶ

に公開

はじめに

こんにちは!株式会社ココナラで技術戦略室に所属している、たにかずです。

「GraphQL、便利そうだけど学習コストが…」「RESTの経験はあるけど、GraphQLの"お作法"が分からない」。

そんな声に応えるため、ココナラではGraphQL開発を始めるメンバー向けのガイドラインを整備しています。

初めてのGraphQLのような名著はありますが、日本語の書籍が少ないのが現状です。
特に「操作タイプ」と「型システム」に絞り込み、REST/gRPCとの比較を通じて、経験者が直感的に理解できる内容を目指しました。

これからGraphQLを学ぶ多くのチームの助けになれば幸いです。

今回公開するガイドラインはこちらです✨

GraphQLの操作タイプ

GraphQLは、APIとのインタラクションを定義するために主に3つの操作タイプを提供します。これらは、データの取得データの変更、そしてリアルタイムなデータ更新の購読という、APIの最も一般的なユースケースに対応しています。

1. 参照:Query (クエリ)

Queryは、APIからデータを参照(取得)するために使用される操作タイプです。クライアントが必要なデータのみを指定できるため、オーバーフェッチ(不要なデータの取得)やアンダーフェッチ(必要なデータが足りないため複数回リクエストを行う)といった問題を解決します。

特徴:

  • データの取得: サーバー上のデータを読み取ります。
  • 冪等性: 同じクエリを何度実行しても、サーバー側のデータに副作用(変更)はありません
  • 柔軟性: 必要なフィールドとネストされたリレーションシップを正確に指定できます。

使用例:

query GetUserData {
  user(id: "123") {
    name
    email
    posts {
      title
      createdAt
    }
  }
}
  • フィールド引数:
    クエリ内で指定する各データ項目は「フィールド」と呼ばれます。フィールドには、特定のデータを絞り込むための「引数」を渡すことができます(例: user(id: "123"))。

  • エイリアス:
    同じフィールドを異なる引数で複数回クエリする場合や、クエリ結果のキー名を変更したい場合に「エイリアス」を使用できます。

    query GetMultipleUsers {
      # エイリアスを使って、同じ user フィールドを異なるIDで取得
      userOne: user(id: "1") {
        name
      }
      userTwo: user(id: "2") {
        name
        email
      }
    }
    

[!TIP]
REST API

Queryは、REST APIにおけるGETリクエストに最も近い概念です。しかし、GraphQLのQueryは、単一のエンドポイント(通常は/graphql)に対してデータを要求し、クライアントがそのレスポンスの構造を柔軟に定義できる点が異なります。これにより、複数のGETリクエストを組み合わせたり、特定のデータを取得するために何度もエンドポイントを叩く必要がなくなります。

[!TIP]
gRPC

Queryは、gRPCUnary RPC(単一のリクエストと単一のレスポンス)でデータを取得するシナリオと類似しています。GraphQLは、gRPCのProtocol Buffers定義に近いスキーマ定義言語(SDL)を使用してデータ構造を厳密に定義します。クライアントがデータ取得のクエリを直接記述できるため、特定のサービスメソッドを呼び出すというよりは、よりデータ中心のアプローチを取ります。


2. 更新:Mutation (ミューテーション)

Mutationは、APIを介してサーバー側のデータを**更新(作成、変更、削除)**するために使用される操作タイプです。

特徴:

  • データの変更: サーバー上のデータに**副作用(変更)**を伴う操作を行います。
  • 非冪等性: 同じミューテーションを複数回実行すると、異なる結果になる可能性があります(例: incrementCounterミューテーション)。
  • 構造化された入力: 変更するデータを構造化された「入力タイプ(Input Objects)」として渡すことが一般的です。これにより、引数の検証や型の安全性が向上します。
  • 変更されたデータの取得: ミューテーションを実行した後、その操作によって変更されたデータを直ちにクエリすることができます。

使用例:

mutation CreatePost($title: String!, $content: String!) {
  createPost(input: { title: $title, content: $content }) {
    id
    title
    createdAt
    author {
      name
    }
  }
}
  • 入力タイプ (Input Objects):
    複雑な引数を扱う際に、関連するフィールドをまとめて単一の入力オブジェクトとして定義します。これにより、ミューテーションの引数が整理され、可読性が向上します。
  • 返り値:
    ミューテーションの実行後、その操作によって変更されたオブジェクトや関連するデータをGraphQLクエリと同様に取得できます。これにより、クライアントはサーバーからの最新の状態をすぐに反映できます。

[!TIP]
REST API

Mutationは、REST APIにおけるPOSTPUTDELETEリクエストの役割を担います。RESTが/users/{id}のようなリソース指向のURLとHTTPメソッドの組み合わせで操作を表現するのに対し、GraphQLでは単一のエンドポイントに対して、具体的な操作名(例: createPost)と入力データ、そして操作後に取得したいデータの構造を明確に記述します。ミューテーションの実行後に、その変更によって得られたデータをすぐに取得できるのは、RESTのレスポンス設計よりも強力な点です。

[!TIP]
gRPC

Mutationは、gRPCUnary RPCでサーバー側の状態を変更するサービスメソッドと非常に似ています。GraphQLの**スキーマ定義(SDL)**は、Protocol Buffersサービス定義メッセージ定義と概念的に対応し、厳密な型チェックを提供します。gRPCが個々のサービスメソッドの呼び出しに重点を置くのに対し、GraphQLは「操作名」と「その操作によって変更されるデータ」をより密接に結びつけ、変更後のデータの取得をクライアントに委ねる点が異なります。


3. 通知:Subscription (サブスクリプション)

Subscriptionは、サーバーからの**リアルタイムなデータ更新を購読(通知を受け取る)**するために使用される操作タイプです。特定のイベントが発生した際に、サーバーからクライアントへ自動的にデータがプッシュされます。WebSocketなどのプロトコルを利用して実装されることが一般的です。

特徴:

  • リアルタイム性: サーバーからのデータ更新を即座にクライアントに通知します。
  • イベント駆動: 特定のイベント(例: 新しいメッセージが投稿された、ユーザーがオンラインになった)が発生したときにトリガーされます。
  • 持続的な接続: クライアントとサーバー間で持続的な接続(例: WebSocket)を確立します。

使用例:

subscription OnNewMessage {
  newMessage {
    id
    text
    sender {
      username
    }
  }
}
  • WebSocketなどによる実装:
    サブスクリプションは通常、HTTPリクエスト/レスポンスのサイクルではなく、WebSocketのような持続的な接続プロトコルを介して行われます。クライアントは一度サブスクリプションリクエストを送信すると、イベントがトリガーされるたびにサーバーからデータがプッシュされます。

[!TIP]
REST API

標準的なREST APIにはSubscriptionに直接対応する概念はありません。リアルタイム機能を実現するためには、ロングポーリングServer-Sent Events (SSE)、またはWebSocket別途実装する必要があります。GraphQLのSubscriptionは、これらのリアルタイム通信の複雑さを抽象化し、GraphQLのクエリ構文一貫して購読できるようにします。開発者は、リアルタイムのデータストリームGraphQLスキーマの一部として定義し、クライアントは一般的なGraphQLクライアントライブラリを通じてそれを購読できます。

[!TIP]
gRPC

Subscriptionは、gRPCServer-streaming RPC(サーバーが複数のレスポンスをクライアントにストリーミングする)と概念的に類似しています。どちらもサーバーからクライアントへの継続的なデータプッシュを可能にします。しかし、gRPCがプロトコルレベルサービスメソッドメッセージストリームを厳密に定義するのに対し、GraphQLのSubscriptionは、データ取得の柔軟性(クエリと同様に取得したいフィールドを指定できる)と、イベント駆動のデータ配信に重点を置いています。GraphQLは、リアルタイムの「データグラフ」の更新を抽象化する上位レベルの概念を提供します。


GraphQLの型

GraphQLは、APIが提供するデータを厳密に型付けするための堅牢な型システムを備えています。これにより、クライアントとサーバー間でデータの形状や振る舞いを明確に合意でき、型安全な通信強力なイントロスペクション機能(APIが提供するスキーマ自体を問い合わせる機能)を実現します。

GraphQLの型は大きく「基本型(Scalar Types)」「複合型(Object Types, Enum Types, List, Non-Null)」「高度な型(Interface Types, Union Types)」「入力型(Input Objects)」に分類されます。

1. 基本型 (Scalar Types)

**Scalar Types**は、GraphQLにおける最も基本的な「プリミティブな」データ型です。これらは、それ以上分解できない最小単位の値を表します。

ビルトイン(組み込み)スカラー型:

  • Int: 符号付き32ビット整数。
    • 例: id: Int
  • Float: 符号付き倍精度浮動小数点数。
    • 例: price: Float
  • String: UTF-8文字のシーケンス。
    • 例: name: String
  • Boolean: true または false
    • 例: isActive: Boolean
  • ID: 一意の識別子を表す型。シリアライズは String と同じですが、意味的に「識別子」であることを示し、特にリレーのようなフレームワークでキャッシュのキーとして利用されることが多いです。
    • 例: userId: ID

[!NOTE] カスタムスカラー型
これらの組み込み型に加えて、日付 (Date)、JSON (JSON)、URL (URL) など、特定のアプリケーション固有の複雑なデータを表現するためにカスタムスカラー型を定義することも可能です。カスタムスカラー型は、そのデータのシリアライズ(サーバーからクライアントへ)とデシリアライズ(クライアントからサーバーへ)のロジックをサーバー側で定義します。

2. 複合型

2.1. Object Types (オブジェクト型)

Object Typesは、GraphQLスキーマの中核をなす型で、APIが提供する「モノ」や「エンティティ」を表します。これらは、名前と、そのオブジェクトが持つフィールドの集まりから構成されます。各フィールドもまた、特定の型(スカラー型、他のオブジェクト型、リストなど)を持ちます。

定義例:

type User {
  id: ID!
  name: String!
  email: String
  posts: [Post!]! # Post オブジェクト型のリスト
}

type Post {
  id: ID!
  title: String!
  content: String
  author: User! # User オブジェクト型
}
  • フィールド: オブジェクト型の具体的なデータ項目です。各フィールドには型が指定され、引数を持つこともできます(例: posts(limit: Int))。

2.2. Enum Types (列挙型)

Enum Typesは、あらかじめ定義された一連の許容される値のみを持つ型です。これにより、特定のフィールドが取りうる値を制限し、型安全性を保証します。

定義例:

enum Status {
  PENDING
  ACTIVE
  COMPLETED
  CANCELLED
}

type Task {
  id: ID!
  title: String!
  status: Status! # Status 列挙型を使用
}
  • 型安全性: Enum Typesを使用することで、無効な値がAPIを通じて渡されるのを防ぐことができます。

2.3. List (リスト)

**List型は、特定の型のコレクション(配列)**を表します。角括弧 [] で型を囲むことで表現されます。リストの要素は、スカラー型、オブジェクト型、インターフェース型、ユニオン型など、任意の型にできます。

定義例:

type User {
  id: ID!
  name: String!
  hobbies: [String!] # 文字列のリスト
  posts: [Post!]!   # Post オブジェクトのリスト
}
  • [String!] の意味:
    • String!: リストの各要素がnullであってはならないことを示します。
    • [String!]: リスト自体はnullを許容することを示します(つまり、hobbies: nullは有効です)。
  • [Post!]! の意味:
    • [Post!]!: リストの各要素も、リスト自体もnullであってはならないことを示します。

2.4. Non-Null (非null)

Non-Null型は、特定のフィールド引数nullであってはならないことを示します。型の後ろに感嘆符 ! をつけることで表現されます。

定義例:

type User {
  id: ID!        # ID は null であってはならない
  name: String!  # name は null であってはならない
  email: String  # email は null でもよい (省略可能)
}
  • データ整合性: スキーマレベルで必須フィールドを定義することで、データの整合性を強制できます。
  • クライアントの恩恵: クライアント側は、! が付いているフィールドが必ず値を持つことを保証されるため、nullチェックのコードを減らすことができます。

3. 高度な型

3.1. Interface Types (インターフェース型)

**Interface Types**は、複数のオブジェクト型が共有するフィールドのセットを定義する抽象型です。これにより、異なるオブジェクト型が共通のプロパティを持つことを保証し、クライアントはこれらの共通フィールドをまとめてクエリできます。

定義例:

interface Node {
  id: ID!
}

type User implements Node {
  id: ID!
  name: String!
  email: String
}

type Post implements Node {
  id: ID!
  title: String!
  content: String
}
  • ポリモーフィズム: クライアントはNodeインターフェースをクエリすることで、それがUserであってもPostであっても、共通のidフィールドを取得できます。
  • 再利用性: 共通のフィールド定義を複数のオブジェクト型で再利用できます。

3.2. Union Types (共用体型)

Union Typesは、あるフィールド指定された複数のオブジェクト型のいずれかを返す可能性があることを示します。インターフェースとは異なり、共通のフィールドを持つ必要はありません。

定義例:

union SearchResult = User | Post | Comment

type Query {
  search(text: String!): [SearchResult!]!
}
  • 異なる型の結果: 例えば、検索結果がユーザー、投稿、コメントのいずれかになりうる場合にUnion Typesを使用します。

  • ... on Type: クライアントはUnion Typesをクエリする際に、... on Type構文(インラインフラグメント)を使用して、どの型のデータが返されるかを指定し、それぞれの型に固有のフィールドを取得します。

    query Search($text: String!) {
      search(text: $text) {
        ... on User {
          id
          name
        }
        ... on Post {
          id
          title
          author {
            name
          }
        }
        ... on Comment {
          id
          text
        }
      }
    }
    

4. 入力型:Input Objects (入力オブジェクト)

**Input Objects**は、主にMutation引数として使用される、構造化された入力データを定義するための特別なオブジェクト型です。通常のオブジェクト型と似ていますが、Input Objectsフィールドはそれ自体が引数を持つことはできません。

定義例:

input CreatePostInput {
  title: String!
  content: String
  authorId: ID!
}

type Mutation {
  createPost(input: CreatePostInput!): Post! # CreatePostInput を引数として使用
}
  • ミューテーションの引数: Mutationに複雑なデータを渡す際に、引数を整理し、可読性と型安全性を向上させます。
  • 明確な意図: inputキーワードで定義されるため、これが入力のために使われる型であることが明確になります。

ココナラでは積極的にエンジニアを採用しています。
本記事で解説したGraphQLのようなモダンな技術を積極的に活用し、サービス開発に取り組んでませんか?

採用情報はこちら。
https://coconala.co.jp/recruit/engineer/

カジュアル面談希望の方はこちら。
カジュアルにココナラの魅力やキャリアパスについて詳しく聞いてみませんか?
https://open.talentio.com/r/1/c/coconala/pages/70417

Discussion