はじめに
こんにちは。株式会社HRBrainで内定者インターンをしている橋本です。主にバックエンドの開発をしています。
この記事ではGraphQLのクエリの書き方を、公式ドキュメントの流れに沿って説明していきたいと思います。
本記事はHRBrain Advent Calendar 2022の21日目の記事です。
なぜこれを取り上げようと思ったか
インターンとして働いているなかでGraphQLに触れる機会があったのですが、当時私はGraphQLを扱うのが初めてで、クエリの書き方がよくわからず困ってしまいました……。
理解してしまえばなんてことないのですが、最初はかなり苦労した覚えがあるので、そんな過去の私と同じような経験をする人を減らすべく、一歩ずつ丁寧にクエリの書き方について説明したいと思いこの記事を作成するに至りました。
またアニメ好きである私にドンピシャのGraphQLサーバーが提供されていると知ったことも理由の1つです。これは後ほど紹介します。
この記事で説明すること、しないこと
説明すること
- クエリの基本的な書き方
- 引数について
- フラグメントについて
- ディレクティブについて
- インラインフラグメントについて
説明しないこと
- GraphQLとは
- Query,Mutationとは
- スキーマ定義の書き方
- GraphQLサーバーの実装方法
今回取り扱うGraphQLサーバー、AniList APIv2について
今回はこちらのAPIを用いて、クエリの書き方について説明していきたいと思います。1
これはAniListという、気に入ったアニメなどを簡単に管理できるWebアプリが提供しているAPIで、膨大なデータベースからアニメや漫画の検索、キャラクターやスタッフの情報の取得などが行えます。ガイドラインに沿った非営利目的での利用は無料と案内されているので、ありがたく使わせてもらうことにします。
さらに嬉しいのはこちらからブラウザ上で動かすこともできるということです。面倒な環境構築などは必要ありません。2
使い方もシンプルでわかりやすいです。
また、スキーマ定義などは以下のリンクから確認できます。
眺めてるだけでワクワクしてきますね。
それでは早速このAPIを用いてGraphQLのクエリの書き方について学んでいきましょう。
本編
Fields, Arguments
GraphQLにおける基本的なクエリの書き方は以下の通りです。
{
クエリ名 (引数) {
フィールド1
フィールド2
...
フィールドN
}
この書き方にならってMedia
というQueryを叩いてみましょう。これは引数にとった値から該当するアニメや漫画の情報を取得するQueryです。最初なので丁寧に進めます。
クエリを叩くためにまずはMedia
のスキーマ定義を確認しましょう。
こちらの検索窓にMedia
と入力してみると、以下のような検索結果が得られると思います。
ふむふむ、引数にはid
,idMal
,startDate
などを入れることができ、そこからMedia
という型で戻り値を得ることができるのですね、(説明も簡潔についていてわかりやすい!)
またMedia
型の詳細についても確認できます。
今回はsearch
という引数にone piece
を指定することにしましょう。また得られるMedia
型の戻り値のなかでもgenres
とそのアニメの人気度を表すaverageScore
というフィールドを指定してクエリを叩いてみることにします。最終的なクエリは以下のようになります。
{
Media (search:"one piece") {
genres
averageScore
}
}
ちなみにクエリの入力中にCtrl+C
を押すと補完が聞くので、適宜活用すると便利です。
それでは早速画面上部の▶ボタンを押してクエリを実行してみましょう!すると画面右側に次のようなレスポンスが表示されます。
{
"data": {
"Media": {
"genres": [
"Action",
"Adventure",
"Comedy",
"Fantasy"
],
"averageScore": 91
}
}
}
バッチリデータを得ることができています。averageScore
が91
とは、さすがワンピースは人気ですね。これがGraphQLの基本的なクエリの書き方になります。
次はtitle
フィールドを取得してみましょう。これはその名の通り、メディアのタイトル情報を表すフィールドです。
スキーマ定義を確認すると、title
フィールドはMediaTitle
型のオブジェクトを返すようです。
またMediaTitle
型にはromaji
,english
,native
,userPreferred
というフィールドがあり、それぞれ「現地語でのローマ字表記」、「英語圏での公式タイトル」、「現地語での公式タイトル」、「認証済みユーザーの選択言語」が対応しています。
今回はromaji
とenglish
を指定してクエリを叩いてみます。この場合のクエリとレスポンスは以下のようになります。フィールドをネストしている部分が前回と違いますね。
{
Media (search:"one piece") {
title{
native
english
}
}
}
{
"data": {
"Media": {
"title": {
"native": "ONE PIECE",
"english": "One Piece"
}
}
}
なんと、英語圏の公式タイトルは日本のものと違うんですね。初めて知りました……。
Aliases
さて、次はワンピースの人気度を他のアニメと比べてみることにしましょう。
先ほどの書式通りに書くと次のようになります。しかしこれだとエラーが発生し、期待したレスポンスを得ることができません。
{
Media (search:"one piece") {
title{
native
}
averageScore
}
Media (search:"banana fish") { # banana fishというアニメと比較する。
title{
native
}
averageScore
}
}
{
"errors": [
{
"message": "Fields \"Media\" conflict because they have differing arguments. Use different aliases on the fields to fetch both if this was intentional.",
"status": 400,
"locations": [
{
"line": 2,
"column": 3
},
{
"line": 8,
"column": 3
}
]
}
],
"data": null
}
このエラーの原因は、(エラーメッセージにもあるように)戻り値にMedia
というフィールドが複数含まれ、衝突が起こるためです。これを回避するためにalias
を利用します。
なにも難しいことはなく、クエリの前にalias名:
を追加するだけです。
{
- Media(search: "one piece") {
+ ONE_PIECE: Media (search:"one piece") {
title {
native
}
averageScore
}
- Media(search: "banana fish") {
+ BANANA_FISH: Media(search: "banana fish") {
title {
native
}
averageScore
}
}
{
"data": {
"ONE_PIECE": {
"title": {
"native": "ONE PIECE"
},
"averageScore": 91
},
"BANANA_FISH": {
"title": {
"native": "BANANA FISH"
},
"averageScore": 84
}
}
}
私のお気に入りのアニメであるBANANA FISHは惜しくも84点でした。良いアニメなので皆さんぜひ見てみてください。(布教)3
Fragments
お次はフラグメントです。フラグメントを使うと一連のフィールドを再利用できます。
実は先ほどのクエリには少し冗長な部分があるので、それをフラグメントを用いて書き直してみましょう。
冗長なのは具体的には以下の部分です。このフィールド指定がクエリの中で2回繰り返されています。
title {
native
}
averageScore
今回は指定しているフィールドが少なく、かつ繰り返しも2回だったのでそこまで問題にはなりません。しかし実際のプロダクトコードのなかでは膨大なフィールドの塊を何回も再利用する場面が出てきます。そうなると非常にコードが読みづらくなります。考えるだけで恐ろしいです。
それを解決するのがフラグメントです。百聞は一見にしかず、ということで書き直した例を早速見てもらいましょう。
{
ONE_PIECE: Media(search: "one piece") {
...TitleAndScore
}
BANANA_FISH: Media(search: "banana fish") {
...TitleAndScore
}
}
fragment TitleAndScore on Media {
title {
native
}
averageScore
}
先ほど繰り返していた冗長な部分がTitleAndScore
というフラグメントにまとめられてスッキリしました。on Media
というのは「TitleAndScore
というフラグメントがMedia
型の一部のフィールドを指定している」ということを表しています。
Fragment
はもともと欠片という意味だと思い出せば理解しやすいですね。
Operation name
今まで書いてきたクエリには任意で名前をつけることができます。これをオペレーション名といいます。オペレーション名をつけることで、そのクエリが何を取得するものなのかが一目でわかるようになります。
具体的な実装方法としては、今まで書いてきたクエリの先頭に(クエリ形式) (オペレーション名)
とつけるだけです。クエリ形式にはquery
、Mutation
、subscription
の3種類がありますが、今回のクエリ形式はデータを取得するquery
なので次のようになります。
+ query ComparisonToOnePiece{
- {
ONE_PIECE: Media(search: "one piece") {
...TitleAndScore
}
BANANA_FISH: Media(search: "banana fish") {
...TitleAndScore
}
}
fragment TitleAndScore on Media {
title {
native
}
averageScore
}
レスポンスは先ほどと全く変わりません。ただクエリに名前をつけただけなので。
Variables
今までは引数をクエリのなかにベタ書きしていましたが、これを外から渡せるような形式にしてみましょう。
具体的には、外から引数として渡したアニメタイトルとワンピースの人気度を比較できるようにしてみましょう。
これは公式のドキュメントにもあるように、次の3ステップで実装できます。
- オペレーション名に続けて外から受け取る引数を
$(変数名)
という書式で指定し、その型を書きます。
(今回はTitle
という引数をString
型で受け取る。) - 受け取った引数を使いたいクエリには
$(変数名)
という形で渡します。(今回の場合だとsearch: $Title
) - 引数をjson形式で渡します。
この手順に沿って書き換えたクエリと引数は以下のようになります。
query ComparisonToOnePiece($Title: String) {
ONE_PIECE: Media(search: "one piece") {
...TitleAndScore
}
MyFavoriteAnime: Media(search: $Title) {
...TitleAndScore
}
}
fragment TitleAndScore on Media {
title {
native
}
averageScore
}
{
"Title": "banana fish"
}
GraphiQLでは左下に変数を入力するところがあることに注意してください。ここまで入力して実行すると、期待通りの結果が得られます。これでいろんなアニメとワンピースを比較することが、渡す引数を変更するだけでできるようになりました。
また型定義の横に!
を入れるとその引数が必須であることを要請できます。
query ComparisonToOnePiece($Title: String!) {
# 略
}
さらに型宣言の後にデフォルト値を追加することで、クエリ内の変数にデフォルト値を割り当てることもできます。このようにすると引数に何も渡さなかった場合はこのデフォルト値が代わりに渡されることになります。
query ComparisonToOnePiece($Title: String = "banana fish") {
# 略
}
Directives
ディレクティブはフィールドまたはフラグメントに添付することができ、クエリの実行に影響を与えることができます。早速例を見てみましょう。
- query ComparisonToOnePiece($Title: String) {
+ query ComparisonToOnePiece($Title: String, $GetID: Boolean!) {
ONE_PIECE: Media(search: "one piece") {
...TitleAndScore
}
MyFavoriteAnime: Media(search: $Title) {
...TitleAndScore
}
}
fragment TitleAndScore on Media {
+ id @include(if: $GetID)
title {
native
}
averageScore
}
新しいGetID
という引数と@include
というオプションが追加されました。
この@include
というのがディレクティブです。もう一つ@skip
というディレクティブも用意されています。これらは次のような機能をもっています。
ディレクティブ | 説明 |
---|---|
@include(if: Boolean) |
このデイレクティブをつけたフィールドは、引数がtrue の場合のみ結果に含める。 |
@skip(if: Boolean) |
このデイレクティブをつけたフィールドは、引数がtrue の場合はスキップされる。 |
名前そのまんまの感じで覚えやすいですね。
つまり今回行った変更としては、ディレクティブ@include
と新しい引数であるGetID
を追加して、id
というフィールドを取得するかどうかを引数を経由することで選択できるようになったということです。
引数GetID
をフラグメントの定義内から参照できているところもポイントですね。
それでは実際に引数に"GetID":true
を追加してクエリを実行してみましょう。
{
"Title": "banana fish",
"GetID": true
}
{
"data": {
"ONE_PIECE": {
"id": 30013,
"title": {
"native": "ONE PIECE"
},
"averageScore": 91
},
"MyFavoriteAnime": {
"id": 100388,
"title": {
"native": "BANANA FISH"
},
"averageScore": 84
}
}
}
確かにid
を取得できていますね。
このディレクティブという機能は次のような使用場面が想定されています。(公式から引用)
For example, we can imagine a UI component that has a summarized and detailed view, where one includes more fields than the other.
(たとえば、一方が他方よりも多くのフィールドを含む、要約ビューと詳細ビューを持つ UIコンポーネントを想像できます。(Google翻訳))
Mutations
Mutation
のクエリを叩くには認証が必要になるのでこの記事では割愛します。しかしこれもQuery
の場合と大して変わらないため、ここまで読み進められた方であればすぐに理解できると思います。
Inline Fragments
GraphQLのスキーマ定義ではユニオン型を定義できます。
例えばAniList APIのActivity
というクエリでは戻り値としてActivityUnion
という型が指定されており、これは次のようなユニオン型のオブジェクトです。
union ActivityUnion = TextActivity | ListActivity | MessageActivity
よってActivity
というクエリは実行するまで、上の3つの型のどれが返されるのかわからないということになります。当然型が違えばそこに定義されているフィールドも異なります。
一方でクエリは取得したいフィールドを指定して書いていました、この場合のクエリはどのように書けば良いのでしょうか?
それを解決するのがインラインフラグメントです。
これは場合分けの要領で、返ってくる型に合わせたフィールドを定義するものとなっています。
今回も先に実装例を見てみましょう。
query GetActivityInfo($id: Int) {
Activity(id: $id){
__typename
... on TextActivity{
userId
text
siteUrl
}
... on ListActivity{
userId
status
media{
title
}
siteUrl
}
... on MessageActivity{
id
message
siteUrl
}
}
}
上のクエリは、各Activity
に紐付けられているid
を引数にとって、そのid
に対応するActivity
の情報を返すものとなっています。
そしてそのActivity
の型に合わせて取得するフィールドを変更しています。
まず__typename
というフィールドでは戻り値がどの型なのかを取得できます。
クライアント側はここで取得できる情報を使うことで、処理内容を変えることができます。
そして続く... on <オブジェクト型>
という記述では、戻り値が<オブジェクト型>
だった場合に取得したいフィールド名を指定しています。下の例では「戻り値の型がTextActivity
の場合にはuserId
とtext
、siteUrl
のフィールドを取得する」ということを宣言しています。
... on TextActivity{
userId
text
siteUrl
}
他2つ(ListActivity
、MessageActivity
)も同様です。これでユニオン型を戻り値にもつクエリに対してもクエリが書けるようになりました。実際にクエリを実行してみましょう。
無事にレスポンスが返ってきました。(ちなみにここで取得しているActivity
は私が投稿したものです。)
最後に
この記事では丁寧にGraphQLのクエリの書き方について見てきました。この記事では紹介しきれなかった機能がGraphQLにはまだまだあるので、興味がある方は公式のドキュメントをご覧ください。
そして株式会社HRBrainでは新しいメンバーを募集しています。
とても働きやすい環境で、インターン生でありながらも新規の機能開発などをのびのびやらせてもらっています。興味がある方はぜひご応募ください。