はじめに
初めて GraphQL API(query) を使う人が、GitHub GraphQL API の公式ドキュメント読んだだけでは確実につまづくだろうなぁというポイントをまとめています。
あくまで大枠を理解してもらうことに主眼を置いているので、細かい仕様などは説明しきれていませんがご了承ください。
記事のゴール
「GraphQLってなに?」という人が、データ取得のクエリを作成できるようになること
※ 本記事では Mutation
については扱いません
参考資料
もくじ
- GraphQL APIの概要
-
クエリ操作について
- クエリの例
- GraphQL APIに初めて触れた人が直面する疑問
- 公式ドキュメントのサンプルコードを1行ずつ補足します
- ① Query タイプのオペレーション宣言
- ② Query タイプが持つ repository フィールドへのクエリ
- ③ repository フィールドが持つ issues フィールドへのクエリ
- ④ Issues フィールドが持つ edges フィールドへのクエリ
- ⑤ edges フィールドが持つ node フィールドへのクエリ
- ⑥ node フィールドが持つ各フィールドへのクエリ
- ⑦ labels フィールドが持つ各フィールドへのクエリ
- Example query を振り返る
- GitHub GraphQL API(query) の全体を把握する
- おわりに
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 で利用されているクエリをそのまま使いつつ解説していきます。
query {
repository(owner:"octocat", name:"Hello-World") {
issues(last:20, states:CLOSED) {
edges {
node {
title
url
labels(first:5) {
edges {
node {
name
}
}
}
}
}
}
}
}
上記をコマンドで実行する場合は以下のようになります。
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 の実行結果
{
"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 行ずつ解説していきます。
- ① Query タイプのオペレーション宣言
- ② Query タイプが持つ repository フィールドへのクエリ
- ③ repository フィールドが持つ issues フィールドへのクエリ
- ④ Issues フィールドが持つ edges フィールドへのクエリ
- ⑤ edges フィールドが持つ node フィールドへのクエリ
- ⑥ node フィールドが持つ各フィールドへのクエリ
- ⑦ labels フィールドが持つ各フィールドへのクエリ
① Query タイプのオペレーション宣言
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 のサーバーが処理を行うための起点になるため、オペレーションの宣言はクエリ内に必ず含める必要があります。
また、クエリに名前を付けることも可能です。
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 フィールドへのクエリ
repository(owner:"octocat", name:"Hello-World") {
To begin the query, we want to find arepository
object. The schema validation indicates this object requires anowner
and aname
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!)
:リポジトリのオーナー名(ユーザー名または組織名)
必須なのは owner
と name
なので、octocat/Hello-World
リポジトリに対するクエリの場合はサンプルの通り、 repository(owner:"octocat", name:"Hello-World") {
になるというわけですね。
③ repository フィールドが持つ issues フィールドへのクエリ
issues(last:20, states:CLOSED) {
To account for all issues in the repository, we call theissues
object. (We could query a singleissue
on arepository
, 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つあり、
- さっきの
repository
クエリのドキュメントを見る - クエリを投げる(Discovering the GraphQL API参照)
です。
【click to expand】※クエリを投げる場合はこんな感じで確認できます
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
という名前のオブジェクトが返ることが分かります。
{
"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を返すことはない!)であることを示しているよ。
※ GraphQLの型システムでは、型名の後に !
が付くことで、その型がnon-nullableであると指定されます
- Schema validation indicates this object requires a
last
orfirst
number of results as an argument, so we provide20
.
訳:Arguments for issues
に記載されている引数を指定することで、絞りをかけられるよ。今回だったら「Issue のうち最新の20件」が欲しいから、last:20
で指定するよ。
- The docs also tell us this object accepts a
states
argument, which is anIssueState
enum that acceptsOPEN
orCLOSED
values. To find only closed issues, we give thestates
key a value ofCLOSED
.
訳:Arguments for issues
では states
も引数で指定できるって書いてあるよ。「解決済み Issues」の情報が欲しいので、states:CLOSED
を指定するよ。
④ Issues フィールドが持つ edges フィールドへのクエリ
edges {
We knowissues
is a connection because it has theIssueConnection
type. To retrieve data about individual issues, we have to access the node viaedges
.
上記の説明文を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
オブジェクトは以下のように edges
、nodes
、pageInfo
、totalCount
と4つのフィールドを持っています。
{
"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
フィールドを使う形でも同じデータが取れます。
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 フィールドへのクエリ
- node {
Here we retrieve the node at the end of the edge. TheIssueConnection
docs indicate the node at the end of theIssueConnection
type is anIssue
object.
IssueConnection
フィールドが持つ edges
フィールドは、リスト型の IssueEdge
オブジェクトを返します。で、IssueEdge
オブジェクトはノードとして、欲しかった Issue 情報である Issue
オブジェクトを持っていることが分かります。
⑥ node フィールドが持つ各フィールドへのクエリ
- 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 thetitle
,url
, andlabels
fields of the Issue object.
はい、本命の Issue の情報までたどり着きました。
もともとの動機が『 octocat/Hello-World
リポジトリ内で、解決された issue のうち最新の 20 個を取得し、各 issue のタイトル、URL、最初の 5 つのラベルを見たいなぁ!!』だったので、title
、url
、label
を探します。結果、以下のような戻り値であると分かりました。
- 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
)はプリミティブな値を表しており、ドキュメントでは Int
、Float
、String
、Boolean
、ID
がそれにあたると示されています。なので、それ以上分解できない単一の値(スカラー)になるまで、つまり戻り値が Int
、Float
、String
、Boolean
、ID
になるところまで指定しないといけないということですね。
これを踏まえて、上記の title
、url
、labels
フィールドを見てみます。
-
title
(String!
):String
なのでここが末端 -
url
(URI!
):ドキュメント より形式が指定されたString
なのでここが末端 -
labels
(LabelConnection
):Connection
オブジェクトなのでさらに深堀りが必要になる
もうちょっとがんばりましょう。
⑦ labels フィールドが持つ各フィールドへのクエリ
The
labels
field has the typeLabelConnection
. As with theissues
object, becauselabels
is a connection, we must travel its edges to a connected node: thelabel
object. At the node, we can specify thelabel
object fields we want to return, in this case,name
.
もはや IssueConnection と同じです。
labels
(LabelConnection
) > edges
([LabelEdge]
) > node
(Label
) > name
(String!
) の順番に進んでいくと、最終的にプリミティブな値であるスカラー(String
)にたどり着きました。
Example query を振り返る
クエリを 1 行ずつ上から見てきましたが、いかがでしたでしょうか。
ドキュメントの例ではクエリ型の選択という大きいところから出発して開設されていますが、実際にクエリを作成する際にはほしいデータからの逆算で考えるとよいかもしれません。
「戻り値がスカラーである取得したいフィールド」から逆算して、そのフィールドがどのオブジェクトに属しているのかを 1 つずつ見ていきながらクエリを作成するという感じになるかもです。
GitHub GraphQL API(query) の全体を把握する
GraphQL はデータを要求し取得するための強力なクエリ言語ですが、その効率と柔軟性の秘密は「スキーマ」にあります。
スキーマは GraphQL API を利用する上での地図のようなもので、スキーマへの理解があれば、自分でクエリを作成していくことができるようになります。
以下は、公式が提供している schema.docs.graphql
という GraphQL を利用するうえで必要になるスキーマ情報から、今回解説した Example query に関わる部分を抜き出したものです。
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 Query
はrepository
フィールドを持っており、repository
フィールドに対するクエリはrepository
型のオブジェクトを返す -
type repository
はissues
フィールドを持っており、issues
フィールドに対するクエリはIssueConnection!
型のオブジェクトを返す -
type IssueConnection
はedges
フィールドを持っており、edges
フィールドに対するクエリは リスト型のIssueEdge
オブジェクトを返す
といったように、スキーマは入れ子構造のような関係になっていることが分かります。つまり、スキーマを 1 つずつ辿っていくことで、取得したいデータのクエリを組み立てることができるというわけです。
おわりに
ここで紹介したものはあくまで基礎の部分ですが、この辺を分かっていれば関連情報を調べるときに理解しやすくなるかなと思います。
今回の記事では紹介していませんが、インターフェースやエイリアス、Fragment など周辺の概念なども知っておくとより扱える幅が広がります。
世の中にはいっぱい記事があるので、もし興味を持ったら手を動かしてみてください~!