🌲

実装しながら学ぶ GraphQL with ポケモン

2022/10/09に公開

最近GraphQLに興味が湧いたので実際に実装して勉強してみることにしました。
実装する際に、何かいいデータの題材がないかと探していると、graphql-pokemonというpokemonのデータセットでgraphqlを実装しているレポジトリがありました。
このレポジトリはNode.jsで実装されており、いくつかQueryも定義されている完成されたものです。
今回は自分で実装して勉強することが目的なのでこのレポジトリにあるポケモンデータセットのみ使わせてもらうことにしました。
pokemon.jsonには初代ポケモンのタイプ、技、体重などの様々なデータが入っています📝
今回はこれをデータベースとしてGraphQLを実装していきます。

環境構築

今回は環境構築が簡単なNode.jsでGraphQLを実装します。
PCにNode.jsがインストールされていれば、簡単に環境構築できます。

必要なパッケージをインストールします。

npm init
npm install graphql express express-graphql

これでNode.jsの環境構築は完了です。
次にポケモンデータセットをダウンロードしておきます。
(実際のアプリケーションではここがRDBやNoSQLなどのDBに置き換わることが多いと思います。)
pokemon.jsonをプロジェクトディレクトリに置いておきます。

├── node_modules/
├── package-lock.json
├── package.json
├── pokemons.json
└── server.js

スキーマの定義

GraphQLではAPIの仕様の定義にスキーマを用います。
以下のようにスキーマを定義してみました。以下で詳しく解説します。

const { buildSchema } = require('graphql');
// GraphQL schema
const schema = buildSchema(`
 type Query {
  pokemon(id: Int!): Pokemon
 },
 
 type Pokemon {
  id: Int
  name: String
  types: [String]
  weight: Weight
  height: Height
 },
 type Weight {
   minimum: String
   maximum: String
 },
 type Height {
   minimum: String
   maximum: String
 }
`);

まずQueryの定義です。このクエリではidを引数としてPokemo型のレスポンスを返すという定義をしています。

type Query {
  pokemon(id: Int!): Pokemon
 },

さらにその下で使用するPokemon型の定義しています。pokemon.jsonには様々なパラメータがありますが、すべてをPokemon型に定義する必要はありません。適宜使いたいパラメータがあれば追加してください。
ここでは id, 名前, タイプ, 体重, 高さ をポケモン型として定義しています。

type Pokemon {
  id: Int
  name: String
  types: [String]
  weight: Weight
  height: Height
 },
 type Weight {
   minimum: String
   maximum: String
 },
 type Height {
   minimum: String
   maximum: String
 }

クエリの実装

ではいよいよQueryを実装します。
以下に今までのコードと合わせた完成版を載せておきます。

const express = require('express');
const graphqlHTTP = require('express-graphql').graphqlHTTP
const { buildSchema } = require('graphql');

const schema = buildSchema(`
 type Query {
  pokemon(id: Int!): Pokemon
  pokemons: [Pokemon]
 },
 type Pokemon {
  id: Int
  name: String
  types: [String]
  weight: Weight
  height: Height
 },
 type Weight {
   minimum: String
   maximum: String
 },
 type Height {
   minimum: String
   maximum: String
 }
`);
const pokemons = require("./pokemons.json");

const getPokemon = function({id}) { 
  return pokemons.filter(pokemon => {
    return pokemon.id == id;
  })[0];
}

const root = {
  pokemon: getPokemon,
};

const app = express();
app.use('/graphql', graphqlHTTP({
 schema: schema,
 rootValue: root,
 graphiql: true
}));

app.listen(4000, () => console.log('Express GraphQL Server Now Running On localhost:4000/graphql'));

APIの実装部分はここですね。
rootでルーティングの定義をします。ここではpokemoというクエリが来たらgetPokemon()が実行されるようにしています。idを引数にとってそのidのpokemonを返すシンプルな関数です。

const getPokemon = function({id}) { 
  return pokemons.filter(pokemon => {
    return pokemon.id == id;
  })[0];
}

const root = {
  pokemon: getPokemon,
};

graphQL実行

ブラウザでhttp://localhost:4000/graphqlにアクセスするとgraphQLのクライアントコンソールを使用することができます。
コンソールの左側に以下のクエリを貼って左上の実行ボタンを押してクエリを送ってみましょう。

query {
  pokemon(id: 1) {
    name,
    types,
    weight {
      minimum,
      maximum
    },
    height {
      minimum,
      maximum
    }
  }
}

すると右側にレスポンスが表示されます。id=1としているので図鑑No.1のフシギダネ(Bulbasaur)がレスポンスとして得られました。

レスポンスを絞る

同じクエリで受け取るパラメータを変更してみましょう。以下のように名前とタイプだけを指定すると、体重、高さのがレスポンスからなくなります。

query {
  pokemon(id: 1) {
    name,
    types,
  }
}

ポケモンらしいクエリを作ってみる

あるポケモンを指定したときにそのポケモンが有利を取れるポケモンたちを返すクエリを作ってみます。ここでの有利とは相手の弱点となるタイプを1つ以上所持していることとします。(種族値、覚える技などはここでは無視します👋)

全体コード

const getAdvantageousPokemons = function({id}) { 
  const type2index = {
    "Normal": 0,
    "Fire": 1,
    "Water": 2,
    "Electric": 3,
    "Grass": 4,
    "Ice": 5,
    "Fighting": 6,
    "Poison": 7,
    "Ground": 8,
    "Flying": 9,
    "Psychic": 10,
    "Bug": 11,
    "Rock": 12,
    "Ghost": 13,
    "Dragon": 14,
    "Dark": 15,
    "Steel": 16,
    "Fairy": 17,
  }

  const typeTable = [
    [0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, -1, -2,  0,  0, -1,  0],
    [0, -1, -1,  0,  1,  1,  0,  0,  0,  0,  0,  1, -1,  0, -1,  0,  1,  0],
    [0,  1, -1,  0, -1,  0,  0,  0,  1,  0,  0,  0,  1,  0, -1,  0,  0,  0],
    [0,  0,  1, -1, -1,  0,  0,  0, -2,  1,  0,  0,  0,  0, -1,  0,  0,  0],
    [0, -1,  1,  0, -1,  0,  0, -1,  1, -1,  0, -1,  1,  0, -1,  0, -1,  0],
    [0, -1, -1,  0,  1, -1,  0,  0,  1,  1,  0,  0,  0,  0,  1,  0, -1,  0],
    [1,  0,  0,  0,  0,  1,  0, -1,  0, -1, -1, -1,  1, -2,  0,  1,  1, -1],
    [0,  0,  0,  0,  1,  0,  0, -1, -1,  0,  0,  0, -1, -1,  0,  0, -2,  1],
    [0,  1,  0,  1, -1,  0,  0,  1,  0, -2,  0, -1,  1,  0,  0,  0,  1,  0],
    [0,  0,  0, -1,  1,  0,  1,  0,  0,  0,  0,  1, -1,  0,  0,  0, -1,  0],
    [0,  0,  0,  0,  0,  0,  1,  1,  0,  0, -1,  0,  0,  0,  0, -2, -1,  0],
    [0, -1,  0,  0,  1,  0, -1, -1,  0, -1,  1,  0,  0, -1,  0,  1, -1, -1],
    [0,  1,  0,  0,  0,  1, -1,  0, -1,  1,  0,  1,  0,  0,  0,  0, -1,  1],
    [-2, 0,  0,  0,  0,  0,  0,  0,  0,  0,  1,  0,  0,  1,  0, -1,  0,  0],
    [0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  1,  1,  0, -1, -2],
    [0,  0,  0,  0,  0,  0, -1,  0,  0,  0,  1,  0,  0,  1,  0, -1,  0, -1],
    [0, -1, -1, -1,  0,  1,  0,  0,  0,  0,  0,  0,  1,  0,  0,  0, -1,  1],
    [0, -1,  0,  0,  0,  0,  1, -1,  0,  0,  0,  0,  0,  0,  1,  1, -1,  0],
  ]

  const attacker =  pokemons.filter(pokemon => {
    return pokemon.id == id;
  })[0];
    for(const defender of pokemons){
    let point = 0;
    for(const attackerType of attacker.types){
      for(const defenderType of defender.types){
        point += typeTable[type2index[attackerType]][type2index[defenderType]];
      }
      if (point >= 1) {
        advantageousPokemons.push(defender)
        break
      }
    }
  }

  return advantageousPokemons
}

タイプ相性表を定義する関係上コードが長くなっていますが、難しいことは行っていません。
まずリクエストパラメータのidから攻撃するポケモンを取得し、すべてのポケモンに対して攻撃ポケモンのタイプと防御ポケモンのタイプの相性を計算しています。攻撃ポケモンのタイプごとに相性ポイントが1以上になれば抜群以上の攻撃方法を持っていることになります。

相性ポイント

相性 相性point
こうかばつぐん +1
こうかいまひとつ -1
こうかなし -2

以下のクエリで図鑑No.6 リザードン(Charizard)が有利をとれるポケモンを取得してみましょう

query {
  advantageousPokemons(id: 6) {
    name,
    types,
  }
}

すると以下のようにリザードンが有利なポケモンを取得できます。

{
  "data": {
    "advantageousPokemons": [
      {
        "name": "Bulbasaur",
        "types": [
          "Grass",
          "Poison"
        ]
      },
      {
        "name": "Ivysaur",
        "types": [
          "Grass",
          "Poison"
        ]
      },
      {
        "name": "Venusaur",
        "types": [
          "Grass",
          "Poison"
        ]
      },
      {
        "name": "Caterpie",
        "types": [
          "Bug"
        ]
      },
      {
        "name": "Metapod",
        "types": [
          "Bug"
        ]
      }
      ....
}

Rest API との違いを考えてみる

ここでRestとの違いを考えてみます。
GraphQLではパラメータ、レスポンスの型を適宜したクエリでデータのやりとりをします。
しかし、同じようなことはRest APIでもできます。では、GraphQL特有なものは何でしょうか?🤔
GraphQLでは取得したいデータのみ取得し、不要なパラメータは取り除くことができます。今回実際にポケモンの体重、高さをレスポンスから取り除くことをしています。今回の例シンプルなのであまり恩恵は感じませんが、複雑なネスト構造のレスポンスに不要なデータが多く含まれる場合、それらから必要なデータを抽出するロジックは複雑になってしまいます。その作業をGraphQLが自動で行ってくれるのは大きなメリットだと感じました。
同じようなことをRestでやろうとすると、抽出するロジックをバックエンドで実装する必要があり、さらに、それぞれのレスポンス用に複数エンドポイントを作成する必要があります。

まとめ

実際に実装することでGraphQLの理解を深めることができました。
また、pokemonデータセットを用意してくださった、graphql-pokemonに感謝します🙇‍♂️
今回作成したコードはGitHubに置いておきます。

ポケモンデータを使ってGraphQLのクエリを作成するのは面白いのでみなさんもぜひやってみてください!

Discussion