Ishida-IT LLC

< Back

DockerでNode.jsアプリケーションを開発する (3) MySQL用コンテナを追加

Posted: 2019-11-22

1. docker-compose.yml ファイルを編集

docker-compose.ymlファイルにMySQLコンテナに関する記述を追加します。

今回のサンプルではMySQL5.7の公式イメージを使うことにします。

docker-compose.yml

version: '3'
services:
  mysql:
    image: mysql:5.7
    env_file: ./mysql/mysql.env
    environment:
      - TZ=Asia/Tokyo
    ports:
      - '3306:3306'
    volumes:
      - ./mysql/conf:/etc/mysql/conf.d/:ro
      - mysqldata:/var/lib/mysql
    networks:
      - backend

  app:
    image: node:12
    env_file: ./app.env
    environment:
      - TZ=Asia/Tokyo
      - DEBUG=app:*
    tty: true
    ports:
      - '3000:3000'
    volumes:
      - ./src:/app
    working_dir: /app
    command: npm run dev
    networks:
      - backend
    depends_on:
      - mysql

networks:
  backend:

volumes:
  mysqldata:

「mysql」と「app」の2つのサービスを定義しています。

mysqlサービスの設定では、「volumes:」でホスト側のmysql/confフォルダをコンテナの/etc/mysql/conf.d/ディレクトリにマウントしてMySQLのデフォルト設定を上書きしています。

また、mysql/dataフォルダをコンテナの/var/lib/mysql/ディレクトリにマウントすることで、コンテナが削除されてもデータが消えずにホスト側で保持されるようにしています。

また、「mysqldata」というボリュームをコンテナの/var/lib/mysql/ディレクトリにマウントすることで、コンテナが削除されてもデータが消えずに保持されるようにしています。(ホスト側のフォルダをそのままmysqlのデータ保存先としてマウントするとWindows環境で上手く動かなかったので、Dockerのボリュームをマウントするように修正しました。)

「mysql」「app」の両コンテナとも「environment:」で「TZ=Asia/Tokyo」としてタイムゾーンを日本時間に合わせています。もしUTCで良い場合はこの設定は不要です。

またその他の環境変数については「env_file:」の指定で別ファイルに定義したものを取り込むようにしています。もちろんdocker-compose.yml内に全てを書いても構わないのですが、一般的にはデータベースの接続情報などをソースコントロールに含めない方が良いと思いますのでこのサンプルでも.gitignoreに「*.env」を追加して含めないようにしました。



mysql/conf/フォルダにmy.cnfファイルを作成します。

mysql/conf/my.cnf

#
# The MySQL database server configuration file.
#

[client]
default-character-set=utf8mb4

[mysql]
default-character-set=utf8mb4

[mysqldump]
default-character-set=utf8mb4

[mysqld]
character-set-server=utf8mb4
collation-server=utf8mb4_bin
lower_case_table_names=1

# Enable access from the host machine.
bind-address=0.0.0.0

この例では日本語などのマルチバイト文字列とEmojiが使えるように設定しています。



mysqlフォルダにmysql.envファイルを作成します。

mysql/mysql.env

MYSQL_ROOT_HOST=%
MYSQL_ROOT_PASSWORD=(ルートパスワード)
MYSQL_USER=(ユーザー名)
MYSQL_PASSWORD=(パスワード)
MYSQL_DATABASE=todo

これはMySQLのコンテナに与える環境変数の設定になります。データベース名は「todo」としておきました。



ルートフォルダにapp.envファイルを作成します。

app.env

MYSQL_SERVER=mysql
MYSQL_USER=(ユーザー名)
MYSQL_PASSWORD=(パスワード)
MYSQL_DATABASE=todo

これはNode.jsのアプリケーションが動くコンテナに与える環境変数の設定になります。アプリケーション内でこの環境変数の値を見てデータベースに接続しに行くようにします。



2. MySQL コンテナを動かす

> docker-compose up -d

上のコマンドでmysqlとappの2つのコンテナが起動するはずです。

docker-compose ps で確認しておきましょう。

> docker-compose ps

Docker Compose ps

もし起動しない場合は、docker-compose.ymlの内容を再度確認してください。



3. MySQLにログイン

「docker-compose exec」を使ってMySQL サーバーにログインします。

> docker-compose exec mysql mysql -uroot -p

コンテナがすでに動いているので、この場合は「run」ではなく「exec」を使って動いているコンテナ内でコマンドを実行します。

「mysql」が2回続いていますが、1回目はdocker-composeが認識するサービス名、2回目はコンテナ内で実行するコマンド名としての「mysql」です。

ログイン時のパスワードは、mysql/mysql.envで指定したパスワードを入力します。



4. MySQLの文字コード設定を確認

MySQLにログイン出来たら、念のため文字コードの設定を確認しておきましょう。

mysql> show variables like 'char%';

MySQL Variables

「mysql/conf/my.cnf」ファイルで指定した通りの設定が反映されていればOKです。



5. データベースを確認

データベースの状態も確認しておきます。

mysql> show databases;

Show Databases

環境変数で指定した「todo」というデータベースが存在しているのが分かります。

mysql> use todo;
mysql> show tables;

Show Tables

todoデータベースの中身は現時点では何もありません。

テーブルの作成と開発用初期データの挿入はもちろん手作業でも構わないのですが、SequelizeというライブラリのDBマイグレーション機能を利用して行うと便利です。



6. Express.jsアプリケーションにSequelizeを導入

まずはアプリケーション用コンテナ内でSequelizeとその依存パッケージをインストールします。

> docker-compose run --rm app npm i mysql2 sequelize sequelize-cli

次に、sequelize-cliを使ってSequelizeの初期化を行います。

> docker-compose run --rm app npx sequelize-cli init

これによって、srcフォルダ内に

  • config
  • migrations
  • models
  • seeders

の4つのフォルダが作成されます。


次に、「sequelize-cli model:generate」を使って最初のモデルクラスを生成します。

> docker-compose run --rm app npx sequelize-cli model:generate --name Task --attributes task:string,done:boolean

上の例では「task」と「done」という2つのプロパティを持つ「Task」というモデルクラスを生成しています。

modelsフォルダに「task.js」ファイルが出来ているのが分かります。

models/task.js

'use strict';
module.exports = (sequelize, DataTypes) => {
  const Task = sequelize.define(
    'Task',
    {
      task: DataTypes.STRING,
      done: DataTypes.BOOLEAN,
    },
    {}
  );
  Task.associate = function(models) {
    // associations can be defined here
  };
  return Task;
};

また同時にmigrationsフォルダに「(日時)-create-task.js」というファイルも出来ています。

migrations/[yyyyMMddHHmmss]-create-task.js

'use strict';
module.exports = {
  up: (queryInterface, Sequelize) => {
    return queryInterface.createTable('Tasks', {
      id: {
        allowNull: false,
        autoIncrement: true,
        primaryKey: true,
        type: Sequelize.INTEGER,
      },
      task: {
        type: Sequelize.STRING,
      },
      done: {
        type: Sequelize.BOOLEAN,
      },
      createdAt: {
        allowNull: false,
        type: Sequelize.DATE,
      },
      updatedAt: {
        allowNull: false,
        type: Sequelize.DATE,
      },
    });
  },
  down: (queryInterface, Sequelize) => {
    return queryInterface.dropTable('Tasks');
  },
};

この時点でのフォルダ構成は下の画像のようになります。

Model Generated



7. sequelize-cliでDBマイグレーションを実行

さて、次はDBマイグレーションを実行してtodoデータベースにテーブルを作成したいところですが、その前にDB接続情報を正しくセットアップする必要があります。

自動で生成された状態だとconfigフォルダに「config.json」というファイルが出来ていると思いますが、それを「config.js」にリネームして、内容を下のように書き換えます。

これは、docker-composeで設定した環境変数の値をそのままDB接続情報として使うためです。

module.exports = {
  development: {
    username: process.env.MYSQL_USER,
    password: process.env.MYSQL_PASSWORD,
    database: process.env.MYSQL_DATABASE,
    host: process.env.MYSQL_SERVER,
    dialect: 'mysql',
  },
  test: {
    username: process.env.MYSQL_USER,
    password: process.env.MYSQL_PASSWORD,
    database: process.env.MYSQL_DATABASE,
    host: process.env.MYSQL_SERVER,
    dialect: 'mysql',
  },
  production: {
    username: process.env.MYSQL_USER,
    password: process.env.MYSQL_PASSWORD,
    database: process.env.MYSQL_DATABASE,
    host: process.env.MYSQL_SERVER,
    dialect: 'mysql',
  },
};

また、modelsフォルダのindex.jsファイルを開き、’config.json’となっている部分を’config.js’に変更しておきます。

Models index.js

この状態で、下のコマンドを実行すると、SequelizeによるDBマイグレーションが実行されます。

今回のサンプルの場合は、何も無かったtodoデータベースにtasksテーブルが作成されます。

> docker-compose run --rm app npx sequelize-cli db:migrate

DB Migrate



8. sequelize-cliで初期データを挿入

アプリケーションの開発中やテスト中はデータベースの内容を初期化したりデータを任意の状態に戻したりする場合がありますが、そのようなときにもSequelizeのシード機能を使うと便利です。

seedersフォルダ内に「(日付)-tasks.js」という名前のファイルを作って下の内容で保存します。

'use strict';

const db = require('../models/');

module.exports = {
  up: (queryInterface, Sequelize) => {
    return queryInterface.bulkInsert(
      'tasks',
      [
        {
          task: 'Write the blog article',
          done: false,
          createdAt: new Date(),
          updatedAt: new Date(),
        },
        {
          task: 'Purchase new laptop PC',
          done: false,
          createdAt: new Date(),
          updatedAt: new Date(),
        },
        {
          task: 'Go to swim',
          done: false,
          createdAt: new Date(),
          updatedAt: new Date(),
        },
        {
          task: 'Order a pizza',
          done: true,
          createdAt: new Date(),
          updatedAt: new Date(),
        },
      ],
      {}
    );
  },

  down: (queryInterface, Sequelize) => {
    return queryInterface.bulkDelete('Users', null, {});
  },
};

次のコマンドを実行すると、tasksテーブルに4件のレコードが挿入されます。

> docker-compose run --rm app npx sequelize-cli db:seed:all

Select Tasks



9. Express.jsアプリケーションでデータの一覧を表示

データベースに初期データが投入出来たら、次にアプリケーションのトップ画面にタスク一覧を表示しましょう。

const db = require('../models');

とすることで、db.TaskでTaskモデルを参照することが出来るようになっています。

あとは、

const tasks = await db.Task.findAll();

でTaskの一覧を取得することが出来ます。

そのTask一覧をViewに渡して、router.get(‘/‘)ハンドラのやることは終わりです。

routes/index.js

const express = require('express');
const router = express.Router();
const db = require('../models');

router.get('/', async function(req, res) {
  const tasks = await db.Task.findAll();
  res.render('index', { title: 'Docker-Node.js', tasks });
});


views/index.jadeファイルでは、渡されてきたtasksに対して

each t in tasks

という書式でループしてリストを表示しています。

views/index.jade

extends layout

block content
  h1= title
  h2 Tasks
  ul.tasks
    each t in tasks
      li
        form(action="update", method="post")
          input(type='hidden' name='id' value=t.id)
          input(
            type='checkbox'
            name='done'
            checked=t.done
            onclick="this.closest('form').submit();"
          )

        span.task= t.task

        form(action="delete", method="post")
          input(type='hidden' name='id' value=t.id)
          button.delete X

  div.newtask
    form(action="create", method="post")
      input(type='text' name='task')
      button(type='submit') Add

このViewでは、Taskの一覧を表示する他に、Create/Update/Deleteが出来るようになっています。

それぞれのUI要素をFormタグで囲んで、ボタンが押されたら

  • /create
  • /update
  • /delete

というURLにデータをPOSTするようになっています。



routes/index.jsの方ではそれぞれのURLに対応したルートハンドラを定義してTaskの挿入・更新・削除の処理を行っています。

routes/index.js (続き)

router.post('/create', async function(req, res) {
  const newTask = db.Task.build({
    task: req.body.task,
    done: false
  });
  await newTask.save();

  res.redirect('/');
});

router.post('/update', async function(req, res) {
  const task = await db.Task.findByPk(req.body.id);
  if (task) {
    task.done = !!(req.body.done);
    await task.save();
  }

  res.redirect('/');
});

router.post('/delete', async function(req, res) {
  const task = await db.Task.findByPk(req.body.id);
  if (task) {
    await task.destroy();
  }

  res.redirect('/');
});


module.exports = router;

今回はサンプルなのでエラー処理などを省略してしまいましたが、本格的にSequelizeを使ったアプリケーションの作成方法については、また別のブログ記事として詳しく書きたいと思います。



最後にスタイルシートを編集して見た目を整えます。

最終的に出来上がったのは、下の画像のようなTodoリストアプリケーションでした。

Task List



ソースコード

ここまでの全ソースコードは、下記から参照可能です。

https://github.com/ishidait/docker-nodejs/tree/blog-3



次回

次回はこのアプリケーションをHerokuにデプロイして公開するまでの流れを見てみたいと思います。