Sequelize 透過 Migration 來修改 Table 資訊

花了一點時間才理解這個流程。

簡述

不知道你有沒有和我一樣,曾經想要對 Table 新增欄位,所以就直接這樣子做:

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
// 20220331071729-create-category.js
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('Categories', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
name: {
type: Sequelize.STRING
},
isDeleted: { // <- 新增這個欄位
type: Sequelize.BOOLEAN
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
},
async down(queryInterface, Sequelize) {
await queryInterface.dropTable('Categories');
}
};

這時候你去執行 npx sequelize-cli db:migrate 會發現沒有用:

1
2
3
Loaded configuration file "config\config.json".
Using environment "development".
No migrations were executed, database schema was already up to date.

這種時候該怎麼辦?你有兩條路可以走:

  1. 執行 db:migrate:undo,接著再重新跑一次 db:migrate
  2. 執行 db:migration:create,寫一個新的 migration 來處理

這兩個差在第一種會直接把 Table Drop 掉(通常是這樣,但實際上看內容決定),你原本存的資料就直接沒了,而第二種就是為了避免這個問題的解決方案,也是我這篇要講的解法。

假設我目前的 Table 長這樣:

table-before

現在我想對新增一個 isDeleted 欄位,所以就照剛剛說的,我先下指令來生一個新的 Migration 出來:

1
2
# 命名慣例最好是 update create add .. 之類的當作開頭
sequelize migration:create --name 'add-isDeleted-to-category'

接著產生的檔案內容會是這樣:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
'use strict';

module.exports = {
async up (queryInterface, Sequelize) {
/**
* Add altering commands here.
*
* Example:
* await queryInterface.createTable('users', { id: Sequelize.INTEGER });
*/
},

async down (queryInterface, Sequelize) {
/**
* Add reverting commands here.
*
* Example:
* await queryInterface.dropTable('users');
*/
}
};

簡單來說,就是在裡面寫你想執行的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
'use strict';

module.exports = {
async up (queryInterface, Sequelize) {
// 用 addColumn 新增
await queryInterface.addColumn(
'Categories', // table name
'isDeleted', // new field name
// dataType
{
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: false
}
)
},

async down (queryInterface, Sequelize) {
// 用 removeColumn 新增
await queryInterface.removeColumn('Categories', 'isDeleted');
}
};

記得 updown 之間要有對應關係,我在 up 裡做新增,down 就應該該做移除,這樣之後跑指令的時才不會出錯。

接下來就跟一開始一樣,下指令去跑 migration:

1
npx sequelize-cli  db:migrate

這樣就完美的保留資料,也新增了欄位,可喜可賀,可喜可賀。

table-after

之後如果又不想要這個欄位的話也很簡單,只要執行:

1
npx sequelize-cli  db:migrate:undo --name 20220331082106-add-isDeleted-to-category.js

就大功告成啦。

但是這樣還沒有完成,還有一件事情得做

故意用這麼大的字就是因為這件事很重要,絕對不能忘記。

總而言之,不要忘記 Model 的存在,當你用 JS 來操作資料庫時都是透過 Model 來執行的,所以雖然你剛剛透過 Migration 在 Table 新增了欄位,但這並不代表 Model 的資料結構會跟著自動更新,所以請務必記得:

  • 跑完 Migration 後要再去重新設定 Model 的資料結構
  • 跑完 Migration 後要再去重新設定 Model 的資料結構
  • 跑完 Migration 後要再去重新設定 Model 的資料結構

這很重要,真的。

如果你沒有這樣做的話,就會有這個問題:

1
2
3
4
5
6
7
const { Category } = require('./models');

~async function () {
// 撈出所有資料
const categories = await Category.findAll();
console.log('result', JSON.stringify(categories, null, 4));
}()

輸出結果:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 沒有把 isDeleted 撈出來
Executing (default): SELECT `id`, `name`, `createdAt`, `updatedAt` FROM `Categor
ies` AS `Category`;

result [
{
"id": 1,
"name": "JavaScript",
"createdAt": "2021-03-31T00:00:00.000Z",
"updatedAt": "2021-03-31T00:00:00.000Z"
// 消失的 isDeleted
}
]

所以再說一次,記得去改 Model 的資料結構:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
'use strict';
const {
Model
} = require('sequelize');
module.exports = (sequelize, DataTypes) => {
class Category extends Model {
static associate(models) {
Category.hasMany(models.Post);
}
}
Category.init({
name: DataTypes.STRING,
isDeleted: DataTypes.BOOLEAN, // <- 補上剛剛加的欄位
}, {
sequelize,
modelName: 'Category',
});
return Category;
};

一定要確保 Model 跟資料庫的狀態是同步的,在用的時候才不會出問題。

附註

  1. 如果有改 Migration 的名稱的話,記得到資料庫的 sequelizemeta 把原本的名稱也改掉,不然執行指令時會出錯。

  2. 想知道其他操作可以參考 這篇,主要就是換個 method 來執行而已,基本結構都大同小異。

mentor-program-day108 重新理解 Sequelize 中的 Migration 和 Model
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×