6
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

10分で理解する GitHub GraphQL API (Query)

Posted at

はじめに

初めて GraphQL API(query) を使う人が、GitHub GraphQL API の公式ドキュメント読んだだけでは確実につまづくだろうなぁというポイントをまとめています。

あくまで大枠を理解してもらうことに主眼を置いているので、細かい仕様などは説明しきれていませんがご了承ください。

記事のゴール

「GraphQLってなに?」という人が、データ取得のクエリを作成できるようになること

※ 本記事では Mutation については扱いません

参考資料

もくじ

GraphQL APIの概要

GraphQLについて

GraphQL は Facebook によって開発された API のためのクエリ言語です。従来の REST API では各エンドポイントでサーバーが返すデータを決定していたのに対し、GraphQL は単一のエンドポイントを通じてデータの取得や操作を可能にします

つまり、クライアント側で必要なデータ構造のクエリを定義し、それをサーバーに送信することで、必要な情報だけを取得できるというわけです。

イメージとして、「SQL とそっくり」と捉えてもらえばけっこうしっくりくるんじゃないかなと思っています。(SQL は RDB に対するクエリ言語ですが、GraphQL は API のためのクエリ言語です!)

そんなこんなで、GitHub でも REST API だけでなく GraphQL API が使えるようになっています。

GitHubは、インテグレーターにとって大きな柔軟性を提供してくれることから、GraphQLを選択しました。 必要—なデータを正確に定義する機能と、必要_なデータ_のみを—定義できることは、従来の REST API エンドポイントよりも強力な利点です。 GraphQL を使用すると、複数の REST 要求を 1 回の呼び出しに 置き換えて、指定したデータをフェッチできます。

参考:GitHub が GraphQL に投資した理由について書かれたブログ

GitHub Rest API(v3)との違い

Rest API と比較したときの GraphQL の特徴・利点は、より柔軟にデータ取得ができること、とだけ覚えてればOKかと思います。

  • REST API(v3)
    • 固定のエンドポイントからデータを取得するので、事前に決定されている構造でデータが返される
      • 1 度に必要以上のデータを取得する「オーバーフェッチング」が起きやすい
      • 必要なデータを 1 度に取得できない「アンダーフェッチング」が起きやすい
    • 上記より、各エンドポイントに対して投げるリクエストの数が増えやすい
  • GraphQL API(v4)
    • クライアントが必要なデータの構造をクエリとして送信するため、必要なデータを過不足なく取得できる
    • 上記が最大の特徴であり利点だが、REST API とは全く違うアプローチをとっているので学習コストが高め

GitHub GraphQLのオペレーション種類

一般的に GraphQL では以下の 3 種類の操作が定義されていますが、GitHub GraphQL API はそのうちの 2 種類をサポートしています。

The two types of allowed operations in GitHub's GraphQL API are queries and mutations.

  • Query
    • データを取得する(読みとる)ための操作
    • REST API の GET リクエストのようなもの
  • Mutation
    • データの作成、編集、削除など、データを更新するための操作
    • REST API の POST / PUT / DELETE リクエストのようなもの
  • Subscription
    • リアルタイムでデータの変更を受け取るための操作
    • データを継続的に取得する際などに使われる
    • 現時点で GitHub GraphQL API ではサポートされていない

クエリ操作について

ここから本題の GitHub GraphQL API の Query 操作について解説します。

クエリの例

公式ドキュメントの Example query で利用されているクエリをそのまま使いつつ解説していきます。

GitHub公式ドキュメント内の Example query
query {
  repository(owner:"octocat", name:"Hello-World") {
    issues(last:20, states:CLOSED) {
      edges {
        node {
          title
          url
          labels(first:5) {
            edges {
              node {
                name
              }
            }
          }
        }
      }
    }
  }
}

上記をコマンドで実行する場合は以下のようになります。

公式ドキュメントのExample queryを実行するコマンド
curl -X POST -H "Authorization: bearer YOUR_ACCESS_TOKEN" -H "Content-Type: application/json" --data '{ "query": "query GetLast20ClosedIssues { repository(owner: \"octocat\", name: \"Hello-World\") { issues(last: 20, states: CLOSED) { edges { node { title url labels(first: 5) { edges { node { name } } } } } } } }" }' https://api.github.com/graphql

また、上記の実行結果は以下のようになります。

【click to expand】 ※ Example query の実行結果
result.json
{
    "data": {
        "repository": {
            "issues": {
                "edges": [
                    {
                        "node": {
                            "title": "Internationalization",
                            "url": "https://github.com/octocat/Hello-World/issues/2831",
                            "labels": {
                                "edges": []
                            }
                        }
                    },
                    {
                        "node": {
                            "title": "SydScreenShot_FullPage.png (972×6654)",
                            "url": "https://github.com/octocat/Hello-World/issues/2856",
                            "labels": {
                                "edges": []
                            }
                        }
                    },
                    {
                        "node": {
                            "title": "Hello, world!2",
                            "url": "https://github.com/octocat/Hello-World/issues/2857",
                            "labels": {
                                "edges": []
                            }
                        }
                    },
                    {
                        "node": {
                            "title": "Hello, world!",
                            "url": "https://github.com/octocat/Hello-World/issues/2858",
                            "labels": {
                                "edges": []
                            }
                        }
                    },
                    {
                        "node": {
                            "title": "Hello, world!",
                            "url": "https://github.com/octocat/Hello-World/issues/2859",
                            "labels": {
                                "edges": []
                            }
                        }
                    },
                    {
                        "node": {
                            "title": "test",
                            "url": "https://github.com/octocat/Hello-World/issues/2861",
                            "labels": {
                                "edges": []
                            }
                        }
                    },
                    {
                        "node": {
                            "title": "https://github.com/octocat/Hello-World/issues/2874#issue-2054638316",
                            "url": "https://github.com/octocat/Hello-World/issues/2883",
                            "labels": {
                                "edges": []
                            }
                        }
                    },
                    {
                        "node": {
                            "title": "El disciplina",
                            "url": "https://github.com/octocat/Hello-World/issues/2885",
                            "labels": {
                                "edges": []
                            }
                        }
                    },
                    {
                        "node": {
                            "title": "take a test",
                            "url": "https://github.com/octocat/Hello-World/issues/3004",
                            "labels": {
                                "edges": []
                            }
                        }
                    },
                    {
                        "node": {
                            "title": " octocat/Hello-World",
                            "url": "https://github.com/octocat/Hello-World/issues/3013",
                            "labels": {
                                "edges": []
                            }
                        }
                    },
                    {
                        "node": {
                            "title": "Hello",
                            "url": "https://github.com/octocat/Hello-World/issues/3021",
                            "labels": {
                                "edges": []
                            }
                        }
                    },
                    {
                        "node": {
                            "title": "Found a bug",
                            "url": "https://github.com/octocat/Hello-World/issues/3056",
                            "labels": {
                                "edges": []
                            }
                        }
                    },
                    {
                        "node": {
                            "title": "test linked issue",
                            "url": "https://github.com/octocat/Hello-World/issues/3057",
                            "labels": {
                                "edges": []
                            }
                        }
                    },
                    {
                        "node": {
                            "title": "Hello world",
                            "url": "https://github.com/octocat/Hello-World/issues/3067",
                            "labels": {
                                "edges": []
                            }
                        }
                    },
                    {
                        "node": {
                            "title": "Hello, world!",
                            "url": "https://github.com/octocat/Hello-World/issues/3103",
                            "labels": {
                                "edges": []
                            }
                        }
                    },
                    {
                        "node": {
                            "title": "Dfr",
                            "url": "https://github.com/octocat/Hello-World/issues/3127",
                            "labels": {
                                "edges": []
                            }
                        }
                    },
                    {
                        "node": {
                            "title": "01-10-Tree",
                            "url": "https://github.com/octocat/Hello-World/issues/3161",
                            "labels": {
                                "edges": []
                            }
                        }
                    },
                    {
                        "node": {
                            "title": "https://developer.grammarly.com/API KEY QfXE9AHW2PdtmBDO6ppHXsM1",
                            "url": "https://github.com/octocat/Hello-World/issues/3168",
                            "labels": {
                                "edges": []
                            }
                        }
                    },
                    {
                        "node": {
                            "title": "N",
                            "url": "https://github.com/octocat/Hello-World/issues/3183",
                            "labels": {
                                "edges": []
                            }
                        }
                    },
                    {
                        "node": {
                            "title": "Bug: Issue with loading the homepage",
                            "url": "https://github.com/octocat/Hello-World/issues/3202",
                            "labels": {
                                "edges": []
                            }
                        }
                    }
                ]
            }
        }
    }
}

GraphQL APIに初めて触れた人が直面する疑問

『こんな感じで取得できるんか便利そうだね!』

────んで、このクエリどうやって作った...????????????

『公式ドキュメントでクエリの例って説明されてるけど、いまいちよく分からんな...。』

公式ドキュメントのサンプルコードを1行ずつ補足します

おそらくこんな動機があったのでしょう。

octocat/Hello-World リポジトリ内で、解決された issue のうち最新の 20 個を取得し、各 issue のタイトル、URL、最初の 5 つのラベルを見たいなぁ!!』

というわけで、以下でクエリを 1 行ずつ解説していきます。

はじめてのGitHub-GraphQL_全体.png

① Query タイプのオペレーション宣言

はじめてのGitHub-GraphQL_query.png

  • query {
    Because we want to read data from the server, not modify it, query is the root operation. (If you don't specify an operation, query is also the default.)

GitHub GraphQLのオペレーション種類 で説明しましたが、GitHub GraphQL API では以下の2つのオペレーションをサポートしています。

  • Query:データを取得する(読む)ための操作
  • Mutation:データを作成、編集、削除など、データを更新するための操作

今回の GraphQL を利用する動機が「最新の issue を見たい」なので、利用するのは Query オペレーションです。

クエリの頭で query と宣言していますが、GraphQL のサーバーが処理を行うための起点になるため、オペレーションの宣言はクエリ内に必ず含める必要があります。

また、クエリに名前を付けることも可能です。

公式ドキュメントのQueryにFetchRecentClosedIssuesという名前を付けました
curl -X POST -H "Authorization: bearer YOUR_ACCESS_TOKEN" -H "Content-Type: application/json" --data '{ "query": "query FetchRecentClosedIssues { repository(owner: \"octocat\", name: \"Hello-World\") { issues(last: 20, states: CLOSED) { edges { node { title url labels(first: 5) { edges { node { name } } } } } } } }" }' https://api.github.com/graphql

② Query タイプが持つ repository フィールドへのクエリ

はじめてのGitHub-GraphQL_repository.png

  • repository(owner:"octocat", name:"Hello-World") {
    To begin the query, we want to find a repository object. The schema validation indicates this object requires an owner and a name argument.

日本語版では、「クエリを開始するには、repository オブジェクトを検索します。 スキーマの検証により、このオブジェクトに owner と name の引数が必要であることがわかります。」とありますが、初見ではまぁ意味がわかりません。

そこで、まず見るべきドキュメントはコチラになります。

The query type defines GraphQL operations that retrieve data from the server.

GitHub GraphQL Query の中には、クエリの中の大分類ともいえるいくつかの種類が用意されているのですが、ここでは「Queryの中でもどういったQuery?」を宣言することで絞りを入れるようなイメージです。

公式ドキュメントの Example query の例では「octocat/Hello-World リポジトリ内で、解決された issue」を取得したいので、repository を選択しています。

さて、repository のドキュメントを確認すると「Arguments for repository」で必要な引数が記載されています。

  • followRenames (Boolean):デフォルトで true
  • name (String!):リポジトリ名
  • owner (String!):リポジトリのオーナー名(ユーザー名または組織名)

必須なのは ownername なので、octocat/Hello-World リポジトリに対するクエリの場合はサンプルの通り、 repository(owner:"octocat", name:"Hello-World") { になるというわけですね。

③ repository フィールドが持つ issues フィールドへのクエリ

はじめてのGitHub-GraphQL_issues.png

  • issues(last:20, states:CLOSED) {
    To account for all issues in the repository, we call the issues object. (We could query a single issue on a repository, but that would require us to know the number of the issue we want to return and provide it as an argument.)

先ほどの ② Query タイプが持つ repository フィールドへのクエリrepository クエリを選択したことは分かりました。次の関心事は、「リポジトリ情報からどの情報を引っ張ってくるか?」です。

そのためにまず、この Repository クエリがどんなオブジェクトを返すのかを知る必要があります。

方法は2つあり、

です。

【click to expand】※クエリを投げる場合はこんな感じで確認できます
repositoryクエリが返すオブジェクトを特定するクエリ
curl -X POST -H "Authorization: bearer YOUR_ACCESS_TOKEN" -H "Content-Type: application/json" --data '{ "query": "query { __type(name: \"Repository\") { name kind description fields { name } } }" }' https://api.github.com/graphql

Repository という名前のオブジェクトが返ることが分かります。

戻ってくるJSONオブジェクト
{
    "data": {
        "__type": {
            "name": "Repository",
            "kind": "OBJECT",
            "description": "A repository contains the content for a project.",
            "fields": [
                {
                    "name": "allowUpdateBranch"
                },
                {
                    "name": "archivedAt"
                },
                {
                    "name": "assignableUsers"
                },
                {
                    "name": "autoMergeAllowed"
                },
                {
                    "name": "branchProtectionRules"
                },
                ※以下略

というわけで、 repository クエリは repository オブジェクトを返すことが分かりました。そこが分かったら、repository オブジェクトに関するドキュメントを見てみましょう。

ドキュメントを確認すると、repository オブジェクトはたくさんのフィールドを持っていることが分かります。

今回は「解決された issue のうち最新の 20 件」が欲しいので、個別の Issue である issue フィールドではなく issues フィールドを選択します。

以下で、公式ドキュメントの クエリの例 で説明されている Some details about the issues object: の部分をいい感じに翻訳します。

  • The docs tell us this object has the type IssueConnection.

訳:issues というフィールドが IssueConnection 型を返すことを意味しており、IssueConnection 型はnon-nullable(このフィールドがnullを返すことはない!)であることを示しているよ。

はじめてのGitHub-GraphQL_IssueConnection.png

※ GraphQLの型システムでは、型名の後に ! が付くことで、その型がnon-nullableであると指定されます

  • Schema validation indicates this object requires a last or first number of results as an argument, so we provide 20.

訳:Arguments for issues に記載されている引数を指定することで、絞りをかけられるよ。今回だったら「Issue のうち最新の20件」が欲しいから、last:20 で指定するよ。

  • The docs also tell us this object accepts a states argument, which is an IssueState enum that accepts OPEN or CLOSED values. To find only closed issues, we give the states key a value of CLOSED.

訳:Arguments for issues では states も引数で指定できるって書いてあるよ。「解決済み Issues」の情報が欲しいので、states:CLOSED を指定するよ。

④ Issues フィールドが持つ edges フィールドへのクエリ

はじめてのGitHub-GraphQL_edges1.png

  • edges {
    We know issues is a connection because it has the IssueConnection type. To retrieve data about individual issues, we have to access the node via edges.

上記の説明文を1つずつ解読します。

We know issues is a connection because it has the IssueConnection type.

GraphQL には connection という概念があります。

GitHub GraphQL API での 1 度のクエリリクエストにより、特定の connection オブジェクト(例えば IssueConnection )が 1 つ得られます。connection オブジェクトはそれに関連する複数のアイテム(この場合は Issue の)情報をリストで持っています。(例えば edges フィールドや nodes フィールドなど)

詳しくはこちらの記事を読むとイメージが付くかと思います。(図解がとても分かりやすくてありがたい)

なので、とりあえず「issues フィールドの戻り値は IssueConnection 型ってことは、Issue に関する connection オブジェクトを介してデータを取得することができるんだな」くらいの理解でいったんよいと思います。

To retrieve data about individual issues, we have to access the node via edges.

「個々の issue に関するデータを取得するには、edges を介して node にアクセスする必要があるよ」とのことですが、実は Example query の場合は edges を介さなくても同じデータを取得することができます。

例えば、IssueConnection オブジェクトは以下のように edgesnodespageInfototalCount と4つのフィールドを持っています。

IssueConnectionの情報
{
    "data": {
        "__type": {
            "name": "IssueConnection",
            "kind": "OBJECT",
            "description": "The connection type for Issue.",
            "fields": [
                {
                    "name": "edges",
                    "args": []
                },
                {
                    "name": "nodes",
                    "args": []
                },
                {
                    "name": "pageInfo",
                    "args": []
                },
                {
                    "name": "totalCount",
                    "args": []
                }
            ]
        }
    }
}

GitHub公式ドキュメント内の Example query では上記の edges フィールドを使っていますが、以下のような nodes フィールドを使う形でも同じデータが取れます。

GitHub公式ドキュメント内の Example query のedges使わないバージョン
query {
  repository(owner:"octocat", name:"Hello-World") {
    issues(last:20, states:CLOSED) {
      nodes {
        title
        url
        labels(first:5) {
          nodes {
            name
          }
        }
      }
    }
  }
}

なのですが、なぜここで 「個々の issue に関するデータを取得するには edges を介してね」と言われているかというと、簡単に言えば今回のような IssueConnection オブジェクト(つまりはIssueの一覧)を操作する際に、edges を介したほうが融通が利きやすいからです。

今回の例からは話が逸れますが、例えば直近に起票された Issue とその関連情報を 500 件取得したいとして、1 度のリクエストで 100 件分を取得しようとすると、リクエストを 5 回投げる必要があります。このとき、100 件目と 101 件目をどう判別したらよいでしょうか。

IssueConnection オブジェクトのような複数の Issue を内包したオブジェクトにおいて、edges は各 node (つまり個々のIssue)への参照だけでなく、各 node がオブジェクト全体の中でどの位置にいるかという追加情報(ページネーション用の cursor など)を提供します。

Issue が多数ある場合、全てを一度にロードしてクライアントに返すのは効率が悪い(もしかしたら不可能かも)場合があるので、GraphQL では edges を通じてそれぞれの node にアクセスし、各 edge が持つ cursor を利用して、どのデータが既に取得されたかを追跡しながらページネーションを効果的に実施できるようになっています。

まとめるとこんな感じです。

  • edges フィールドはページネーションや node のメタデータが必要な場合に便利
  • nodes フィールドは直接 node にアクセスするようなシンプルなデータ取得の場合に効率的

話を戻すと、個別の Issue に関わる周辺情報などを取得する場合は、 edges を介してデータ取得すると融通利くよ!ということが分かっていればよい気がします。

⑤ edges フィールドが持つ node フィールドへのクエリ

はじめてのGitHub-GraphQL_node1.png

- node {
Here we retrieve the node at the end of the edge. The IssueConnection docs indicate the node at the end of the IssueConnection type is an Issue object.

IssueConnection フィールドが持つ edges フィールドは、リスト型の IssueEdge オブジェクトを返します。で、IssueEdge オブジェクトはノードとして、欲しかった Issue 情報である Issue オブジェクトを持っていることが分かります。

⑥ node フィールドが持つ各フィールドへのクエリ

はじめてのGitHub-GraphQL_issue_fields.png

  • Now that we know we're retrieving an Issue object, we can look at the docs and specify the fields we want to return:
    Here we specify the title, url, and labels fields of the Issue object.

はい、本命の Issue の情報までたどり着きました。

もともとの動機が『 octocat/Hello-World リポジトリ内で、解決された issue のうち最新の 20 個を取得し、各 issue のタイトル、URL、最初の 5 つのラベルを見たいなぁ!!』だったので、titleurllabel を探します。結果、以下のような戻り値であると分かりました。

  • title (String!)
  • url (URI!)
  • labels (LabelConnection)

今まで「このフィールドはこのオブジェクトを返す」で辿ってきましたが、どこまでいけば終わりが来るのでしょうか。その答えは以下のドキュメントにまとまっています。

GraphQL queries return only the data you specify. To form a query, you must specify fields within fields (also known as nested subfields) until you return only scalars.

Scalars are primitive values: Int, Float, String, Boolean, or ID.
When calling the GraphQL API, you must specify nested subfields until you return only scalars.

GraphQLにおいて、スカラー(Scalar)はプリミティブな値を表しており、ドキュメントでは IntFloatStringBooleanID がそれにあたると示されています。なので、それ以上分解できない単一の値(スカラー)になるまで、つまり戻り値が IntFloatStringBooleanID になるところまで指定しないといけないということですね。

これを踏まえて、上記の titleurllabels フィールドを見てみます。

  • title (String!):String なのでここが末端
  • url (URI!):ドキュメント より形式が指定された String なのでここが末端
  • labels (LabelConnection):Connection オブジェクトなのでさらに深堀りが必要になる

もうちょっとがんばりましょう。

⑦ labels フィールドが持つ各フィールドへのクエリ

はじめてのGitHub-GraphQL_labels.png

The labels field has the type LabelConnection. As with the issues object, because labels is a connection, we must travel its edges to a connected node: the label object. At the node, we can specify the label object fields we want to return, in this case, name.

もはや IssueConnection と同じです。

labels (LabelConnection) > edges ([LabelEdge]) > node (Label) > name (String!) の順番に進んでいくと、最終的にプリミティブな値であるスカラー(String)にたどり着きました。

Example query を振り返る

クエリを 1 行ずつ上から見てきましたが、いかがでしたでしょうか。

はじめてのGitHub-GraphQL_全体.png

ドキュメントの例ではクエリ型の選択という大きいところから出発して開設されていますが、実際にクエリを作成する際にはほしいデータからの逆算で考えるとよいかもしれません。

「戻り値がスカラーである取得したいフィールド」から逆算して、そのフィールドがどのオブジェクトに属しているのかを 1 つずつ見ていきながらクエリを作成するという感じになるかもです。

GitHub GraphQL API(query) の全体を把握する

GraphQL はデータを要求し取得するための強力なクエリ言語ですが、その効率と柔軟性の秘密は「スキーマ」にあります。

スキーマは GraphQL API を利用する上での地図のようなもので、スキーマへの理解があれば、自分でクエリを作成していくことができるようになります。

以下は、公式が提供している schema.docs.graphql という GraphQL を利用するうえで必要になるスキーマ情報から、今回解説した Example query に関わる部分を抜き出したものです。

schema.docs.graphqlから今回見た部分を抜き出したもの
type Query {
  repository(
    followRenames: Boolean = true
    name: String!
    owner: String!
  ): Repository
}

type Repository implements Node & PackageOwner & ProjectOwner & ProjectV2Recent & RepositoryInfo & Starrable & Subscribable & UniformResourceLocatable {
  issues(
    after: String
    before: String
    filterBy: IssueFilters
    first: Int
    labels: [String!]
    last: Int
    orderBy: IssueOrder
    states: [IssueState!]
  ): IssueConnection!
}

type IssueConnection {
  edges: [IssueEdge]
  nodes: [Issue]
  pageInfo: PageInfo!
  totalCount: Int!
}

type IssueEdge {
  cursor: String!
  node: Issue
}

type Issue implements Assignable & Closable & Comment & Deletable & Labelable & Lockable & Node & ProjectV2Owner & Reactable & RepositoryNode & Subscribable & SubscribableThread & UniformResourceLocatable & Updatable & UpdatableComment {
  labels(
    after: String
    before: String
    first: Int
    last: Int
    orderBy: LabelOrder = {field: CREATED_AT, direction: ASC}
  ): LabelConnection
  title: String!
  url: URI!
}

type LabelConnection {
  edges: [LabelEdge]
  nodes: [Label]
  pageInfo: PageInfo!
  totalCount: Int!
}

type LabelEdge {
  cursor: String!
  node: Label
}

type Label implements Node {
  name: String!
}

例えば、

  • type Queryrepository フィールドを持っており、repository フィールドに対するクエリは repository 型のオブジェクトを返す
  • type repositoryissues フィールドを持っており、issues フィールドに対するクエリは IssueConnection! 型のオブジェクトを返す
  • type IssueConnection edges フィールドを持っており、edges フィールドに対するクエリは リスト型の IssueEdge オブジェクトを返す

といったように、スキーマは入れ子構造のような関係になっていることが分かります。つまり、スキーマを 1 つずつ辿っていくことで、取得したいデータのクエリを組み立てることができるというわけです。

おわりに

ここで紹介したものはあくまで基礎の部分ですが、この辺を分かっていれば関連情報を調べるときに理解しやすくなるかなと思います。

今回の記事では紹介していませんが、インターフェースやエイリアス、Fragment など周辺の概念なども知っておくとより扱える幅が広がります。

世の中にはいっぱい記事があるので、もし興味を持ったら手を動かしてみてください~!

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?