Tech

【Node.js+RDB】Sequelize使い方まとめ

2020年6月3日

データベースを管理していた元インフラエンジニアの管理人です。

Node.jsでデータベースMySQL, MariaDB, PostgresSQL, SQLiteなどを使いたいときに便利なライブラリSequelizeについて。

Node.js + Express + MySQLの環境でSequelizeを導入する手順メモです。
SQLiteなど他のDBを使う場合でもほとんど同じなので参考にしていただければ。
導入から実際にレコードをHTTPで返すところまで解説しています。

公式マニュアル(英語)

 

パッケージインストール

Express generatorでサーバの立ち上げができる環境ができている前提で解説していきます。

まずはsequelizeを動かすのに必要なパッケージインストール。

$ npm install -s nodemon sequelize-cli mysql2 sequelize

sequelize-cli はmigrationファイルを自動生成するコマンドを打つのに使ったりします。

 

使用するディレクトリ

Expressを使っているので以下のようなディレクトリ構成になっているはず。

このうち、config、migrations、models、seedersは後述のコマンドで自動生成するので、まだ作らなくてOK。

 

Sequelizeを使う準備

Sequelizeを使うにあたって、まずは以下のコマンドを実行。

$ sequeliza init

そうすると、configディレクトリの下に config.json(環境設定ファイル)、modelsの下に index.js(モデル読込)が作られる。

自動生成された index.js は次のようなファイル。

'use strict';

const fs = require('fs');
const path = require('path');
const Sequelize = require('sequelize');
const basename = path.basename(__filename);
const env = process.env.NODE_ENV || 'development';
const config = require(__dirname + '/../config/config.json')[env];
const db = {};

let sequelize;
if (config.use_env_variable) {
  sequelize = new Sequelize(process.env[config.use_env_variable], config);
} else {
  sequelize = new Sequelize(config.database, config.username, config.password, config);
}

fs
  .readdirSync(__dirname)
  .filter(file => {
    return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js');
  })
  .forEach(file => {
    const model = sequelize['import'](path.join(__dirname, file));
    db[model.name] = model;
  });

Object.keys(db).forEach(modelName => {
  if (db[modelName].associate) {
    db[modelName].associate(db);
  }
});

db.sequelize = sequelize;
db.Sequelize = Sequelize;

module.exports = db;

 

SequelizeのようなORM(ORマッパー)を使うときはモデルをたくさん定義しておくことになりますが、それらを個別にimportしなくても網羅的に読み込んでくれるのが、この index.js さんです。

あとはモデルを自分で定義すればDBの読み書きができる。
簡単だね!!

 

モデルの定義とマイグレーション

そして肝心のモデル定義ファイルを作成します。

いちいちモデルファイルをゼロから作るのは面倒なので、コマンド実行することでモデル定義ファイルとマイグレーションファイルのテンプレートを自動生成しましょう。

nameなどのカラム(項目)を持つ Owner というテーブルを作るときのコマンド例↓

$ sequelize model:generate --name Owner --attributes unique:string,class_id:bigint,name:string,followers:integer

これを実行すると以下のようなモデル定義が models の下に作られる(自動生成)。

'use strict';
module.exports = (sequelize, DataTypes) => {
  const Owner = sequelize.define('Owner', {
    unique: DataTypes.STRING,
    name: DataTypes.STRING,
    class_id: DataTypes.BIGINT,
    followers: DataTypes.INTEGER
  }, {});
  Owner.associate = function(models) {
    // associations can be defined here
  };
  return Owner;
};

 

要件によって、制約とかアソシエーションとか付けたいですよね。
下記のように、モデル定義を要件に合うように加工していきましょう。

'use strict';
module.exports = (sequelize, DataTypes) => {
  const Owner = sequelize.define('Owner', {
    unique: {
      field: "unique",
      type: DataTypes.STRING,
      allowNull: false
    },
    classId: {
      field: "class_id",
      type: DataTypes.BIGINT,
      allowNull: false
    },
    name: {
      field: "name",
      type: DataTypes.STRING,
      allowNull: false
    },
    followers: {
      field: "followers",
      type: DataTypes.INTEGER,
      allowNull: true
    }
  }, {
    tableName: "owners" // テーブル名を直接指定
    timestamps: true,
    updatedAt: "updated_at", 
    createdAt: "created_at",
  });
  Owner.associate = function(models) {
    // アソシエーションの設定
    Owner.belongsTo(models.ClassMap, {
      foreignKey: 'class_id',
    });
  };
  return Owner;
};

 

サンプルでは、Ownerテーブルの各カラムに制約などを付加した上で、Ownerのクラスを定義しているClassMapテーブルとのアソシエーションを設定しました。
タイムスタンプカラムもsequelizeだと勝手に createdAt という名前になってしまうため、created_atと手動で設定しなおしています(テーブル設計、要件に合わせてください)。

一方、sequelize model:generateコマンドを実行するとマイグレーションファイルも自動的に作られます(下記参照)。
まだテーブルが作られていない段階なら、このファイルをテーブル設計に合わせて加工してから、migrationを実行します。

マイグレーションファイルはテーブルの作成・更新を自動化してくれるスクリプトみたいなもの。
ただし、プログラム中でsequelizeが参照するのはモデルファイルだけなので注意が必要です。
マイグレーションファイルは開発時のテーブル定義の更新管理にだけ使うと考えればよいでしょう。

 

'use strict';
module.exports = {
  up: (queryInterface, Sequelize) => {
    return queryInterface.createTable('Owners', {
      id: {
        allowNull: false,
        autoIncrement: true,
        primaryKey: true,
        type: Sequelize.INTEGER
      },
      unique: {
        type: Sequelize.STRING
      },
      class_id: {
        type: Sequelize.BIGINT
      },
      name: {
        type: Sequelize.STRING
      },
      followers: {
        type: Sequelize.INTEGER
      },
      createdAt: {
        allowNull: false,
        type: Sequelize.DATE
      },
      updatedAt: {
        allowNull: false,
        type: Sequelize.DATE
      }
    });
  },
  down: (queryInterface, Sequelize) => {
    return queryInterface.dropTable('Owners');
  }
};

 

ちなみに、既存テーブルのmigrationファイルを作る場合にはこのQiitaが参考になる

既存テーブル(サンプルではownersテーブル)にカラムを追加した例がこれ↓。
Promiseを返す必要があることに注意です。

 

'use strict';

module.exports = {
  up: (queryInterface, Sequelize) => {
    return Promise.all([
      queryInterface.addColumn('owners', 'class_id', {
        type: Sequelize.BIGINT,
        after: 'unique' // for MySQL only
      }),
      queryInterface.addColumn('owners', 'followers', {
        type: Sequelize.INTEGER,
        after: 'name' // for MySQL only
      }),
    ]);
  },

  down: (queryInterface, Sequelize) => {
    return Promise.all([
      queryInterface.removeColumn('owners', 'class_id'),
      queryInterface.removeColumn('owners', 'followers')
    ]);
  }
};

 

 

migrationの実行は次のコマンド。
環境指定を忘れずにしよう。

$ sequelize db:migrate --env development

 

 

DB接続と接続確認

さきほど sequelize init コマンドでconfigファイルが生成されていると思うので、そこに接続情報を更新しておきます。

試しに使うだけなので環境はdevelopmentに更新。
本番環境ではパスワード等の直書きは避けたい。環境変数を使おう

{
    "development": {
        "username": "root",
        "password": "passw0rd",
        "database": "sequelize", 
        "config": {
            "host": "db",
            "dialect": "mysql",
            "logging": "console.log"
        }
    },
    "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
    }
}

 

app.jsにDB接続用のコードを追加。
接続を確認するだけの記述です。

try {
    sequelize.authenticate();
    console.log('Connection has been established successfully.');
} catch (error) {
    console.error('Unable to connect to the database:', error);
}

 

Model synchronization

Sequelize側で定義したモデルと実際のDBの定義に違いがでることも考えられますよね?
そういった場合には当然エラーを出したり予期しない動作をすることも考えられます。

それを防ぐために、Sequelizeではmodel synchronizationという仕組みが用意されています。

Model synchronizationを実行することで、プログラム側で定義したテーブルがDBにない場合は新規でテーブルを作り、テーブルのカラムがモデルと相違しているときはカラムを変更してくれるなど、よきにはからってくれます。

app.js などに記述を追加しておきます↓

models.sequelize.sync().then(() => {
  console.log('Seems like the backend is running fine...');
}).catch((err) => {
  console.log(err, 'Something went wrong with the operation');
});

 

 

DBから取得

いよいよDBからselectしてきましょう。

下記コードを router.get() の中に書けば、ブラウザ上にSELECTした内容を json形式で表示してくれます。

ここでは、findAll()ですべてのレコードを取得しています。

const db = {};
cdb.Owner = models.Owner;
 models.sequelize.transaction(async t => {
    const b = await Owner.findAll();
    if (b !== null) {
      return Promise.resolve(res.send(b));
    } else {
      return Promise.resolve(res.sendStatus(404));
    }
  });

 

ちなみに、sequealizeのトランザクション設計には2通りあります。
Unmanaged transactions Managed transactionsです。
簡単にいえば、手動でコミット&ロールバックするか、自動でしてくれるかの違い。
ここではmanagedの方を使用しているため、明示的にロールバックの記述をしていません。
-> manual

サンプルはここまで。
APIの仕様に応じてrouter.post()とかrouter.delete()にINSERTやDELETEのコード追加することで、sequelizeを使ったREST APIを作ることができます。
いろいろ試してみると楽しいです。

 

-Tech
-, ,

© 2020 スターレイヴ