こんにちは、”はふぃ”です。

仕事でGraphQLを使うことになったので、GraphQLとは何かをいろんなサイトを漁って調べたのでまとめました。

GraphQLとは

GraphQLはFacebookが開発しているWeb APIであり、クエリ言語スキーマ言語からなります。

クエリ言語

  • リクエストのための言語
  • 以下の3種類がある
    • query:データ取得
    • mutation:データ更新
    • subscription:サーバーサイドからのイベントの通知

スキーマ言語

  • 仕様を記述するための言語
  • クエリがリクエストされると、スキーマ言語で記述されたスキーマに従ってGraphQL処理系で処理されて、レスポンスを返す

とりあえず使ってみる

説明ばかりではよくわからないと思うので、GraphQLを試すことができる最低限の環境を構築し、そこでいろいろ試してみましょう!

→ コードはこちらで見れるので参考にしてください

環境構築

Node.js + Expressで最低限の環境を構築し、それらに対応したGraphQLのpackageであるapollo-server-expressを使ってGraphQLを試せる環境を作りたいと思います。

packageのインストール

$ npm install apollo-server-express

最低限のコード

< server.js >

const express = require('express');
const { ApolloServer, gql } = require('apollo-server-express');

// スキーマ
const typeDefs = gql`
  type Query {
    shops: [Shop]
  }

  type Shop {
    name: String!,
    main: String!
  }
`;

// ダミーデータ
const shops = [
  {
    name: 'mcdonalds',
    main: 'humberger'
  },
  {
    name: 'starbucks',
    main: 'coffee'
  }
]

// リゾルバー
const resolvers = {
  Query: {
    shops: () => shops,
  },
};

const server = new ApolloServer({ typeDefs, resolvers });

const app = express();
server.applyMiddleware({ app });

app.listen({ port: 3000 }, () =>
  console.log(`🚀 Server ready at http://localhost:3000${server.graphqlPath}`)
);

解説

コードを簡単に説明すると、スキーマ・ダミーデータ・リゾルバーを搭載したサーバーをapollo-server-expressのメソッドであるApolloServerを利用して構築しています。

スキーマ・ダミーデータ・リゾルバーについてはそれぞれ解説していきます。

スキーマ
// スキーマ
const typeDefs = gql`
  type Query {
    shops: [Shop]
  }

  type Shop {
    name: String!,
    main: String!
  }
`;
  • type Query:クエリ言語の時に説明したqueryのために必要な特殊な型名
  • type Shopnamemainというフィールドを持ったオブジェクト
  • 配列型は[Shop]のように[]で囲う
  • String!のように末尾に ! がついている場合は「nullにならないこと」を意味する
ダミーデータ
// ダミーデータ
const shops = [
  {
    name: 'mcdonalds',
    main: 'humberger'
  },
  {
    name: 'starbucks',
    main: 'coffee'
  }
]

※ GraphQL独自のものではなく、テスト用にデータベースの役割をしてデータを保持する存在が必要だったので作成したもの。

リゾルバー
// リゾルバー
const resolvers = {
  Query: {
    shops: () => shops,
  },
};
  • リクエストが来た時の処理を表す
  • 上記の処理は、「shopsを指定するクエリがきた場合にはshops(ダミーデータ)を返す」ことを示している

動作確認

$ npm start

上記のコマンドでサーバーを起動したら、http://localhost:3000/graphqlにアクセスします。

すると上記のような画面が表示されます。

左側にクエリを入力し、真ん中の「▶️ボタン」を押すと右側にレスポンスが表示されます。

実際に以下のようなクエリを入力してみましょう。

query {
  shops {
    name,
    main
  }
}

これは「namemain フィールドをもつshopsを取得する」というクエリなので、以下のようなレスポンスが返ってきます。

{
  "data": {
    "shops": [
      {
        "name": "mcdonalds",
        "main": "humberger"
      },
      {
        "name": "starbucks",
        "main": "coffee"
      }
    ]
  }
}

様々なクエリ

基本的なクエリについてはわかったので、ここからはクエリやコードを少しずつカスタマイズしてGraphQLでできることを増やしていきたいと思います。

引数

引数を指定することでその引数にあったレスポンスを取得することができます。

試しに「引数にnameを指定して、nameにマッチしたShopを返す」よう実装してみましょう。

まず、コードに引数ありのクエリ用のリゾルバーを追加します。

< server.js >

…
// スキーマ
const typeDefs = gql`
  type Query {
    shops: [Shop],
    shop(name: String!): Shop
  }

  type Shop {
    name: String!,
    main: String!
  }
`;

…

// リゾルバー
const resolvers = {
  Query: {
    shops: () => shops,
    shop: (obj, args, context, info) => {
      const { name } = args
      return shops.find(_shop => _shop.name === name)
    }
  }
};
…

クエリを以下のようにすると、指定した「starbucks」にマッチするShopだけがレスポンスで返ってきます。

query {
  shop(name: "starbucks") {
    name,
    main
  }
}

ちなみに画面左下のQUERY VARIABLESを利用すると以下のようにクエリを変更しても同じ結果が返ってきます。

# クエリ
query ($name: String!){
  shop(name: $name) {
    name,
    main
  }
}
# QUERY VARIABLES
{
  "name": "starbucks"
}

Mutation

データ更新系のクエリであるmutationを試したいと思います。

更新

nameにマッチしたShopmainを更新する」

< server.js >

…
// スキーマ
const typeDefs = gql`
  …
  type Mutation {
    update(name: String!, newMain: String!): [Shop]
  }
  …
`;

…

// リゾルバー
const resolvers = {
  …
  Mutation: {
    update: (obj, args, context, info) => {
      const { name, newMain } = args
      shops.forEach(_shop => {
        if (_shop.name === name) {
          // 値を更新
          _shop.main = newMain
        }
      })
      return shops
    }
  }
};

クエリとQUERY VARIABLESはそれぞれ以下のように設定し、name: starbucksmainfrappuccinoに更新します。

# クエリ
mutation ($name: String!, $newMain: String!){
  update(name: $name, newMain: $newMain) {
    name,
    main
  }
}
# QUERY VARIABLES
{
  "name": "starbucks",
  "newMain": "frappuccino"
}

新規作成

「新しい要素を追加する」

< server.js >

…
// スキーマ
const typeDefs = gql`
  …
  type Mutation {
    update(name: String!, newMain: String!): [Shop],
    create(name: String!, main: String!): [Shop]
  }
  …
`;

…

// リゾルバー
const resolvers = {
  …
  Mutation: {
    …
    create: (obj, args, context, info) => {
      const { name, main } = args
      shops.push({ name, main })
      return shops
    }
  }
};
…

name: misterdonutmain: donutShopを追加します。

# クエリ
mutation ($name: String!, $main: String!){
  create(name: $name, main: $main) {
    name,
    main
  }
}
# QUERY VARIABLES
{
  "name": "misterdonut",
  "main": "donut"
}

削除

nameにマッチしたShopを削除する」

< server.js >

// スキーマ
const typeDefs = gql`
  …
  type Mutation {
    update(name: String!, newMain: String!): [Shop],
    create(name: String!, main: String!): [Shop],
    delete(name: String!): [Shop]
  }
  …
;

…

// リゾルバー
const resolvers = {
  Mutation: {
    …
    delete: (obj, args, context, info) => {
      const { name } = args
      return shops.filter(_shop => _shop.name !== name)
    }
  }
};
# クエリ
mutation ($name: String!){
  delete(name: $name) {
    name,
    main
  }
}
# QUERY VARIABLES
{
  "name": "starbucks"
}

Union

Unionは指定された複数の型のうち、いずれかの型を表します。

このUnionを使って、

「引数qnameに含むShopまたはFoodを返す」

という検索クエリ:searchByNameを作ってみましょう。

< server.js >

// スキーマ
const typeDefs = gql`
  type Query {
    …
    searchByName(q: String!): [SearchResult]
  }

  …

  type Shop {
    name: String!,
    main: String!,
    foods: [Food]
  }

  type Food {
    name: String!,
    price: Int!
  }

  union SearchResult = Shop | Food
`;

…

// ダミーデータ
const shops = [
  {
    name: 'mcdonalds',
    main: 'humberger',
    foods: [
      {
        name: 'potato',
        price: 200
      },
      {
        name: 'chickenNugget',
        price: 300
      }
    ]
  },
  {
    name: 'starbucks',
    main: 'coffee',
    foods: [
      {
        name: 'sandwich',
        price: 500
      }
    ]
  }
]

…

// リゾルバー
const resolvers = {
  Query: {
    …
    searchByName: (obj, args, context, info) => {
      const { q } = args
      const results = []
      shops.forEach(_shop => {
        if (_shop.name.includes(q)) {
          results.push(_shop)
        }
        _shop.foods.forEach(_food => {
          if (_food.name.includes(q)) {
            results.push(_food)
          }
        })
      })
      return results
    }
  },

  …
  
  SearchResult: {
    __resolveType(obj, context, info){
      if (obj.main) {
        return 'Shop'
      }
      if (obj.price) {
        return 'Food'
      }
      return null
    }
  }
};

いきなりたくさんのコードを追加してしまったのでそれぞれ解説します。

  • スキーマ
    • QueryにsearchByNameを追加
    • 新しくFoodというTypeを追加
    • Shopは複数のFoodをFieldにもつ
    • SearchResultUnionで定義(Shop or Food
  • ダミーデータ
    • FoodのデータをそれぞれのShopのデータに追加
  • リゾルバー
    • searchByNameは、まずshopsの中身を確認し、引数qを含むnameをもつ_shopを結果の配列に追加。加えてその_shopfoodsも確認し、引数qを含むnameをもつ_foodも結果の配列に加えるよう実装
    • SearchResult__resolveTypeを定義(これが必要)

続いてクエリですが、... on Typeで表すconditional fragmentを使って設定してみます。

例えば、… on ShopはレスポンスがShopならばnamemainを返すという意味になります。

# クエリ
query ($q: String!){
  searchByName(q: $q) {
    ... on Shop {
      name
      main
    }
    ... on Food {
      name
      price
    }
  }
}

QUERY VARIABLESはヒット件数を多くするために以下のようにします。

# QUERY VARIABLES
{
  "q": "a"
}

すると結果はShopFoodが混ざったレスポンスになることがわかります。

このようにUnionを使うことで、型が異なるオブジェクトを一緒に検索することができました。

RESTとの違い

GraphQLを調べるとREST APIとの違いについて説明されたサイトが数多くヒットします。

REST APIに関しては現在主流となっているのでご存知の方も多いと思いますが、比較した時のGraphQLのメリット・デメリットをざっとまとめました。

GraphQLのメリット

  • エンドポイントが1つ
    • 処理ごとにエンドポイントを増やす必要がないので、管理がしやすい
  • 必要なフィールドだけを容易に取得可能
    • REST APIの場合は不要なフィールドを指定するパラメータかエンドポイントを追加して対応する必要がある
  • バージョン管理で情報がわかりやすい
    • クエリでリクエストしたフィールドを明示するため、「誰が」「どの程度」といったところまでフィールド単位で情報を把握することが可能
  • クエリの学習コストが小さい

GraphQLのデメリット

  • パフォーマンスの分析がやりにくい
    • エンドポイントが1つなので、エンドポイントごとに記録や分析といったことができない
  • 画像などのバイナリデータの扱いが苦手
    • GraphQLは多くの場合でJSONを利用するため、バイナリデータのシリアライズが苦手でデータ量が大きくなってしまう

参考にしたサイト

投稿者: はふぃ

若手のWEBフロントエンジニア。最近はバレーボールにハマってます!

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

CAPTCHA