GraphQL 入門 (前編)

はじめに

こんにちは、システムエンジニアの kota です。
今回は、個人的にまだ触ったことのなかった GraphQL に入門してみたので、ブログとしてまとめてみました。

1. GraphQL とは?

GraphQLは、APIのためのクエリ言語であり、データ駆動型のアプリケーションを構築するためのランタイムです。Facebookが開発し、2015年にオープンソースとして公開されました。

これはエンドポイントに対するクエリを行うREST APIとは異なり、クライアント側で必要なデータを具体的に指定できるため、ネットワークの使用効率を最適化し、パフォーマンスを向上させることができます。また、APIのバージョン管理の問題を解消し、APIとフロントエンドの開発をデコープル(分離)することを可能にします。

2. GraphQL が登場するまでの技術

GraphQLが登場する以前、Webアプリケーションのデータ交換の主流はRESTfulなAPIでした。RESTはHTTPプロトコルに厳密に従い、エンドポイントを利用してリソースにアクセスします。

しかしながら、このRESTfulなアプローチには幾つかの問題がありました。

1つ目は「オーバーフェッチング」や「アンダーフェッチング」という問題です。「オーバーフェッチング」はクライアントが必要なデータよりも多くのデータを取得することを指し、無駄なネットワークと計算リソースを消費します。一方、「アンダーフェッチング」はクライアントが必要な全てのデータを一度に取得できず、複数のリクエストを行う必要があることを指します。

2つ目は、リソースごとに独立したエンドポイントが必要となるため、APIが大きくなるにつれてエンドポイントの数が増え、管理が難しくなるというエンドポイントの複雑性です。また、複数のエンドポイントからデータを取得する際には、複数のリクエストが必要となります。

3つ目は、データの形式や構造が変わる度に新しいバージョンのエンドポイントを作成する必要があるため、APIのバージョニングが困難になるというバージョニング問題です。

4つ目は、エンドポイントの設計がクライアントが必要とするデータの構造に大きく依存するため、クライアントとサーバー間の強い結合を意味します。これはフロントエンドとバックエンドの開発を同時に進行させることが難しくなるという問題です。

これらの問題は、GraphQLが登場することで大きく改善されました。具体的には、GraphQLでは1つのエンドポイントで複数のリソースを取得でき、バージョニングの必要性を排除し、クライアントとサーバーのデコープリングを可能にします。

3. GraphQL の登場で何が変わったのか?

GraphQLが登場すると、上記のような問題を大きく緩和することができました。それは、GraphQLがクライアントにデータの特定と取得の精度を提供するからです。

具体的には、GraphQLはクライアントに対してどのフィールドを取得するかを指定する能力を与え、これにより必要なデータのみを正確に取得できるようになりました。これにより、ネットワークの使用効率が向上し、不要なデータの取得によるリソースの浪費を避けることができます。

また、GraphQLはクエリ言語としての機能を持つため、複雑なデータの取得やデータの絞り込みも可能となりました。これらの要素は、APIの柔軟性とパフォーマンスを向上させる上で非常に重要な要素です。

4. GraphQL vs REST: それぞれの違いと利点

RESTとGraphQLはどちらもAPIを構築するための技術ですが、以下のような違いがあります。

まず、RESTではエンドポイントごとに固定のデータ構造が返されます。これはシンプルで理解しやすい一方で、前述の「オーバーフェッチング」や「アンダーフェッチング」の問題が生じやすいです。

それに対し、GraphQLはクライアントが必要なデータを自由に指定できるため、不要なデータの取得を避けることができます。これは効率的なネットワーク利用と高いパフォーマンスに繋がります。

さらに、RESTでは複数のエンドポイントを跨いでデータを取得する必要がある場合、複数のリクエストを行う必要があります。一方のGraphQLでは、1つのリクエストで複数のリソースを取得できます。これにより、リクエストの数を大幅に減らすことが可能になりました。

コードの比較

GraphQLとREST APIの違いを理解するために、簡単な例を用いてそれぞれのリクエストとレスポンスを比較してみます。

例えば、あるユーザーの情報とそのユーザーが投稿したブログ記事のタイトルを取得したいとします。それぞれのAPIスタイルでどのように行うか見てみましょう。

REST API

REST APIでは、通常は各リソースに対して個別のエンドポイントがあります。まずユーザーの情報を取得し、その後、そのユーザーの投稿を取得するために、別々のリクエストを行うことになります。

1つ目のリクエスト(ユーザー情報の取得):

GET /users/<ID>

レスポンス:

{
  "id": "<ID>",
  "name": "Alice",
  "age": 20
}

2つ目のリクエスト(ユーザーの投稿の取得):

GET /users/<ID>/posts

レスポンス:

[
  {
    "id": "1",
    "title": "First Post",
    "content": "Hello, world!"
  },
  {
    "id": "2",
    "title": "Second Post",
    "content": "Another post"
  }
]

GraphQL

一方、GraphQLでは、一つのリクエストで必要なすべての情報を取得することが可能です。また、クライアントは必要なデータの形状と量を正確に指定することができます。以下にその例を示します。

リクエスト:

{
  user(id: "<ID>") {
    id
    name
    age
    posts {
      id
      title
      content
    }
  }
}

レスポンス:

{
  "data": {
    "user": {
      "id": "<ID>",
      "name": "Alice",
      "age": 20,
      "posts": [
        {
          "id": "1",
          "title": "First Post",
          "content": "Hello, world!"
        },
        {
          "id": "2",
          "title": "Second Post",
          "content": "Another post"
        }
      ]
    }
  }
}

このように、GraphQLは一つのリクエストで複数の関連するリソースを取得することができ、またクライアントは必要なデータを正確に指定できるという利点があります。これに対し、REST APIでは複数のリクエストが必要になることがあり、またサーバー側で定義された形状と量のデータが返されるという特性があります。

5. GraphQL の主要な機能(クエリ、ミューテーション、サブスクリプション)

GraphQLの主要な機能はクエリ、ミューテーション、そしてサブスクリプションです。

クエリ(Query) はデータの読み取りを行います。以下はGraphQLクエリの例です:

query {
  user(id: 1) {
    name
    email
  }
}

クエリは、idが1のユーザーのnameとemailフィールドを取得します。queryキーワードは省略可能ですが、記載しておくことで、リーダブルでわかりやすくなります。userはフィールド名であり、この場合のidは引数を表します。引数を使うことで、特定のユーザーや投稿などを取得できます。

ミューテーション(Mutation) はデータの変更を行います。これにはデータの作成、更新、削除が含まれます。以下はミューテーションの例です:

mutation {
  createUser(input: {name: "John", email: "john@example.com"}) {
    id
    name
    email
  }
}

ミューテーションは、createUser関数を呼び出して新しいユーザーを作成します。input引数を使ってユーザーのnameとemailを提供します。この関数は新しく作成されたユーザーのid、name、emailを返します。ミューテーションはデータの作成、更新、削除など、データを変更する操作に使われ、必ずmutationキーワードで始まります。

サブスクリプション(Subscription) はリアルタイムのデータの更新を行います。サーバー側のイベントがトリガーされた時にクライアントに通知されます。以下はサブスクリプションの例です:

subscription {
  userCreated {
    id
    name
    email
  }
}

サブスクリプションは、新しいユーザーが作成されたときにリアルタイムでそのid、name、emailをクライアントに通知します。サブスクリプションは、サーバー上で特定のイベント(この場合は新しいユーザーの作成)が発生したときにクライアントに通知するために使用されます。サブスクリプションはsubscriptionキーワードで始まります。

6. GraphQL のスキーマと型システムについて

GraphQLは、APIを通じて利用可能なデータの形状と相互作用を明確に定義するための強力な型システムを提供します。これは「スキーマ」として知られています。スキーマは、クライアントがサーバーから取得できるデータと、そのデータがどのように相互作用するかを定義します。

型システム

このスキーマは、特定の形状や特性を持つオブジェクトを表す「型」で構成されています。型は、単純なスカラー型(String、Int、Boolean、Float、ID)から、より複雑なオブジェクト型、列挙型、インターフェース型、ユニオン型など、様々な種類があります。これらを組み合わせて、APIが表現するデータの全体像を形作ります。

例えば、ブログシステムのAPIを想像してみてください。このシステムでは、スキーマはおそらく「ユーザー」、「記事」、「コメント」などのオブジェクト型を持つでしょう。それぞれの型はそれぞれ、名前や年齢(ユーザー)、タイトルや内容(記事)、本文(コメント)など、さまざまなフィールドを持つことでしょう。以下に具体的なコードを示します。

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

type Post {
  id: ID!
  title: String!
  content: String
  author: User!
  comments: [Comment!]!
}

type Comment {
  id: ID!
  text: String!
  post: Post!
  author: User!
}

これらの型は、スカラー型や他のオブジェクト型を持つことができます。この例では、User型のpostsフィールドはPost型のリストを持つことを示しており、その逆に、Post型のauthorフィールドはUser型を持つことを示しています。

特に、[Post!]!のような表記について説明します。この表記はPost型の配列を示していますが、!が二つ存在し、それぞれ異なる意味を持っています。

1つ目の!Postオブジェクトが必須(null不可)であることを示します。これは、配列が存在する場合、その中に含まれる全ての要素がPost型のオブジェクトでなければならず、null要素は許容されないことを意味します。

2つ目の!は、配列自体が必須(null不可)であることを示します。つまり、postsフィールドは常にPost型の配列を返すべきで、その配列自体がnullになることは許されません。

従って、[Post!]!は、配列自体が必ず存在し、その配列の中身は1つ以上のPost型のオブジェクトで構成され、その中にnullが含まれることはない、という意味を持ちます。これにより、各フィールドの型とそのnull許容性を詳細に定義することができます。

リレーションシップ

このようにして、GraphQLのスキーマは、データ間のリレーションシップも表現します。たとえば、「ユーザー」は複数の「記事」を持つことができ、それぞれの「記事」は複数の「コメント」を持つことができます。これらのリレーションシップは、オブジェクト型のフィールドを通じて表現されます。このリレーションシップは、クライアントが必要なデータだけを選択して取得することを可能にします。

スキーマ定義言語(SDL)

GraphQLのスキーマは、スキーマ定義言語(SDL)と呼ばれる特殊な構文を使用して記述されます。このSDLはGraphQLのデータ構造を定義するための言語で、上記のUserPost型のような各種のデータ型を定義することが可能です。

型定義では、型名の後に続く括弧内にフィールドを定義し、各フィールドの後ろにはそのフィールドの型を指定します。フィールド名の後ろに!をつけると、そのフィールドが必須(null不可)であることを示します。また、型名の前に[ ]をつけると、その型の配列を示します。

また、スキーマには以下の特別な型が存在します:

  • Query型: クエリのエントリポイントとなります。クライアントがデータを読み取るときに使用します。
  • Mutation型: データの変更(作成、更新、削除)を扱います。クライアントがデータを変更するときに使用します。
  • Subscription型: リアルタイムの更新を監視します。クライアントがデータのリアルタイムな変更を追跡するときに使用します。

これらの特別な型を使うことで、クライアントはサーバーに対してデータの読み取り、変更、リアルタイムの監視といった様々な操作を要求することが可能になります。

type Query {
  users: [User!]!
  user(id: ID!): User
  posts: [Post!]!
}

type Mutation {
  createUser(name: String!, age: Int!): User!
  createPost(title: String!, content: String, authorId: ID!): Post!
}

type Subscription {
  postCreated: Post!
}

上記の例では、Query型が定義されており、usersuserpostsといったフィールドを持っています。これらのフィールドはそれぞれ、全ユーザーのリスト、指定したIDのユーザー、全投稿のリストを返します。

一方、Mutation型は新しいユーザーや投稿を作成するためのフィールドを定義しており、それぞれcreateUsercreatePostという名前で、必要なパラメータと共に定義されています。これらの操作により、クライアントは新しいユーザーや投稿を作成することができます。

Subscription型は新しい投稿が作成されたときにその情報をリアルタイムに提供するためのフィールドpostCreatedを定義しています。これにより、クライアントは新しい投稿の作成をリアルタイムで監視することができます。

これらの型とリレーションシップを利用することで、クライアントは必要なデータを正確にクエリすることができ、サーバーはクエリの妥当性を検証し、正確なデータを返すことができます。これはGraphQLのスキーマと型システムの強力さを示しています。

参考記事

まとめ

以上が、GraphQLの基本についての解説でした。次回の後編では、より詳細なGraphQLの使い方や応用について解説します。