2020/9 GraphQLを使ってみた - 実践編

北川 嵩大
大阪
GraphQL記事のトップ画像

こんにちは、大阪事業所の北川です。

前回はGraphQLとはどういうものなのか、ということで概要と特徴についてご紹介させていただきました。 そして今回は実践編として、実際にGraphQLを使用してデータベースから値を取得、返却するAPIを作成していきましょう。

前回の記事はこちら

事前準備

本件は環境構築にDockerを利用いたします。 そのため、事前にDockerのインストールを行ってください。 また、複数のコンテナを稼働させますので、Docker Composeを利用いたします。 ただし、Dockerのインストール方法や使用方法などは省かせていただきますので、必要な方は下記の記事をご参照ください。

構成

今回構築する環境の最終構成は下記の図の通りとなります。 構成図 Node.jsのフレームワークであるExpressでサーバーを構成し、そこでApolloというライブラリを使用してGraphQLを扱えるよう調整します。 ここからPostgreSQLに接続し、データベース内のデータを取得して返却を行えるようにいたします。 Adminerは、PostgreSQLへのデータ登録を行うための管理ツールとなります。

データベースの準備

Dockerにてデータベースを稼働させます。 データベースについてはPostgreSQLを使用し、データベースを操作するツールとしてadminerというものを利用いたします。 どちらも、DockerHubにて提供されているものがありますので、そのまま利用いたします。

Dockerを準備

PostgreSQL, Adminerを動作させるためのDockerを作成いたします。

1. PostgreSQLのDockerを追加

  1. ディレクトリを作成
mkdir -p graphql_sample/postgresql/build
mkdir -p graphql_sample/postgresql/env
  1. Dockerfileを作成
# postgresql/build/Dockerfile
FROM postgres:12
  1. envを作成
# postgresql/env/.env
POSTGRES_PASSWORD=password
PGDATA=/var/lib/postgresql/data

2. adminerのDockerを追加

  1. ディレクトリを作成
mkdir -p graphql_sample/adminer/build
mkdir -p graphql_sample/adminer/env
  1. Dockerfileを作成
# adminer/build/Dockerfile
FROM adminer:4.7.6
  1. envを作成
# adminer/env/.env
POSTGRES_PASSWORD=password

3.Docker Composeを作成

  1. docker-compose.ymlを作成
    ※それぞれのDockerfileにはDockerhubにて公開されているイメージを定義しているだけとなります。そのため、Dockerfileは作成せずにdocker-compose.ymlに直接イメージの定義を行うだけでも問題ありません。
# graphql_sample/docker-compose.yml
version: '3.7'
services:
  # PostgreSQL
  graphqlsample_postgresql:
    build: 
      context: ./postgresql/
      dockerfile: build/Dockerfile
    image: graphqlsample_postgresql
    container_name: graphql_sample_postgresql
    env_file:
      - ./postgresql/env/.env
    volumes:
      - ./postgresql/postgresql_data:/var/lib/postgresql/data
  # adminer
  graphqlsample_adminer:
    build: 
      context: ./adminer/
      dockerfile: build/Dockerfile
    image: graphqlsample_adminer
    container_name: graphqlsample_adminer
    env_file:
      - ./adminer/env/.env
    ports:
      - "8001:8080" # Adminerポート

構成確認

現段階でのディレクトリ構成はこのようになります。

エクスプローラ1

動作確認

PostgreSQLとadminerが、それぞれ問題なく動作しているか確認していきます。

1. 起動/サーバーにログイン

  1. docker-compose.ymlと同階層に移動する
  2. docker-compose コマンドでDockerを起動する
docker-compose up --build -d
docker-compose exec graphqlsample_graphql bash

2. adminerにログイン

  1. localhost:8001にアクセスしてadminerのログイン画面を表示
  2. 表示されたログイン画面でDBのログイン情報を入力し、PostgreSQLにアクセス
データベース種類PostgreSQL
サーバーgraphqlsample_postgres
※docker-composeにて定義したサービス名をホスト名として利用可能
ユーザ名postgres
※PostgreSQLのデフォルトのユーザー名
パスワードpassword
※postgresql/env/.env にて定義したパスワードを利用
データベースpostgres
※PostgreSQLのデフォルトのデータベース
adminerのログイン

3. サンプルデータを作成

  1. テーブルを作成
  1. テーブル定義を入力して保存
テーブル名Books
列名 長さ
id integer (未指定)
title character varying 100
author character varying 100
price integer (未指定)
  1. 作成したテーブルにデータを追加
  1. 2件のデータをそれぞれ入力して保存
1件目
id1
titletitle1
authorauthor1
price100
2件目
id2
titletitle2
authorauthor2
price200

Expressの起動

GraphQLのサーバーライブラリはApolloを使用いたします。 他にはGraphQLの開発元であるFacebook社が用意したRelayがありますが、Web上での情報量が多いApolloといたしました。 このApolloをNode.jsのExpressで動作させるため、まずはDockerとExpressの起動までを行います。

Dockerを準備

1. GraphQL用のDockerを追加

  1. ディレクトリを作成
mkdir -p graphql_sample/graphql/build
mkdir -p graphql_sample/graphql/env
  1. Dockerfileを作成
# graphql/build/Dockerfile
FROM amazonlinux:2

# yumのアップデート+キャッシュ削除
RUN yum -y update && yum clean all

RUN yum -y groupinstall "Development Tools"
RUN useradd express -md /home/express && \
    echo 'express:express' | chpasswd && \
    echo 'express ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers

RUN yum -y install \ 
    sudo \
    kernel-devel \
    kernel-headers \
    libyaml-devel \
    libffi-devel \
    tk-devel \
    zip \
    wget \
    tar \
    zlib \
    zlib-devel \
    bzip2 \
    bzip2-devel \
    readline \
    readline-devel \
    openssl \
    openssl-devel \
    gdbm-devel \
    procps

# nodejs install
RUN curl -sL https://rpm.nodesource.com/setup_12.x | bash -
RUN yum -y install nodejs

# change user
USER express
ENV GOPATH="/home/express" \
    PATH="$PATH:/home/express/bin" \
    HOME="/home/express"

WORKDIR /home/express/app
  1. 空のenvを作成
# graphql/env/.env

2. Docker Composeを作成

  1. docker-compose.ymlの最下部に追記
# graphql_sample/docker-compose.yml
  # GraphQL
  graphqlsample_graphql:
    build: 
      context: ./graphql/
      dockerfile: build/Dockerfile
    image: graphqlsample_graphql
    container_name: graphqlsample_graphql
    env_file:
      - ./graphql/env/.env
    ports:
      - "3000:3000"
    volumes:
      - "./graphql/app:/home/express/app"
    tty: true

Dockerfile追加後の構成確認

現段階でのディレクトリ構成はこのようになります。

Expressディレクトリ構成

Express導入

GraphQLサーバーはNode.jsのフレームワークであるExpress上で稼働させますので、Expressを導入します。

1. Docker起動/サーバーにログイン

  1. docker-compose.ymlと同階層に移動する
  2. docker-compose コマンドでDockerを起動する
docker-compose up --build -d
docker-compose exec graphqlsample_graphql bash

2. Express Generatorをインストール

  1. スケルトンを作成するexpress-generatorをグローバルインストール
sudo npm install -g express-generator

3. スケルトンを作成

  1. スケルトンを作成(今回viewはejsを選択)
express --view ejs
  1. スケルトンと同時に作成されたpackage.json内のライブラリをインストール
npm install

Express導入後の構成確認

Express導入後のディレクトリ構成

動作確認

1. Expressを確認

  1. Expressを起動
npm run start
  1. 任意のブラウザで localhost:3000 へアクセスする
3. npm run start は Ctrl + c で終了することが可能

Apollo GraphQLの起動

それでは、導入したExpressにApolloを組み込んでいきましょう。

インストール

ExpressにApollo GraphQLを組み込む

  1. apollo-server-expressをインストール
npm install --save apollo-server-express

コード修正

app.jsでApolloを使用するようコードを修正
参考:https://www.npmjs.com/package/apollo-server-express

var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
// 追記
var { ApolloServer, gql } = require('apollo-server-express');

var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');

var app = express();

// 追記 ここから---
// スキーマ
const typeDefs = gql`
  # クエリ
  type Query {
    hello: String
  }
`
// リゾルバ(スキーマ内のクエリと返却値の紐付け)
const resolvers = {
  Query: {
    hello: () => 'Hello world!'
  }
};
const server = new ApolloServer({ typeDefs, resolvers });
server.applyMiddleware({ app });
// 追記 ここまで---

動作確認

  1. 再度Expressを起動し直す
npm run start
  1. 任意のブラウザで localhost:3000/graphql へアクセスする
  1. 左側のテキストエリアにクエリを入力、真ん中の右向き三角をクリックすると右側にクエリ結果が表示される。
    hello クエリには "Hello world!" の固定文字列を返すようにしているので下記の結果となります。

スキーマとリゾルバの外部ファイル化

実際に開発していくことになると、クエリやリゾルバは肥大化していきます。 そのため、どちらも app.js に記載してしまうと app.js が肥大してしまい、コードの見通しが悪くなってしまいます。 そこで、 app.js 内に記載したスキーマとリゾルバを外部ファイル化してみましょう。

インストール

  1. graphql-tools をインストール
    GraphQLが提供しているツールをインストールします。 このツールに、ファイルに書き出したGraphQL文字列を読み込んだり、リゾルバを設定するためのモジュールが提供されています。
npm install --save graphql-tools

コード修正(スキーマ)

1. スキーマを別ファイルに書き出し

  1. appディレクトリ直下に schema.graphql を作成する
  2. 作成したファイルに typeDefs 変数に格納した文字列を記載する
# schema.graphql
# クエリ
type Query {
  hello: String
}

2. 書き出したスキーマを読み込み

  1. app.js 内で使用するモジュールを追加します loadSchemaSync, GraphQLFileLoader: スキーマを読み込む際に使用します
# app.js
var { ApolloServer, gql } = require('apollo-server-express');
// 追記
var { loadSchemaSync, GraphQLFileLoader, addResolversToSchema } = require('graphql-tools');
  1. app.js の内容を変更
var app = express();

// 変更
// const typeDefs = gql`
//   # クエリ
//   type Query {
//     hello: String
//   }
// `
const typeDefs = loadSchemaSync(path.resolve(__dirname, './schema.graphql'), { loaders: [new GraphQLFileLoader()] });
// 変更なし
const resolvers = {
  Query: {
    hello: () => 'Hello world!'
  }
};

// 追記
const schema = addResolversToSchema({
  schema: typeDefs,
  resolvers: resolvers
});

// 変更
// const server = new ApolloServer({ typeDefs, resolvers });
const server = new ApolloServer({ schema });
server.applyMiddleware({ app });

動作確認(スキーマ)

  1. npm run startで再度Expressを起動
  2. 任意のブラウザで localhost:3000/graphql へアクセス
  3. クエリが正常に動作することを確認する

コード修正(リゾルバ)

1. リゾルバを別ファイルに書き出し

  1. appディレクトリ直下に resolvers.js を作成する
  2. 作成したファイルに resolvers に格納した内容を返却するモジュールを作成する
// resolvers.js
module.exports = {
  Query: {
    hello: () => 'Hello world!'
  }
};

2. 書き出したリゾルバを読み込み

  1. app.js の内容を変更
// const resolvers = {
//   Query: {
//     hello: () => 'Hello world!'
//   }
// };
const resolvers = require('./resolvers')

動作確認(リゾルバ)

  1. npm run startで再度Expressを起動
  2. 任意のブラウザで localhost:3000/graphql へアクセス
  3. クエリが正常に動作することを確認する

リファクタリング

typeDefs と schema の変数名についての境界が曖昧になっているので、正しい変数名に直しましょう。
変数名についてはGraphQL ApolloやGraphQL Toolの公式ドキュメントに沿った形で修正しています。

コード修正

var app = express();

// typeDefs を schema とする
// const typeDefs = loadSchemaSync(path.resolve(__dirname, './schema.graphql'), { loaders: [new GraphQLFileLoader()] });
const schema = loadSchemaSync(path.resolve(__dirname, './schema.graphql'), { loaders: [new GraphQLFileLoader()] });
const resolvers = require('./resolvers')

// schema を schemaWithResolvers とする
// const schema = addResolversToSchema({
const schemaWithResolvers = addResolversToSchema({
  // schema: typeDefs,
  schema: schema,
  resolvers: resolvers
});

// const server = new ApolloServer({ schema });
const server = new ApolloServer({ schema: schemaWithResolvers });
server.applyMiddleware({ app });
### 各変数名について 各変数名について、このように命名された理由を考察してみます - loadSchemaSync の格納変数名を typeDefs から schema といたしました
スキーマを外部ファイル化する前は、クエリ(型など)の定義という意味で C言語に沿った形(typedef)で typeDefs としたのでしょうか
ただ、loadSchemaSyncの返却値はGraphQLSchemaというクラスとなり、より一般的な単語の schema としたのかと考えます - addResolversToSchema の格納変数を schema から schemaWithResolvers といたしました
addResolversToSchema の返却結果も loadSchemaSync と同様の GraphQLSchema クラスですが、addResolversToSchemaによって内部に resolvers を含んでいますのでWithをつけた形としたのかと考えます

PostgreSQLと連携

最後に、ApolloとPostgreSQLを連携させてみましょう。

インストール

Node.jsでのDB接続用のパッケージを組み込む
sequelize をインストール

npm install --save pg sequelize

コード修正

1. GraphQLへRDB接続用の環境変数を追加

# graphql/env/.env
# ホストに接続
RDB_DB_ENDPOINT=graphqlsample_postgresql
RDB_DB_PORT=5432
RDB_DB_DATABASE=postgres
RDB_DB_USER=postgres
RDB_DB_PASS=password

2. Expressのソース改修

  1. ディレクトリを作成
mkdir -p graphql_sample/graphql/app/db
mkdir -p graphql_sample/graphql/app/models
mkdir -p graphql_sample/graphql/app/queries/book
  1. DB接続用のクラスを作成
# app/db/sequelize.js
const Sequelize = require('sequelize');

const sequelize = new Sequelize(
    process.env.RDB_DB_DATABASE,
    process.env.RDB_DB_USER,
    process.env.RDB_DB_PASS,
    {
        // 接続先ホストを指定
        host: process.env.RDB_DB_ENDPOINT,
        port: process.env.RDB_DB_PORT,

        // logging: console.log,
        maxConcurrentQueries: 100,

        // 使用する DB 製品を指定
        dialect: 'postgres',

        pool: {
            max: 5,
            min: 0,
            acquire: 30000,
            idle: 10000,
            acquire: 20000
        }
    }
);

module.exports = sequelize;
  1. モデルを追加
// graphql_sample/graphql/app/models/Book.js
const Sequelize = require('sequelize');
const sequelize = require('../db/sequelize');

/**
 * Books テーブルの Entity モデル
 */
const Book = sequelize.define('Books', {
    id: {
        type: Sequelize.INTEGER,
        primaryKey: true,
        autoIncrement: true
    },
    title: {
        type: Sequelize.STRING
    },
    author: {
        type: Sequelize.STRING
    },
    price: {
        type: Sequelize.INTEGER
    }
}, {
    // タイムスタンプの属性 (updatedAt, createdAt) が不要ならば次のプロパティは false
    timestamps: false,

    // テーブル名を変更したくない場合は次のプロパティを true
    // デフォルトでは sequelize はテーブル名を複数形に変更する
    freezeTableName: true
});

module.exports = Book;
  1. クエリを作成
// graphql_sample/graphql/app/queries/book/findBookById.js
const Book = require('../../models/Book')

const findBookById = async (obj, args, context, info) => {
    const id = args.id
    const book = await Book.findByPk(id, { raw: true });

    if (!book) {
        return null
    }
    return book
}

module.exports = findBookById
  1. スキーマを更新
    追加したBookとfindBookByIdの定義を行います。
# schema.graphql
# 追加 Bookの定義
type Book { 
    id: ID, 
    title: String, 
    author: String, 
    price: Int 
}

type Query {
  hello: String,
  # 追加 単一のBookを取得するクエリ定義
  findBookById(id: ID): Book
}
  1. リゾルバを更新
    スキーマ定義と追加したクエリ findBookById の関連付けを行います。
// resolvers.js
const findBookById = require('./queries/book/findBookById')

module.exports = {
  Query: {
    hello: () => 'Hello world!',
    findBookById: findBookById,
  }
};

動作確認

  1. 環境変数を追加したので、一度Dockerの再起動を行う
docker-compose up --build -d
  1. npm run startで再度Expressを起動
  2. 任意のブラウザで localhost:3000/graphql へアクセス
  3. クエリが正常に動作することを確認する
GraphQLの開発画面でのPostgreSQLとの連携確認
  1. 存在しないデータを検索
GraphQLの開発画面で存在しないデータを検索
  1. AdminerでPostgreSQLにデータを追加して上記と同様のデータを検索
GraphQLの開発画面で存在しなかったデータを追加 GraphQLの開発画面で存在しなかったデータを検索

まとめ

今回はGraphQLの実践編として、Docker上でのApollo GraphQLのインストールから動作確認、加えて若干のソース修正を加えてスキーマとリゾルバの外部ファイル化までを行いました。

下準備までは少し時間がかかりますが、ここまで作成してしまえばあとはクエリを記載してモデルにてDBからのデータ取得を追加、リゾルバにてクエリとモデルの関連付けを行うだけでAPIを拡張することができます。
また、モデルについてはクエリに沿った結果さえ返すことが出来ればどのような処理でも記載できます。 そのため、RDBだけではなくその他のデータストレージとの連携も可能となります。

実際に案件にて使用した感想としましては

GraphQL(言語)というよりもApollo(ライブラリ)寄りの感想ですが...。
ログの出力やエラー処理などの共通処理については、実運用で使用する場合は事前にしっかりと調整することが必要になりますが、ログの出力タイミングや出力内容についてもApollo側がフォローしてくれる形となりますので、従来のAPIに比べると比較的楽に調整することが出来ました。

機会があれば、皆さんも一度GraphQLを用いたアプリケーションの作成を行ってみてはいかがでしょうか。

※今回はGraphQLの実装を体験するということに主軸を置いておりますので、ログの出力などについては一切考慮しておりません。
実際のアプリケーションを作成する際はリクエストとレスポンスのログ情報が重要になるかと思いますので、別途ご検討ください。

参考

ブログ内リンク

外部リンク

こんな記事も読まれています