44
25

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

HRBrainAdvent Calendar 2022

Day 21

アニメオタク、GraphQLのクエリを学ぶ

Last updated at Posted at 2022-12-20

はじめに

こんにちは。株式会社HRBrainで内定者インターンをしている橋本です。主にバックエンドの開発をしています。
この記事ではGraphQLのクエリの書き方を、公式ドキュメントの流れに沿って説明していきたいと思います。
本記事はHRBrain Advent Calendar 2022の21日目の記事です。

なぜこれを取り上げようと思ったか

インターンとして働いているなかでGraphQLに触れる機会があったのですが、当時私はGraphQLを扱うのが初めてで、クエリの書き方がよくわからず困ってしまいました……。
理解してしまえばなんてことないのですが、最初はかなり苦労した覚えがあるので、そんな過去の私と同じような経験をする人を減らすべく、一歩ずつ丁寧にクエリの書き方について説明したいと思いこの記事を作成するに至りました。
またアニメ好きである私にドンピシャのGraphQLサーバーが提供されていると知ったことも理由の1つです。これは後ほど紹介します。

この記事で説明すること、しないこと

説明すること

  • クエリの基本的な書き方
  • 引数について
  • フラグメントについて
  • ディレクティブについて
  • インラインフラグメントについて

説明しないこと

  • GraphQLとは
  • Query,Mutationとは
  • スキーマ定義の書き方
  • GraphQLサーバーの実装方法

今回取り扱うGraphQLサーバー、AniList APIv2について

今回はこちらのAPIを用いて、クエリの書き方について説明していきたいと思います。1

これはAniListという、気に入ったアニメなどを簡単に管理できるWebアプリが提供しているAPIで、膨大なデータベースからアニメや漫画の検索、キャラクターやスタッフの情報の取得などが行えます。ガイドラインに沿った非営利目的での利用は無料と案内されているので、ありがたく使わせてもらうことにします。
さらに嬉しいのはこちらからブラウザ上で動かすこともできるということです。面倒な環境構築などは必要ありません。2
使い方もシンプルでわかりやすいです。
image.jpg

また、スキーマ定義などは以下のリンクから確認できます。

眺めてるだけでワクワクしてきますね。
それでは早速このAPIを用いてGraphQLのクエリの書き方について学んでいきましょう。

本編

Fields, Arguments

GraphQLにおける基本的なクエリの書き方は以下の通りです。

基本的なクエリの書き方
{
  クエリ名 (引数) {
    フィールド1
    フィールド2
  	...
    フィールドN
}

この書き方にならってMediaというQueryを叩いてみましょう。これは引数にとった値から該当するアニメや漫画の情報を取得するQueryです。最初なので丁寧に進めます。

クエリを叩くためにまずはMediaのスキーマ定義を確認しましょう。
こちらの検索窓にMediaと入力してみると、以下のような検索結果が得られると思います。
image.png
ふむふむ、引数にはid,idMal,startDateなどを入れることができ、そこからMediaという型で戻り値を得ることができるのですね、(説明も簡潔についていてわかりやすい!)
またMedia型の詳細についても確認できます。
image.png

今回はsearchという引数にone pieceを指定することにしましょう。また得られるMedia型の戻り値のなかでもgenresとそのアニメの人気度を表すaverageScoreというフィールドを指定してクエリを叩いてみることにします。最終的なクエリは以下のようになります。

クエリ
{
  Media (search:"one piece") {
    genres
    averageScore
  }
}

ちなみにクエリの入力中にCtrl+Cを押すと補完が聞くので、適宜活用すると便利です。
それでは早速画面上部の▶ボタンを押してクエリを実行してみましょう!すると画面右側に次のようなレスポンスが表示されます。

レスポンス
{
  "data": {
    "Media": {
      "genres": [
        "Action",
        "Adventure",
        "Comedy",
        "Fantasy"
      ],
      "averageScore": 91
    }
  }
}

バッチリデータを得ることができています。averageScore91とは、さすがワンピースは人気ですね。これがGraphQLの基本的なクエリの書き方になります。

次はtitleフィールドを取得してみましょう。これはその名の通り、メディアのタイトル情報を表すフィールドです。
スキーマ定義を確認すると、titleフィールドはMediaTitle型のオブジェクトを返すようです。
image.png
またMediaTitle型にはromaji,english,native,userPreferredというフィールドがあり、それぞれ「現地語でのローマ字表記」、「英語圏での公式タイトル」、「現地語での公式タイトル」、「認証済みユーザーの選択言語」が対応しています。
image.png
今回はromajienglishを指定してクエリを叩いてみます。この場合のクエリとレスポンスは以下のようになります。フィールドをネストしている部分が前回と違いますね。

クエリ
{
  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

今まで書いてきたクエリには任意で名前をつけることができます。これをオペレーション名といいます。オペレーション名をつけることで、そのクエリが何を取得するものなのかが一目でわかるようになります。

具体的な実装方法としては、今まで書いてきたクエリの先頭に(クエリ形式) (オペレーション名)とつけるだけです。クエリ形式にはqueryMutationsubscriptionの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ステップで実装できます。

  1. オペレーション名に続けて外から受け取る引数を$(変数名)という書式で指定し、その型を書きます。
    (今回はTitleという引数をString型で受け取る。)
  2. 受け取った引数を使いたいクエリには$(変数名)という形で渡します。(今回の場合だとsearch: $Title
  3. 引数をjson形式で渡します。

この手順に沿って書き換えたクエリと引数は以下のようになります。

クエリ
query ComparisonToOnePiece($Title: String) {
  ONE_PIECE: Media(search: "one piece") {
    ...TitleAndScore
  }
  MyFavoriteAnime: Media(search: $Title) {
    ...TitleAndScore
  }
}

fragment TitleAndScore on Media {
  title {
    native
  }
  averageScore
}
Query Variables
{
  "Title": "banana fish"
}

GraphiQLでは左下に変数を入力するところがあることに注意してください。ここまで入力して実行すると、期待通りの結果が得られます。これでいろんなアニメとワンピースを比較することが、渡す引数を変更するだけでできるようになりました。
image.png

また型定義の横に!を入れるとその引数が必須であることを要請できます。

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を追加してクエリを実行してみましょう。

Query Variables
{
  "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の場合にはuserIdtextsiteUrlのフィールドを取得する」ということを宣言しています。

    ... on TextActivity{
      userId
      text
      siteUrl
    }

他2つ(ListActivityMessageActivity)も同様です。これでユニオン型を戻り値にもつクエリに対してもクエリが書けるようになりました。実際にクエリを実行してみましょう。
image.png
無事にレスポンスが返ってきました。(ちなみにここで取得しているActivityは私が投稿したものです。)
image.png

最後に

この記事では丁寧にGraphQLのクエリの書き方について見てきました。この記事では紹介しきれなかった機能がGraphQLにはまだまだあるので、興味がある方は公式のドキュメントをご覧ください。

そして株式会社HRBrainでは新しいメンバーを募集しています。

とても働きやすい環境で、インターン生でありながらも新規の機能開発などをのびのびやらせてもらっています。興味がある方はぜひご応募ください。

  1. 他にも簡単に試せるGraphQLサーバーはいろいろあり、こちらにリストアップされてまとめられてます。

  2. GraphiQLとはブラウザ上で動かせるGraphQL IDEの1つです。GraphQL Playgroundとほとんど同じように扱えます。

  3. 私は受験生のころにBANANA FISHを見たのですが、その怒涛の展開に心を支配されてしばらく勉強に身が入らなくなってしまいました。それくらい面白いアニメです。

44
25
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
44
25

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?