[Node.js] SequelizeでMySQLを利用する - その1「チュートリアル編」

Node.jsでO/RMと言えば「Sequelize」です。

PostgreSQL, MySQL, MariaDB, SQLite, Microsoft SQL Serverなど主要なRDBに対応しており、AWS上での利用報告もありますのでRDSなどでももちろん使えます。また現在たどれる最古のバージョンv1.1.2は2011年にリリースされていることからすでに10年以上の歴史がありもちろん現在もメンテナンスが継続しています。これは安心して利用できますね!

というわけで今回はSequelizeを初めて触るチュートリアルとしてまとめていきます。現行バージョンであるv6のドキュメントを元にしています。

インストール

MySQL

詳細は割愛しますがDockerで用意しても良いですし、Amazon LinuxCentOSの場合は以下の記事のMySQLの項目をご覧ください。 blog.katsubemakito.net

macOSの場合はHomeBrewで入れることもできます。 blog.katsubemakito.net

MySQLのインストールが終わったら練習用に適当なユーザーとデータベース(Scheme)を作成しておきます。テーブルは作らなくてOKです(というか作らないでください)

Node.js

Node.jsがインストールされている環境に適当なディレクトリを作成、npm initを実行しpackage.jsonを生成します。

$ mkdir test; cd test;
$ npm init

sequelizeとmysql2を入れれば準備完了です。

$ npm install sequelize mysql2  

無印のmysqlは2年ほど更新されていないようなので、Sequelizeに限らず現在では後継であるmysql2を利用した方が良いようです。

基本的な使い方

接続からシンプルなCRUDするまでを取り上げます。

サンプルコードは必要な部分だけ抜粋していますので、Top-Level Awaitに未対応の環境でawaitが最初に付いている構文を実行する場合は、asyncが付いている関数内かthen()内で実行してください。

MySQLへ接続する

Sequelizeのインスタンスを生成する際に接続情報を指定します。

const { Sequelize, DataTypes } = require('sequelize')

const sequelize = new Sequelize('database_name', 'user', 'password', {
  host: 'localhost',
  dialect: 'mysql',
  // logging: false
})
  • パスワードを設定していない場合はnullを指定します。
  • dialectはここではmysql固定です。
  • 通常Sequelize経由で実行されたSQLconsole.log()デバッグ的に表示されますが、不要な場合はlogging: falseで非表示にできます。

複数のデータベースに同時に接続するような場合は、インスタンスを必要な数だけ作成します。またレプリケーションにも対応していますので、更新関係はマスター、SELECTはスレーブに発行すると言ったことも可能です。

モデルを準備

定義

データベースで言うテーブルの構成をNode.js上でも定義します。以下ではUserテーブルにnameとageというカラムを用意しています。

const User = sequelize.define('User', {  // Userテーブル
  name: {
    type: DataTypes.STRING,  // 文字列型
    allowNull: false         // Not Null
  },
  age: {
    type: DataTypes.INTEGER  // 整数型
  }
})

データ型

モデルの定義時に利用できるデータ型には一般的な物が各種用意されています。

Sequelize MySQL
DataTypes.STRING VARCHAR(255)
DataTypes.TEXT TEXT
DataTypes.BOOLEAN TINYINT(1)
DataTypes.INTEGER INTEGER
DataTypes.BIGINT BIGINT
DataTypes.DOUBLE DOUBLE
DataTypes.FLOAT FLOAT
DataTypes.DECIMAL DECIMAL
DataTypes.DATE DATETIME
DataTypes.DATEONLY DATE
  • 桁数の指定が可能です。
    • 例えばDataTypes.STRING(128)とすると、SQLではVARCHARA(128)となります。
    • BIGINT, FLOAT, DOUBLE, DECIMALも同様です
  • 整数型にUNSIGNED指定をする場合はDataTypes.INTEGER.UNSIGNED
  • 整数型にゼロ字詰めを指定する場合はDataTypes.INTEGER.ZEROFILL
  • UNSIGNEDとゼロ字詰めを同時に指定する場合はDataTypes.INTEGER.UNSIGNED.ZEROFILL

変わったところだと以下のようにUUIDを自動生成してくれる機能もあります。MySQL上にはCHAR(36)としてカラムが作成され、INSERT時にUUIDが自動的に挿入されます。

uniq_id: {
  type: DataTypes.UUID,
  defaultValue: Sequelize.UUIDV4  // もしくは Sequelize.UUIDV1
}

カラムのオプション

CREATE TABLE文で利用する様々な指定が用意されています。

key: {
  type: DataTypes.INTEGER,
  primaryKey: true,         // PRIMARY KEY
  autoIncrement: true,      // AUTO_INCREMENT
  comment: '主キー'          // COMMENT
},
data1: {
  type: DataTypes.STRING(32),
  unique: true,             // UNIQUE
},
flag1: {
  type: DataTypes.BOOLEAN,
  allowNull: false,         // NOT NULL
  defaultValue: true        // DEFAULT
}
bar_id:{
  type: DataTypes.INTEGER,

  // 外部キー
  // 実際生成されるSQLは FOREIGN KEY (`bar_id`) REFERENCES `Bars` (`id`)
  references: {
    model: Bar,
    key: 'id'
  }
} 

テーブル全体のオプション

sequelize.define()の第3引数にオプションを渡すことでテーブル全体の定義ができます。

const User = sequelize.define('User', {
  //-------------------------
  // ここにカラムの定義
  //-------------------------
  name: {
     type: DataTypes.STRING,
     allowNull: false
  }
}, {
  //-------------------------
  // ここにテーブル全体の定義
  //-------------------------
  // MySQL上のテーブル名を複数形にしない(デフォルトはfalse)
  freezeTableName: true,

  // タイムスタンプ機能を利用する(デフォルトはtrue)
  // falseにするとcreatedAt, updatedAtのカラムが作成されません
  timestamps: false,

  // タイムスタンプ機能を一部調整(デフォルトはtrue)
  createdAt: false,         // レコード生成時の時間を記録
  updatedAt: false,         // レコード更新時の時間を記録

  // Booleanではなく文字列を指定するとMySQL上のカラム名を変更できます
  // createdAt: 'created_at',
  // updatedAt: 'updated_at'
})

MySQLへ反映する

モデルの定義が終わったら、Model.sync()を実行することでMySQL上に先ほどの定義を反映することができます。

await User.sync({force: true})

実際に以下のSQLが発行されます。

  • まずテーブル名が「User」ではなく「Users」と複数形になります。気持ち悪い場合はオプションでモデル名とテーブル名を同じスペルにすることも可能です。
  • idは自分でプライマリーキーを指定しなかった場合に自動的に付与されます(自分でプライマリーキーを指定した場合は付与されません)。
  • createdAtupdatedAtはレコードが生成、更新された日時を記録するものでこちらも勝手に付いてきます。他のフレームワークでもよく見かけますね。
CREATE TABLE IF NOT EXISTS `Users` (
   `id`        INTEGER      NOT NULL  auto_increment,
   `name`      VARCHAR(255) NOT NULL,
   `age`       INTEGER,
   `createdAt` DATETIME     NOT NULL,
   `updatedAt` DATETIME     NOT NULL,

   PRIMARY KEY (`id`)
) ENGINE=InnoDB;

なおModel.sync()の実行方法は次の3種類があります。特にforce: trueは既存のデータがすべて削除されてしまうため十分に注意する必要があります。開発中は便利なんですけどね。

Model.sync()
テーブルが存在しない場合は作成されます。すでに存在する場合は何もしません。
Model.sync({ force: true })
テーブルが存在する場合は削除した上で、新たに作成されます。
Model.sync({ alter: true })
テーブルの状態を事前にチェックし、カラム数やデータ型に変更がある場合は、その差分が適用されます。

開発中は非常に便利なんですが、本番で毎回この処理が入るのはパフォーマンス的に問題が出そうなので、Sequelize CLIなどで必要なときにマイグレーションする方が良さそうですね。

データ操作

INSERT

Model.create()でINSERT文が発行できます。

const user = await User.create({name:'Honda', age:12})

実際に発行されるSQLは以下の通り。プライマリーキーであるidとレコードの作成/更新日時が入るcreatedAt, updatedAtも自動的に付与されているのがわかります。またVALUESの各項目がプレースホルダになっており自動的にエスケープされることが期待できますのでSQLインジェクションの心配も無さそうですね。

INSERT INTO `Users` (`id`,`name`,`age`,`createdAt`,`updatedAt`) VALUES (DEFAULT,?,?,?,?);

ちなみにModel.bulkCreate()を使うと一度に複数のデータを挿入できます。裏側では1回のINSERTで複数のデータが挿入されています。

const user = await User.bulkCreate([
  {name:'Honda',    age:18},
  {name:'Yamaha',   age:16},
  {name:'Suzuki',   age:20},
  {name:'Kawasaki', age:24}
])

SELECT

SELECT文を発行する方法はいくつかあります。検索結果の全レコードを取得するにはModel.findAll()を使います。SELECT文で利用できるWHERE句やGROUP BY、ORDER BYなども使えます。もちろんoffsetやlimitなどで件数制限も可能。

const rows = await User.findAll()
rows.forEach(row => {
  const id = row.id
  const name = row.name
  const age  = row.age

  console.log(`${id}: ${name} ${age}`)
})

他にも以下のようなメソッドが用意されています。

Model.findByPk()
プライマリーキーを引数として渡すと該当するレコードが、もし存在しない場合はnullが返されます。
Model.findOne()
一番最初のレコードを1件だけ返します。
Model.findOrCreate()
指定条件で検索し、該当するレコードがあれば返却、もし存在しない場合は指定したデータを挿入します。
Model.findAndCountAll()
条件に一致したレコード数と、モデル内の全レコード数を同時に返却します。ページングするときとか便利ですね。

UPDATE

Model.update()を使います。以下のコードでwhereの条件に合致するレコードのageを21に更新しています。

await User.update({ age: 21 }, {
  where: {
    name: 'Suzuki'
  }
})

DELETE

Model.destroy()を利用します。以下のコードでwhereの条件に合致するレコードをすべて削除します。これdeleteとかremoveじゃないんですね。destroyとか書くのちょっと怖いw

await User.destroy({
  where: {
    name: 'Yamaha'
  }
})

データを丸ごと消してリセットしたい場合は同じくModel.destroy()でTRUNCATEを実行可能です。

await User.destroy({
  truncate: true
})

MySQLから切断する

MySQLから切断するにはsequelize.close()を呼ぶだけです。

await sequelize.close()

ここまでのサンプルコード

/**
 * Sequelize CRUD Sample
 *
 */
const { Sequelize, DataTypes } = require('sequelize')
const sequelize = new Sequelize('database_development', 'root', null, {
  host: 'localhost',
  dialect: 'mysql',
  logging: false,
})

//--------------------------------------------
// Models
//--------------------------------------------
const User = sequelize.define('User', {
  name: {
    type: DataTypes.STRING(128),
    allowNull: false
  },
  age: {
    type: DataTypes.INTEGER
  }
})

//--------------------------------------------
// CRUD
//--------------------------------------------
!(async()=>{
  // MySQL上にテーブルを作成
  await User.sync({alter: true})

  // 既存のデータを削除(TRUNCATE)
  await User.destroy({
    truncate: true
  })

  // Userテーブルへデータを挿入
  const user = await User.bulkCreate([
    {name:'Honda',    age:18},
    {name:'Yamaha',   age:16},
    {name:'Suzuki',   age:20},
    {name:'Kawasaki', age:24}
  ])

  // 'Suzuki'のageを21に更新
  await User.update({ age: 21 }, {
    where: {
      name: 'Suzuki'
    }
  })

  // 'Yamaha'を削除
  await User.destroy({
    where: {
      name: 'Yamaha'
    }
  })

  // Userテーブルの全レコードを取得
  const rows = await User.findAll();
  rows.forEach(row => {
    const id = row.id
    const name = row.name
    const age  = row.age

    console.log(`${id}: ${name} ${age}`)
  })

  // MySQLから切断
  await sequelize.close()
})()

続き

blog.katsubemakito.net

参考ページ