使用Sequelize進行資料庫migration的基本用法

SQL資料庫

開發時大家都會使用git為程式碼做版控,好讓不同開發環境以及佈署環境的程式得以統一。今天我們來體驗一下一個叫Sequelize的DB migration工具來為我們的DB schema做版控。

把DB schema版控起來最大的好處是DB schema就可以像程式碼一樣在各個環境都是統一的,不會發生在stg環境驗完測完沒問題的程式因為上到prod環境時忘記加欄位導致prod環境的錯誤狂噴。再來是對於新加入專案的開發人員非常友好,因為開發人員只要使用migration工具下完up指令就可以將本機的資料庫建好很快就可以進入開發,甚至也可以指定版本建出幾天前的舊版DB schema。那接下來就進入我們今天的主題吧。

目錄

  1. 安裝sequelize-cli
  2. 設定sequelize-cli連線
  3. 建立一個新的migration A
  4. 再建立一個新的migration B
  5. 指定執行至第一個migration A就好
  6. 執行完所有migration至最新
  7. 退版撤掉剛剛的migration B
  8. 退版撤掉所有migration
  9. 總結

第1步:安裝sequelize-cli

本篇我們使用的migration工具叫Sequelize,他其實也是Node.js的ORM套件,並且支援主流的Oracle、Postgres、MySQL、MariaDB、SQLite跟SQL Server等等的DB。首先先安裝Sequelize對應使用到的DB驅動與sequelize-cli,我們專案使用的是Postgres所以會裝pgpg-hstore,如果是MySQL的話則是mysql2

npm install --save pg pg-hstore sequelize
npm install --save-dev sequelize-cli

完畢後試試看Sequelize是否正常運作如下。

npx sequelize-cli --help

這些是Sequelize可以使用的指令集。

Sequelize CLI [Node: 18.0.0, CLI: 6.4.1, ORM: 6.21.3]

sequelize-cli <command>

Commands:
  sequelize db:migrate                        Run pending migrations
  sequelize db:migrate:schema:timestamps:add  Update migration table to have timestamps
  sequelize db:migrate:status                 List the status of all migrations
  sequelize db:migrate:undo                   Reverts a migration
  sequelize db:migrate:undo:all               Revert all migrations ran
  sequelize db:seed                           Run specified seeder
  sequelize db:seed:undo                      Deletes data from the database
  sequelize db:seed:all                       Run every seeder
  sequelize db:seed:undo:all                  Deletes data from the database
  sequelize db:create                         Create database specified by configuration
  sequelize db:drop                           Drop database specified by configuration
  sequelize init                              Initializes project
  sequelize init:config                       Initializes configuration
  sequelize init:migrations                   Initializes migrations
  sequelize init:models                       Initializes models
  sequelize init:seeders                      Initializes seeders
  sequelize migration:generate                Generates a new migration file
  sequelize migration:create                  Generates a new migration file
  sequelize model:generate                    Generates a model and its migration
  sequelize model:create                      Generates a model and its migration
  sequelize seed:generate                     Generates a new seed file
  sequelize seed:create                       Generates a new seed file

Options:
  --version  Show version number
  --help     Show help

第2步:設定sequelize-cli連線

Sequelize的連線資訊預設會放在config/config.json內,格式如下,再根據自己的設定修改。如果是個新專案還沒有config/config.json以及migrations/models/資料夾的話,需要先執行npx sequelize init初始化Sequelize。

{
  "development": {
    "username": "root",
    "password": 123456,
    "database": "database_development",
    "host": "127.0.0.1",
    "dialect": "postgres"
  },
  "test": {
    "username": "root",
    "password": 123456,
    "database": "database_test",
    "host": "127.0.0.1",
    "dialect": "postgres"
  },
  "production": {
    "username": "root",
    "password": 123456,
    "database": "database_production",
    "host": "127.0.0.1",
    "dialect": "postgres"
  }
}

第3步:建立一個新的migration A

一個migration需要實作up()down()達成升版跟退版目的。使用以下指令讓Sequelize為我們生成up()down()的骨架。

npx sequelize-cli migration:generate --name init-hell-world

回到migrations/資料夾,可以看到多了個20220802145931-init-hell-world.jsmigration檔案,並且骨架已經填好如下。

'use strict';

module.exports = {
  async up (queryInterface, Sequelize) {
    /** Add altering commands here. */
  },

  async down (queryInterface, Sequelize) {
    /** Add reverting commands here. */
  }
};

修改一下檔案,讓我們來創立一個叫users的表吧。

'use strict';

module.exports = {
  async up (queryInterface, Sequelize) {
    await queryInterface.createTable('users', {
        id: Sequelize.INTEGER,
        gender: Sequelize.INTEGER,
        name: Sequelize.STRING(100),
    });
  },

  async down (queryInterface, Sequelize) {
    await queryInterface.dropTable('users');
  }
};

編輯後存檔,我們已經完成了第一個migration了~

第4步:再建立一個新的migration B

為了擬真實驗Sequelize手動上版的功能,我們再來新增一個migration吧。

npx sequelize-cli migration:generate --name add-column-address-to-user-table

打開migrations/20220802151637-add-column-address-to-user-table.js,這次為migration A新增出來的表新增地址的欄位。

'use strict';

module.exports = {
  async up (queryInterface, Sequelize) {
    await queryInterface.addColumn(
        'users',
        'address',
        { type: Sequelize.STRING },
    );
  },

  async down (queryInterface, Sequelize) {
    await queryInterface.removeColumn('users', 'address');
  }
};

到目前為止,我們已經有兩個migration待執行了,可以使用以下指令查看所有migration的狀態。

npx sequelize-cli db:migrate:status

輸出如下:

Sequelize CLI [Node: 18.0.0, CLI: 6.4.1, ORM: 6.21.3]

Loaded configuration file "config/config.json".
Using environment "development".
down 20220802145931-init-hell-world.js
down 20220802151637-add-column-address-to-user-table.js

跟預期的一樣,目前兩個migration是down的狀態。

第5步:指定執行至第一個migration A就好

我們來小試一下如果想手動跑migration至指定的版本,可以使用以下指令。

npx sequelize-cli db:migrate --to 20220802145931-init-hell-world.js

輸出:

Sequelize CLI [Node: 18.0.0, CLI: 6.4.1, ORM: 6.21.3]

Loaded configuration file "config/config.json".
Using environment "development".
== 20220802145931-init-hell-world: migrating =======
== 20220802145931-init-hell-world: migrated (0.042s)

接著我們到DB看一下table的狀態,users是我們migration A預期產出的表沒錯。

至於SequelizeMeta則是Sequelize自動生成用於幫助他記錄他已經執行過哪些migration用的,可以看到我們的第一個migration A已經被記錄在table內了。

此時如果再下一次指令查看所有migration的狀態。

npx sequelize-cli db:migrate:status

可以看到migration A已經是up的狀態。

Sequelize CLI [Node: 18.0.0, CLI: 6.4.1, ORM: 6.21.3]

Loaded configuration file "config/config.json".
Using environment "development".
up 20220802145931-init-hell-world.js
down 20220802151637-add-column-address-to-user-table.js

第6步:執行完所有migration至最新

剛剛我們讓Sequelize跑migration至指定的版本,但假如今天積了很多migration要跑、或正在部署一個新環境,希望一次跑完所有migration至最新的狀態,我們可以使用剛剛的指令,但是不要帶參數to

npx sequelize-cli db:migrate

輸出:

Sequelize CLI [Node: 18.0.0, CLI: 6.4.1, ORM: 6.21.3]

Loaded configuration file "config/config.json".
Using environment "development".
== 20220802151637-add-column-address-to-user-table: migrating =======
== 20220802151637-add-column-address-to-user-table: migrated (0.021s)

回到DB看一下table的狀態,users果真被加上了address欄位。

SequelizeMeta也多了剛剛跑完的記錄。

目前兩個migration確實都是up的狀態。

npx sequelize-cli db:migrate:status
Sequelize CLI [Node: 18.0.0, CLI: 6.4.1, ORM: 6.21.3]

Loaded configuration file "config/config.json".
Using environment "development".
up 20220802145931-init-hell-world.js
up 20220802151637-add-column-address-to-user-table.js

第7步:退版撤掉剛剛的migration B

上版上到一半發現程式有bug不想上了,身為工程師的我們希望撤掉最近一次執行的migration可以使用以下指令。

npx sequelize-cli db:migrate:undo

輸出:

Sequelize CLI [Node: 18.0.0, CLI: 6.4.1, ORM: 6.21.3]

Loaded configuration file "config/config.json".
Using environment "development".
== 20220802151637-add-column-address-to-user-table: reverting =======
== 20220802151637-add-column-address-to-user-table: reverted (0.021s)

或帶上參數to指定要退版(包含參數的版本)至migration A。

npx sequelize-cli db:migrate:undo:all --to 20220802151637-add-column-address-to-user-table.js

輸出:

Sequelize CLI [Node: 18.0.0, CLI: 6.4.1, ORM: 6.21.3]

Loaded configuration file "config/config.json".
Using environment "development".
== 20220802151637-add-column-address-to-user-table: reverting =======
== 20220802151637-add-column-address-to-user-table: reverted (0.027s)

產圖好累,就相信我他有跑就對了,或去DB眼見為憑。

打指令看一下migration的狀態,跟預期的一樣很乖。

npx sequelize-cli db:migrate:status
Sequelize CLI [Node: 18.0.0, CLI: 6.4.1, ORM: 6.21.3]

Loaded configuration file "config/config.json".
Using environment "development".
up 20220802145931-init-hell-world.js
down 20220802151637-add-column-address-to-user-table.js

第8步:退版撤掉所有migration

當開發完一個微服務想將在本機DB建的schema清掉時,可以使用以下指令,將會撤掉本機DB的migration。

npx sequelize-cli db:migrate:undo:all

輸出:

Sequelize CLI [Node: 18.0.0, CLI: 6.4.1, ORM: 6.21.3]

Loaded configuration file "config/config.json".
Using environment "development".
== 20220802151637-add-column-address-to-user-table: reverting =======
== 20220802151637-add-column-address-to-user-table: reverted (0.020s)

== 20220802145931-init-hell-world: reverting =======
== 20220802145931-init-hell-world: reverted (0.016s)

請千萬小心在production誤下此操作,可能會需要在被發現前收拾行李搬家。

第9步:總結

到這裡,我們已經學會使用Sequelize這個DB migration工具來為DB schema進行版控了。當然,凡事不只有優點也會有缺點。使用migration工具時,由於異動欄位是屬於DDL的語句,有些資料庫像是MySQL底層會對異動的表整表加exclusive lock,此時其他DML操作都會blocking直到當個指令調整的schema完畢。由於此操作對不離線更新的service有比較高的危險性,所以比較有規模有DBA的公司對於prod環境還是會傾向手動一條一條DDL指令去下,確保每條DDL都是他親眼看完跑完無誤的,也就不太會依賴migration工具。當然這就是個取捨,如果專案屬於微服務的性質,使用migration工具就會有他的方便所在。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *