【NodeでWebアプリ】(3) Sequelizeでデータベース接続

express2-2.png

3. データベース接続

Expressはデータベースに接続してデータを読み書きするような機能は提供していませんが、Node.js用に提供されているパッケージを使うことができます。今回の解説では、データベース処理の実装方法をざっくりと把握することを目的として、手短に実装できるSQLiteと、SequelizeというORMを使います。

3.1 Sequelize

Screen Shot 2019-06-22 at 0.16.39.png

SequelizeはNode.js用のORMです。ORMとはObject-Relational Mapping の略で、サーバーサイドのオブジェクト実装と、リレーショナルデータベースのマッピングをするものです。

Screen Shot 2019-06-25 at 18.58.29.png

※ORMにはメリットとデメリットがあるので、実際のプロジェクトでの導入にあたっては総合的な検討が必要だと思います。

Sequelize公式ページ:
http://docs.sequelizejs.com/

3.2 SQLite

今回は手軽に実装できるSqliteを使うことにしますが、SequelizeはMySQL、PostgreSQL、Microsoft SQL Serverをサポートしており、基本的にはどのDBでも同じような流れで使うことができます。また、Sequelize-CLIはSQLiteのインスタンスファイルを生成することができますので、SQLiteをインストールする手間も省けます。

3.3 今回作成するもの

SQLiteデータベースにUserテーブルを作成し、以下のようなカラムを持たせます。

  • firstName : string
  • lastName : string
  • email : string

その後サンプルデータを投入して、JSON形式で出力するところまで作成します。

Screen Shot 2019-06-25 at 19.11.48.png

3.4 パッケージのインストール

以下の3つのパッケージをインストールします。

・sequelize(v5.8.10)
・sequelize-cli (v5.5.0)
・sqlite3 (v4.0.9)

1
$ npm install --save sequelize sequelize-cli sqlite3

3.5 Sequlize-CLIを使ったセットアップ

今回Sequelize-CLIで行うことは大きく4つあります。

(1) DB接続設定
(2) Modelファイル生成、Migrationファイル生成
(3) Migration実行
(4) サンプルデータ生成/投入

Modelファイルというのはサーバーサイドのクラスファイルのようなもので、DBのカラムと同じ情報を持つように構成されます。またMigrationというのは、データベースのテーブルなどを作成することです。(1)〜(4)の中で、ポイントとなるのは(2)の操作です。(2)の操作によって、サーバーサイドとデータベースの両方に対し、1対のペアになるようなデータ構造を実現するためのスクリプトが生成されます。

Screen Shot 2019-06-25 at 7.45.42.png

Sequlize-CLIでの操作概要を把握できたと思いますので、実際にコマンドを実行していきましょう。

(1) DB接続設定

先ず、sequelizeプロジェクトを初期化します。

1
$ npx sequelize init

成功すると以下の4つのディレクトリが生成され、完了メッセージが表示されます。

  • config
  • models
  • migrations
  • seeders
1
2
3
4
Created "config/config.json"
Successfully created models folder at "/Users/naota/express_dev/myapp1/models".
Successfully created migrations folder at "/Users/naota/express_dev/myapp1/migrations".
Successfully created seeders folder at "/Users/naota/express_dev/myapp1/seeders".

configファイル編集

次に./config/config.json を編集してDB接続設定を行います。config.jsonは初期状態では以下のようにMySQLの設定例が記載されています。JSONデータがdevelopment / test / production の3つの部分に分かれており、それぞれの実行環境でデータベースを使い分けられるようになっています。実行環境の指定は環境変数のNODE_ENVで行います。NODE_ENVが未設定の場合はデフォルトでdevelopmentの記述が使用されます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{
"development": {
"username": "root",
"password": null,
"database": "database_development",
"host": "127.0.0.1",
"dialect": "mysql",
"operatorsAliases": false
},
"test": {
"username": "root",
"password": null,
"database": "database_test",
"host": "127.0.0.1",
"dialect": "mysql",
"operatorsAliases": false
},
"production": {
"username": "root",
"password": null,
"database": "database_production",
"host": "127.0.0.1",
"dialect": "mysql",
"operatorsAliases": false
}
}
項目 意味
dialect sqlite DB種別はSQLite
strage ./myapp.sqlite3 DBインスタンスは./myapp.sqlite3

(2) Modelファイル生成、Migrationファイル生成

この操作はORMにとってカギとなる重要な場面なので、実際のプロジェクトでは慎重に行う必要があります。予めデータベースのテーブルやカラムなどの設計をしておく必要があります。いったんこの操作を実行した後で、しばらくしてテーブル名などを変更しようとすると、ソースコードのいろいろな箇所を修正する必要が出て何かと面倒です。

Sequelize-CLIのmodel:generateというコマンドを使います。

1
sequelize model:generate --name [モデル名]--attributes [メンバ情報]

パラメータにモデル名やメンバ情報を指定します。「モデル名」というのはサーバーサイドでの呼び方であり、データベース側では「テーブル名」ということになります。また同じく「メンバ情報」というのはデータベース側では「カラム情報」ということになります。このように、同じ名前をサーバー側とデータベース側で共有するというところがORMが「Object-Relational Mapping」であるためのポイントでもあります。

オプション サーバーサイド DB側
–name User モデル名 テーブル名
–attributes firstName:string,
lastName:string,
email:string
メンバ情報 カラム情報

以下のようにコマンドを実行します。

1
$ npx sequelize model:generate --name User --attributes firstName:string,lastName:string,email:string

コマンドを実行すると、User情報に関する2つのjsファイルが生成されます。1つは、サーバーサイドでUser情報を格納するためのモデル定義のModelファイル。もうひとつはデータベースでUserテーブルを生成するためのMigrationファイルです。

Screen Shot 2019-06-24 at 17.01.49.png

コマンドの実行が成功すると以下のように出力されます。

1
2
New model was created at /(プロジェクトフォルダ)/myapp/models/user.js .
New migration was created at /(プロジェクトフォルダ)/myapp/migrations/(現在日付)-create-user.js .

お時間のある方は生成されたjsファイルの中身を見てみましょう。お急ぎの方は次の「(3) Migration実行」へお進みください。

Modelファイルを見てみる

/models/user.jsを開いてみると以下のようになっています。先ほどパラメータで指定した情報があっさりとオブジェクトとして定義されています。

1
2
3
4
5
6
7
8
9
10
11
12
'use strict';
module.exports = (sequelize, DataTypes) => {
const User = sequelize.define('User', {
firstName: DataTypes.STRING,
lastName: DataTypes.STRING,
email: DataTypes.STRING
}, {});
User.associate = function(models) {
// associations can be defined here
};
return User;
};

Sequelizeが生成したモデルファイルは、全体としてはmodule.exportsでエクスポートされて、Userオブジェクトを返す構造になっています。(Nodeが初めての方へ:Node.jsのmodule.exportsはES6のexport文とは異なりますので軽くご注意ください。)

1
2
3
4
module.exports = function(sequelize, DataTypes){
//...
return User;
}

このようにexportされたモジュールなので、読み込む側では、うっかり以下のように書いてしまいそうです。

1
2
// 誤った読み込み方
var User = require('../models/user.js');

しかし実際は、もう1つ上の階層を使ってrequire('../models/')とします。

1
2
3
4
5
6
// 正しい読み込み方
var db = require('../models/');

db.User.findAll().then(users => {

});

どうしてこのような多段階のモジュール構成になるのかというと、Sequelizeは初期化のときに/models/index.jsという、親モジュールになるモジュールを生成していて、そのモジュールが/models/ディレクトリにある全てのモジュールを読み込み込んだ上で、読み込んだUserモジュールなどにfindAll()などのメソッドを付け加えるという処理をしているからです。

1
2
3
4
5
6
// modelsディレクトリの中のファイルを探して読み込む箇所
.forEach(file => {
const model = sequelize['import'](path.join(__dirname, file));
db[model.name] = model;
}
//...

Migrationファイルを見てみる

続いて、/migrations/(現在日付)-create-user.jsを見てみましょう。このファイルもModelファイルと同じようにmodule.exportsでエクスポートされています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
'use strict';
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.createTable('Users', {
id: {
allowNull: false,
autoIncrement: true,
type: Sequelize.INTEGER
primaryKey: true,
},
firstName: {
type: Sequelize.STRING
},
lastName: {
type: Sequelize.STRING
},
email: {
type: Sequelize.STRING
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
},
down: (queryInterface, Sequelize) => {
return queryInterface.dropTable('Users');
}
};

ざっと眺めて先ほどのModelファイルより情報量が多い理由としては、Migrationはデータベースを操作するためのものなので、カラムの制約に関する指定(Nullを許可、autoIncrement、primaryKeyなど)があるということと、DB用カラムのcreatedAt(レコード作成日時)、updatedAt(レコード更新日時)などが存在していることが挙げられます。

また、”up”と”down”の2つの部分に分かれていますが、downにはupの操作を取り消す(revert)ための操作を書くことができます。例えば “up”が「テーブル作成」ならば、これに対する”down”は「テーブル削除」に相当する操作を記述します。

データの型などを詳細に指定することもできます。例えば、Sequelize.STRINGはデフォルトで VARCHAR(255) になりますがこの値を変更することができます。

Sequelizeの公式サイトに詳細な情報があります。(英語)
http://docs.sequelizejs.com/manual/migrations.html

(3) Migration実行

それではMigrationを実行しましょう。MigrationとはDBの定義ファイルにしたがってDBやテーブルなどを作成することです。Migrationの背後では、SQL文の CREATE TABLEなどが実行されます。

Screen Shot 2019-06-25 at 7.35.24.png

1
$ npx sequelize db:migrate

成功すると以下のように出力されます。

1
2
3
4
Loaded configuration file "config/config.json".
Using environment "development".
== (日付)-create-user: migrating =======
== (日付)-create-user: migrated (0.011s)

Migrationの背後では、例えば以下のようなSQLが実行されています。

1
2
3
4
5
6
7
8
CREATE TABLE `Users` (
`id` INTEGER PRIMARY KEY AUTOINCREMENT,
`firstName` VARCHAR(255),
`lastName` VARCHAR(255),
`email` VARCHAR(255),
`createdAt` DATETIME NOT NULL,
`updatedAt` DATETIME NOT NULL
)

ここまでの段階で、SQLiteのデータベースファイルが作成されました。

DB Browser for SQLiteなどのGUIツールで確認してみましょう。
https://sqlitebrowser.org/

Screen Shot 2019-06-25 at 7.50.27.png

(4) サンプルデータ生成/投入

続いてサンプルデータを作成して、投入していきます。

Screen Shot 2019-06-25 at 7.44.23.png

1
$ npx sequelize seed:generate --name seed-user

成功すると以下のように出力されます。

1
New seed was created at /(プロジェクトフォルダ)/myapp/seeders/(現在日付)-seed-user.js .

./seeders/(現在日付)-seed-user.jsを開いて以下のように編集します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
'use strict';

module.exports = {
up: (queryInterface, Sequelize) => {

return queryInterface.bulkInsert('Users', [{
firstName: 'aaa',
lastName: '',
email: 'aaa@example.com',
createdAt: new Date().toDateString(),
updatedAt: new Date().toDateString()
}, {
firstName: 'bbb',
lastName: 'BBB',
email: 'bbb@example.com',
createdAt: new Date().toDateString(),
updatedAt: new Date().toDateString()
}, {
firstName: 'ccc',
lastName: 'CCC',
email: 'ccc@example.com',
createdAt: new Date().toDateString(),
updatedAt: new Date().toDateString()
}], {});
},

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

サンプルデータを投入

1
$ npx sequelize db:seed:all

成功すると以下のように出力されます。

1
2
3
4
Loaded configuration file "config/config.json".
Using environment "development".
== (日付)-seed-user: migrating =======
== (日付)-seed-user: migrated (0.005s)

GUIツールで確認してみます。

Screen Shot 2019-06-19 at 19.05.01.png

データが挿入されていれば成功です。

3.6 JSON形式で出力する

データベースへの接続準備が整いましたので、アプリケーションから接続してデータを取得してみましょう。今回は、簡単に書くために./routes/users.jsというルーティング用のファイルの中に直接書く方法で行います。(次回のレッスンではMVCアーキテクチャでデータを取得して表示する方法を学びます。)

データベースのデータを読み込む

先ずデータを読み込む部分ですが、これは次の1行を書くだけです。

1
var db = require('../models/');

次に、データを出力する部分は以下のようになります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ユーザーのリストをJSON出力する
router.get('/json/', function(req, res, next) {

// Sequelizeのモデルを使ってデータを取得する
db.User.findAll().then(users => {

if (!users) {
console.log("ユーザーデータを取得できませんでした");
res.send('Error');
} else {
res.json(users);
}
});
});

それぞれの行の意味を簡単に説明します。

1
2
1: // ユーザーのリストをJSON出力する
2: router.get('/json/', function(req, res, next) {

これは、/user/json/というリクエストに対するハンドラを定義しています。前回のレッスンでも学びました。

1
2
4:    // Sequelizeのモデルを使ってデータを取得する
5:  db.User.findAll().then(users => {

ここでは、SequlizeのfindAll()メソッドを使って、データベースのUserテーブルからデータを取得しています。背後では、SQLのSELECT文が実行されます。Sequlizeに限らずNodeでのDBデータ取得は非同期で行われるので、データを取得した後の処理はCallbackかPromiseなどの仕組みを使います。SequlizeのメソッドはPromiseを返すので、成功した場合は.then()で受けます。.then(function(users){})という形でパラメータとして取得したデータを受け取ります。

1
2
3
4
7:      if (!users) {
8: console.log("ユーザーデータを取得できませんでした");
9: res.send('Error');
10: } else {

ここでは、.then()で受け取ったusersデータが空(nullやundefined)かチェックしています。空の場合はエラーを返しています。

1
2
11:           //レスポンスを返す
12: res.json(users);

responsオブジェクトのjson()メソッドを使って、取得したデータをJSON形式で返しています。

以上の処理を./routes/users.jsに記述すると全体としては以下のようになります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var express = require('express');
var router = express.Router();
var db = require('../models/');

/* GET users listing. */
router.get('/', function(req, res, next) {
res.send('respond with a resource');
});

// ユーザーのリストをJSON出力する
router.get('/json/', function(req, res, next) {

// Sequelizeのモデルを使ってデータを取得する
db.User.findAll().then(users => {

if (!users) {
console.log("ユーザーデータを取得できませんでした");
res.send('Error');
} else {
res.json(users);
}
});
});

module.exports = router;

アプリを起動して確認する

それではアプリを起動して確認してみましょう。

1
$ npm start

ブラウザで次のURLにアクセスします。

http://localhost:3000/users/json

以下のようにJOSNデータが表示されれば成功です。

Screen Shot 2019-06-21 at 18.18.07.png

まとめ

Sequlize-CLIを使ってDB実装を行いました。その中で以下のようなコマンドを使いました。

1
2
3
4
5
6
$ npm install --save sequelize sequelize-cli sqlite3
$ npx sequelize init
$ npx sequelize model:generate --name User --attributes firstName:string,lastName:string,email:string
$ npx sequelize db:migrate
$ npx sequelize seed:generate --name seed-user
$ npx sequelize db:seed:all

また、DBのデータをJSONで出力させるために、以下の関数を ./routes/users.jsに追加しました。

1
2
3
4
5
6
7
8
9
10
11
12
/* ユーザーのリストをJSON出力する */
router.get('/json/', function(req, res, next) {
// Sequelizeのモデルを使ってデータを取得する
dbModels.User.findAll().then(users => {

if (!users) {
console.log("ユーザーデータを取得できませんでした");
res.send('Error');
} else {
res.json(users);
}
});

次回はMVCアーキテクチャについて学びます。

この記事をシェアする