SequelizeでDBのテーブルとデータを作成し、CRUD操作してみる（1）

# プロローグ
　`Sequelize`は、公式によると<b>PromiseベースのNode.jsのORMツール</b>とのこと。<br>ORMについてはちゃんとは分かってないけど、「コード上でのデータベース操作を簡単にしてくれるツール」的なモノらしい。これに関しては、調べると<b>インピーダンスミスマッチ</b>とかいう難しそうな概念がでてきたりとかして深みにはまりそうだったので、ワイは「なんか便利らしいしそれ使うんならORMを使わない方法と比べてどうとかはとりあえずいいや」ってことにしたんで今回はスルーするンゴ。

　今回は`Sequelize-cli`というCLIツールを使ってアプリケーションプロジェクトの環境を構築し、DB側のテーブルやデータを作成する手段から、Sequelizeを通したDBに対するCRUD操作までを2つの記事にわたって網羅的に解説していく。

# 開発環境
* `sequelize: 6.21.0`
* `sequelize-cli: 6.4.1`
* `node: 17.1.0`
* `npm: 8.1.2`

# 準備
　Sequelize-cliはSequelizeとは別のパッケージであるため、別個にインストールする必要がある。

```
npm i -D sequelize-cli
```
　テーブルやデータの作成はsequelize-cliがなくても出来るけど、使ったほうが効率的に開発ができるので積極的に使っていきたい。まずは以下を実行してテーブルやデータの作成のための準備をする。

```
npx sequelize init
```
　これによってプロジェクト直下に`/config/config.json`,`/migrations/`,`/models/index.js`が作成される。この構成や`config.json`の名前を変更したい場合は、上記コマンド実行前にプロジェクト直下に`.sequelizerc`を作成してそこで指定しておく。 ⇒詳細は[こちら](https://sequelize.org/docs/v6/other-topics/migrations/#the-sequelizerc-file)
　そしてsequelizeとデータベースを連携させるため、作成された`config.json`の`development`の各種設定を自身の環境のものに変えておく。

# テーブル・モデルの定義
　これでテーブルやデータの作成の準備が出来たので、まずはマイグレーションファイルとモデル（それぞれ何かは後述）を作成する。今回は学籍番号、下の名前、上の名前、論理削除用フィールド（後述）を属性として持つ学生モデルを作る（今回作成するのは1モデルのため、モデル間の[関連性の定義](https://sequelize.org/docs/v6/core-concepts/assocs/)や、[Sequelizeでのテーブル結合（即時読み込み）](https://sequelize.org/docs/v6/advanced-association-concepts/eager-loading/)の仕方などについては扱わない）。[こちら](https://qiita.com/furi-kake/items/37d5b8a74bd0fe662a31)の記事にまとめてるンゴ。

```
npx sequelize model:generate --name Student --attributes studentNo:string,firstName:string,lastName:string,deletedAt:date
```
　これによって`/migrations/YYYYMMDDHHMMSS-create-student.js`（`YYYYMMDDHHMMSS`の部分は実行時点の日付時刻）が、`/models/student.js`が`それぞれ作成される。

```YYYYMMDDHHMMSS-create-student.js
'use strict';
module.exports = {
    async up(queryInterface, Sequelize) {
        await queryInterface.createTable('Students', {
            id: {
                allowNull: false,
                autoIncrement: true,
                primaryKey: true,
                type: Sequelize.INTEGER
            },
            studentNo: {
                type: Sequelize.STRING
            },
            firstName: {
                type: Sequelize.STRING
            },
            lastName: {
                type: Sequelize.STRING
            },
            deletedAt: {
                type: Sequelize.DATE
            },
            createdAt: {
                allowNull: false,
                type: Sequelize.DATE
            },
            updatedAt: {
                allowNull: false,
                type: Sequelize.DATE
            }
        });
    },
    async down(queryInterface, Sequelize) {
        await queryInterface.dropTable('Students');
    }
};
```

```student.js
'use strict';
const {
    Model
} = require('sequelize');
module.exports = (sequelize, DataTypes) => {
    class Student extends Model {
        /**
        * Helper method for defining associations.
        * This method is not a part of Sequelize lifecycle.
        * The `models/index` file will call this method automatically.
        */
        static associate(models) {
            // define association here
        }
    }
    Student.init({
        studentNo: DataTypes.STRING,
        firstName: DataTypes.STRING,
        lastName: DataTypes.STRING,
        deletedAt: DataTypes.DATE
    }, {
        sequelize,
        modelName: 'Student',
    });
    return Student;
};
```
（自動生成されたコードは、自分のエディタの設定とインデントの行数が違っている場合があるため、自動生成後すぐに`Auto Indent`などでフォーマットを整えると良い）

　ここでまずマイグレーションファイルを見てみると、`createTable()`や`dropTable()`というメソッドがあることが分かる。実はマイグレーションファイルはデータベースのテーブル定義とか定義変更のためのファイルになる。

　ところでよく見るとテーブル名が`Students`と複数形になっていることに気づくと思う。これはSequelizeが[inflection](https://www.npmjs.com/package/inflection)というライブラリを使って自動で複数形に行っているみたいで、テーブル名を単数形にしたい場合は手動でマイグレーションファイルをいじる必要があるからちょっとめんどくさい…。しかも`person → people`みたいな複数形が不規則的な名詞もちゃんと変換されるってなってるんだけど、試しにモデル名`stimulus`（単数形）で作ってみたらマイグレーションファイルのテーブルは`stimuli`（複数形）とはならずに`stimulus`のままで、ちょっとここらへんは分からない部分があったり。

　で、テーブルを単数形にする場合は`YYYYMMDDHHMMSS-create-student.js`の`up`の部分のテーブル名だけじゃなくて`down`のほうも単数形にしとかないと、マイグレーションを実行して作成した`Student`を、`migrate:undo`して状態を元に戻す（テーブルを削除する）際に`migrate:undo`が上手くいかないので注意が必要。今回はモデルのほうに合わせてテーブル名も単数形にする。

　それから同じくマイグレーションファイルを見ると、`id`, `createdAt`, `updatedAt`のフィールドが追加されていて、`id`はプロパティにあるように主キーとして設定されている。ワイとしては`model:generate`のコマンドの時に属性として指定した`studentNo`を主キーにしたかったので、最終的に以下のようにマイグレーションファイルを変更する。

``` YYYYMMDDHHMMSS-create-student.js
'use strict';
module.exports = {
  async up(queryInterface, Sequelize) {
    await queryInterface.createTable('Student', {
      studentNo: {
        allowNull: false,
        primaryKey: true,
        type: Sequelize.STRING
      },
      firstName: {
        allowNull: false,
        type: Sequelize.STRING
      },
      lastName: {
        allowNull: false,
        type: Sequelize.STRING
      },
      deletedAt: {
        type: Sequelize.DATE
      },
      createdAt: {
        allowNull: false,
        type: Sequelize.DATE
      },
      updatedAt: {
        allowNull: false,
        type: Sequelize.DATE
      }
    });
  },
  async down(queryInterface, Sequelize) {
    await queryInterface.dropTable('Student');
  }
};
```
　さらに、マイグレーションファイルの変更に合わせてモデルのほうを以下のように変更する。

``` student.js
'use strict';
const {
    Model
} = require('sequelize');
module.exports = (sequelize, DataTypes) => {
    class Student extends Model {
        /**
        * Helper method for defining associations.
        * This method is not a part of Sequelize lifecycle.
        * The `models/index` file will call this method automatically.
        */
        static associate(models) {
            // define association here
        }
    }
    Student.init({
        studentNo: {
            type: DataTypes.STRING,
            primaryKey: true
        },
        firstName: {
            type: DataTypes.STRING,
            allowNull: false
        },
        lastName: {
            type: DataTypes.STRING,
            allowNull: false
        }
    }, {
        sequelize,
        modelName: 'Student',
        freezeTableName: true,
        paranoid: true
    });
    return Student;
};

```
ポイントとしては、
* `studentNo`に主キーとしての情報を追加した。
* モデル単独の変更として、上記`Student.init()`の第二引数のオブジェクトにプロパティを追加した。まず`freezeTableName: true`。これがないと、Sequelizeを使ったCRUD操作の際に、テーブル名を複数形に自動変換してSQL文を実行することになるため、モデル名とテーブル名を単数形で統一している場合、テーブル名が見つからずにエラーになる。次に`paranoid: true`。これがないと論理削除機能が使えないため、ここで追加しておく。
* `Student.init()`の第一引数では`createdAt`と`updatedAt`をあえて追加せず、逆に`deletedAt`を削除している。まず`createdAt`と`updatedAt`第二引数のオブジェクトで`timestamps`を`false`にしていない限り暗黙的に同プロパティの値は`true`と解釈され、第一引数に上記2つのフィールドを指定していなくてもDB側のテーブルにカラムとして存在してさえいれば、Sequelizeでレコードを取得してきた際の結果に同フィールドが含まれるようになる（`attribute`で結果を射影している場合は別）。さらに`paranoid: true`を追加したことで、`deletedAt`についてもDB側にカラムとして存在さえしていれば、モデルファイルに定義がなくてもレコード取得の際に同フィールドを含んで結果を返してくれる。

　ここで、なぜ今回自動生成された`createdAt`と`updatedAt`を消さずに採用しているかについて。上記の通り、`timestamps: false`を指定していない限りSequelizeはDBに上記2カラムがあるものとしてレコードを取得しようとするので、上記2つをマイグレーションファイルから消す場合、モデルにおいて`timestamps: false`を指定しなきゃいけないんだけど、そうすると今度は論理削除が機能しなくなるという致命的な問題が発生するため今回はカラムとして採用している。

　ところでモデルとは何かの説明なしで中身だけ変更しちゃったのでここで補足。モデルはDBのテーブルをJS側で扱うためのもので、JS的には[Model](https://sequelize.org/api/v6/class/src/model.js~model)クラスを拡張したクラスらしい。モデルの定義は今回はCLIが勝手に`Model.init()`としてやってくれてる（`sequelize.define('Model', {})`とする方法もあるけど、これも内部で`Model.init()`を実行しているからやってることは同じらしい）。

# テーブルの作成
　さて、これでテーブル（とモデル）の定義ができたんだけど、まだDBには`Student`テーブルができてないから、以下のコマンドでマイグレーションを実行する。

```
npx sequelize db:migrate
```
　これを実行してもテーブルが作成されない場合は、`config/config.json`に記載したDB情報とかが間違ってないか確認って感じで。さて、上記コマンドが成功するとDBに`Student`テーブルとともに`SequelizeMeta`というテーブルが作成される。現時点でのレコードは以下の通り。

|    |  name  |
| ---- | ---- |
|  1  |  YYYYMMDDHHMMSS-create-student.js  |

実行したコマンドとレコードの中身からも分かるように、`SequelizeMeta`はマイグレーション実行の履歴のためのテーブル。`npx sequelize db:migrate`が実行されると、`SequelizeMeta`のレコードに存在しない = まだ実行されていないマイグレーションファイルが実行されるという仕組みになっている。

ここで以下を実行すると、DB上で`Student`テーブルが削除され、`SequelizeMeta`テーブルでは先ほど見たレコードが消される。
```
npx sequelize db:migrate:undo
```


|    |  name  |
| ---- | ---- |

上記の`npx sequelize db:migrate:undo`は一番最後に実行したマイグレーションをなかったことにして元の状態に戻すコマンドであるのに対して、
```
npx sequelize db:migrate:undo:all
```
とすると今までのすべてのマイグレーションをなかったことにして初期状態に戻すことが出来るが、今回は１回しかマイグレーションを実行していないため、挙動としては同じになる。また、

```
npx sequelize-cli db:migrate:undo:all --to XXXXXXXXXXXXXX-create-student.js
```
のようにマイグレーションファイルを指定することで、指定したマイグレーションファイルを実行する前の状態に戻すことが出来る（例えば3つのマイグレーションファイルを実行した後、2つ目のマイグレーションファイルを`--to`で指定して`db:migrate:undo:all`した場合、2つ目と3つ目のマイグレーションファイルが実行される前の状態に戻る）。

# データの挿入準備
　さてこれでSequelize-cliを通して、モデルのフィールドをカラムとして持つテーブルをDBに作成するということが出来た。次はSequelize-cliを使ってDBにサンプルデータを挿入する方法（シーディングと呼ばれる）をみていく。まずは以下を実行し、シーディングのためのデータを記述するファイルを作成する。

```
npx sequelize seed:generate --name student
```
実行すると`seeders/YYYYMMDDHHMMSS-student.js`が作成される。

``` YYYYMMDDHHMMSS-student.js
'use strict';

module.exports = {
    async up (queryInterface, Sequelize) {
        /**
        * Add seed commands here.
        *
        * Example:
        * await queryInterface.bulkInsert('People', [{
            *   name: 'John Doe',
            *   isBetaMember: false
            * }], {});
            */
        },

        async down (queryInterface, Sequelize) {
            /**
            * Add commands to revert seed here.
            *
            * Example:
            * await queryInterface.bulkDelete('People', null, {});
            */
        }
    };
```
　上記のシードファイルを編集して、DBに挿入するサンプルデータを準備していく。
``` YYYYMMDDHHMMSS-student.js
'use strict';

const data = [{
    studentNo: '001',
    firstName: '翔平',
    lastName: '大谷'
},{
    studentNo: '002',
    firstName: '有',
    lastName: 'ダルビッシュ'
},{
    studentNo: '003',
    firstName: '雄星',
    lastName: '菊池'
},{
    studentNo: '004',
    firstName: '健太',
    lastName: '前田'
},{
    studentNo: '005',
    firstName: '拓一',
    lastName: '沢村'
}];

module.exports = {
    async up (queryInterface, Sequelize) {
        data.forEach((each) => {
            each.createdAt = new Date();
            each.updatedAt = new Date();
        });
        await queryInterface.bulkInsert('Student', data, {});
    },

    async down (queryInterface, Sequelize) {
        await queryInterface.bulkDelete('Student', null, {});
    }
};
```
ポイントとしては、
* 挿入するデータは`queryInterface.bulkInsert()`の第二引数にベタ書きしてもいいんだけど、データの量が多いと可読性が低下してしまうので、変数に定義してそれを代入する形にしている。
* 作成日時の`createdAt`は、このシードファイルを実行した日時がカラムの値として格納されるように、ここで`new Date()`を設定する。更新日時の`updatedAt`は、レコードの値が更新されると自動で`updatedAt`も更新されるから、データ挿入の際はほっといてもいいんだけど、マイグレーションファイルのほうで`allowNull: false`を設定しちゃってるので、こちらもとりあえず`new Date()`を設定しておく。ちなみにここらへんの情報が公式ドキュメントになく、CLIのissueのほうでひんしゅくを買っていたりもする（[ここらへん](https://github.com/sequelize/cli/issues/414#issuecomment-380857160)）。というか自分もこの情報を参考に値の設定を行っている。（実はモデルのほうで`createdAt`と`updatedAt`のプロパティとして`defaultValue: DataTypes.NOW`を設定しとけばシーディングの時に勝手に値を挿入してくれんじゃねって思って試したけどダメだった経緯がある…）

# データの挿入
　さて、シーディングの準備が出来たので、以下のコマンドを実行する。
```
npx sequelize db:seed:all
```

　これでDBの`Student`テーブルに、シードファイルで指定したデータが挿入される。
|    |  studentNo  |  firstName  |  lastName  |  deletedAt  |  creadtedAt  |  updatedAt  |
| ----  | ---- | ---- | ---- | ---- | ---- | ---- |
|  1  |  001  |  翔平  |  大谷  |  null  |  2022-XX-XX XX:XX:XX.XXX+09  |  2022-XX-XX XX:XX:XX.XXX+09  |
|  2  |  002  |  有  |  ダルビッシュ  |  null  |  2022-XX-XX XX:XX:XX.XXX+09  |  2022-XX-XX XX:XX:XX.XXX+09  |
|  3  |  003  |  雄星  |  菊池  |  null  |  2022-XX-XX XX:XX:XX.XXX+09  |  2022-XX-XX XX:XX:XX.XXX+09  |
|  4  |  004  |  健太  |  前田  |  null  |  2022-XX-XX XX:XX:XX.XXX+09  |  2022-XX-XX XX:XX:XX.XXX+09  |
|  5  |  005  |  拓一  |  沢村  |  null  |  2022-XX-XX XX:XX:XX.XXX+09  |  2022-XX-XX XX:XX:XX.XXX+09  |

（`createdAt`と`updatedAt`はマスキングしてるンゴ）

実行したシードデータをDBから削除したい場合は、以下の3つのコマンドが使える。

1.一番最後に実行したシードデータを削除する
```
npx sequelize db:seed:undo
```

2.指定したシードデータを削除する
```
npx sequelize db:seed:undo --seed YYYYMMDDHHMMSS-student.js
```

3.`/seeders/`以下のすべてのシードデータを削除する
```
npx sequelize db:seed:undo:all
```

　これで、Sequelize-cliを使ってDB上にテーブルを作成し、そこにレコード（シードデータ）を挿入することが出来た。キリがいいので今回はここまで。今回の続きは[こちら](https://qiita.com/furi-kake/items/58916af562fe74c82e06)