TECHNICAL BLOG

2020/9/23 # やってみた # Node.js # GraphQL # 入門 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
  2. Dockerfileを作成
    # postgresql/build/Dockerfile
    FROM postgres:12
  3. 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
  2. Dockerfileを作成
    # adminer/build/Dockerfile
    FROM adminer:4.7.6
  3. 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にログイン

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

    adminerのログイン

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

  1. テーブルを作成

  2. テーブル定義を入力して保存

    テーブル名 Books
    列名 型 長さ
    id integer (未指定)
    title character varying 100
    author character varying 100
    price integer (未指定)

  3. 作成したテーブルにデータを追加

  4. 2件のデータをそれぞれ入力して保存

    1件目
    id 1
    title title1
    author author1
    price 100

2件目
id 2
title title2
author author2
price 200

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
  2. 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

3. 空の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をインストール

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

    3. スケルトンを作成

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

    Express導入後の構成確認

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

動作確認

1. Expressを確認

  1. Expressを起動
    npm run start
  2. 任意のブラウザで 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

2. 任意のブラウザで localhost:3000/graphql へアクセスする
  

3. 左側のテキストエリアにクエリを入力、真ん中の右向き三角をクリックすると右側にクエリ結果が表示される。
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');


2. 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

2. 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;

3. モデルを追加

// 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;

4. クエリを作成

// 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

5. スキーマを更新
追加したBookとfindBookByIdの定義を行います。

schema.graphql

追加 Bookの定義

type Book {
id: ID,
title: String,
author: String,
price: Int
}

type Query {
hello: String,

追加 単一のBookを取得するクエリ定義

findBookById(id: ID): Book
}

6. リゾルバを更新
スキーマ定義と追加したクエリ findBookById の関連付けを行います。

// resolvers.js
const findBookById = require('./queries/book/findBookById')

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

### 動作確認
1. 環境変数を追加したので、一度Dockerの再起動を行う

docker-compose up --build -d


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

5. 存在しないデータを検索
GraphQLの開発画面で存在しないデータを検索

6. AdminerでPostgreSQLにデータを追加して上記と同様のデータを検索
GraphQLの開発画面で存在しなかったデータを追加
GraphQLの開発画面で存在しなかったデータを検索

---

## まとめ

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

下準備までは少し時間がかかりますが、ここまで作成してしまえばあとはクエリを記載してモデルにてDBからのデータ取得を追加、リゾルバにてクエリとモデルの関連付けを行うだけでAPIを拡張することができます。
また、モデルについてはクエリに沿った結果さえ返すことが出来ればどのような処理でも記載できます。 そのため、RDBだけではなくその他のデータストレージとの連携も可能となります。 実際に案件にて使用した感想としましては - 入力値のチェックについては、Apolloがスキーマ通りに行ってくれるので基本的には考慮する必要がない - ログ出力やエラー処理も全てApollo側にて一括で調整可能(別途調整が必要) - 開発者はデータの取得から返却までの処理だけを考慮して開発するのみ という感じでしょうか。 GraphQLのスキーマさえ決めてしまえば概要編で書かせていただいた通りで、考慮する点が最小で済むために追加や改修が非常に容易に行うことができました。
GraphQL(言語)というよりもApollo(ライブラリ)寄りの感想ですが...。
ログの出力やエラー処理などの共通処理については、実運用で使用する場合は事前にしっかりと調整することが必要になりますが、ログの出力タイミングや出力内容についてもApollo側がフォローしてくれる形となりますので、従来のAPIに比べると比較的楽に調整することが出来ました。 機会があれば、皆さんも一度GraphQLを用いたアプリケーションの作成を行ってみてはいかがでしょうか。
※今回はGraphQLの実装を体験するということに主軸を置いておりますので、ログの出力などについては一切考慮しておりません。
実際のアプリケーションを作成する際はリクエストとレスポンスのログ情報が重要になるかと思いますので、別途ご検討ください。
## 参考 ### ブログ内リンク - 2018/8 Docker入門:https://www.cview.co.jp/cvcblog/2018.08.21.170409.html - 2018/9 DockerでRails環境構築:https://www.cview.co.jp/cvcblog/2018.09.25.103133.html ### 外部リンク - PostgreSQL: https://hub.docker.com/_/postgres - adminer: https://hub.docker.com/_/adminer - Apollo: https://www.apollographql.com/ - Relay: https://relay.dev/ - npm apollo-server-express: https://www.npmjs.com/package/apollo-server-express