Raspberry Pi 4 的後端修鍊 (8) - migration 要 foreign key

  1. Raspberry Pi 4 的後端修鍊 (8) - migration 要 foreign key
    1. 「讀者」資料表
    2. 想要知道的問題
    3. 執行 migrate
    4. 執行後才知道錯
    5. 心得
    6. 尚未做的

Raspberry Pi 4 的後端修鍊 (8) - migration 要 foreign key

「讀者」資料表

CREATE TABLE reader (
id CHAR(10) NOT NULL,
password VARCHAR(128),
slack_id VARCHAR(50),
line_id VARCHAR(50),
phone CHAR(10),
email VARCHAR(50),
PRIMARY KEY (id),
created_at DATETIME,
updated_at DATETIME,
creator_id INT NOT NULL,
FOREIGN KEY (creator_id) REFERENCES user (id)
) ENGINE = InnoDB;

在加入之前,我想先增加 npm script 的區段,將常用的指令加上去

"scripts": {
//...
"new:table": "npx sequelize model:generate --underscored --force",
"migrate": "npx sequelize db:migrate",
"migrate:undo": "npx sequelize db:migrate:undo",
//...
},

接著輸入指令,訊息會印出原本的指令長怎樣。

$ npm run new:table -- --name reader --attributes password:string,slack_id:string,line_id:string,phone:string,email:string,creator:integer

> lib-backend@1.0.0 new:table
> npx sequelize model:generate --underscored --force --name reader --attributes password:string,slack_id:string,line_id:string,phone:string,email:string,creator:integer


Sequelize CLI [Node: 18.16.0, CLI: 6.6.1, ORM: 6.32.1]

New model was created at /home/admin/case/lib-backend/models/reader.js .
New migration was created at /home/admin/case/lib-backend/migrations/20230912142904-create-reader.js .

migrations/20230912142904-create-reader.js

  • 修改表格名稱,readers -> reader (讓它與真正生成的表格名稱相同)
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('reader', {
id: {
allowNull: false,
autoIncrement: false, // 不自動跳號
primaryKey: true,
type: Sequelize.CHAR(10) // 身份證字號
},
password: {
type: Sequelize.STRING(128),
defaultValue: '000', // 加入預設值
},
slack_id: {
type: Sequelize.STRING(50)
},
line_id: {
type: Sequelize.STRING(50)
},
phone: {
allowNull: false,
type: Sequelize.CHAR(10)
},
email: {
allowNull: false,
type: Sequelize.STRING(50)
},
creator_id: {
allowNull: false,
type: Sequelize.INTEGER,
// foreign key 的設定
references: {
model: 'user',
key: 'id'
},
},
created_at: {
allowNull: false,
type: Sequelize.DATE
},
updated_at: {
allowNull: false,
type: Sequelize.DATE
}
});
},
async down(queryInterface, Sequelize) {
await queryInterface.dropTable('reader');
}
};

models/reader.js

'use strict';
const {
Model
} = require('sequelize');
module.exports = (sequelize, DataTypes) => {
class reader 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
}
}
reader.init({
id: DataTypes.CHAR(10),
password: DataTypes.STRING,
slack_id: DataTypes.STRING,
line_id: DataTypes.STRING,
phone: DataTypes.STRING,
email: DataTypes.STRING,
creator_id: {
DataTypes: DataTypes.INTEGER,
references: {
// This is a reference to another model
model: user,
// This is the column name of the referenced model
key: 'id',
}
},
}, {
sequelize,
modelName: 'reader',
underscored: true,
freezeTableName: true, // 請手動加入這一行
});
return reader;
};

想要知道的問題

  • 預設值 DefaultValue 應該加在哪?migration?model?都加?
  • foreign key 的設定 怎麼加?

執行 migrate

$ npm run migrate

> lib-backend@1.0.0 migrate
> npx sequelize db:migrate


Sequelize CLI [Node: 18.16.0, CLI: 6.6.1, ORM: 6.32.1]

Loaded configuration file "config/config.json".
Using environment "development".
Executing (default): SELECT 1+1 AS result
Executing (default): SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE' AND TABLE_SCHEMA = 'good_ideas_lib_dev';
Executing (default): SHOW FULL COLUMNS FROM `SequelizeMeta`;
Executing (default): SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE' AND TABLE_NAME = 'SequelizeMeta' AND TABLE_SCHEMA = 'good_ideas_lib_dev'
Executing (default): SHOW INDEX FROM `SequelizeMeta`
Executing (default): SELECT `name` FROM `SequelizeMeta` AS `SequelizeMeta` ORDER BY `SequelizeMeta`.`name` ASC;
Executing (default): SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE' AND TABLE_NAME = 'SequelizeMeta' AND TABLE_SCHEMA = 'good_ideas_lib_dev'
Executing (default): SHOW INDEX FROM `SequelizeMeta`
Executing (default): SELECT `name` FROM `SequelizeMeta` AS `SequelizeMeta` ORDER BY `SequelizeMeta`.`name` ASC;
Executing (default): SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE' AND TABLE_NAME = 'SequelizeMeta' AND TABLE_SCHEMA = 'good_ideas_lib_dev'
Executing (default): SHOW INDEX FROM `SequelizeMeta`
Executing (default): SELECT `name` FROM `SequelizeMeta` AS `SequelizeMeta` ORDER BY `SequelizeMeta`.`name` ASC;
== 20230912142904-create-reader: migrating =======
Executing (default): CREATE TABLE IF NOT EXISTS `reader` (`id` CHAR(10) NOT NULL , `password` VARCHAR(128) DEFAULT '000', `slack_id` VARCHAR(50), `line_id` VARCHAR(50), `phone` CHAR(10) NOT NULL, `email` VARCHAR(50) NOT NULL, `creator_id` INTEGER NOT NULL, `created_at` DATETIME NOT NULL, `updated_at` DATETIME NOT NULL, PRIMARY KEY (`id`), FOREIGN KEY (`creator_id`) REFERENCES `user` (`id`)) ENGINE=InnoDB;
Executing (default): SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE' AND TABLE_NAME = 'SequelizeMeta' AND TABLE_SCHEMA = 'good_ideas_lib_dev'
Executing (default): SHOW INDEX FROM `SequelizeMeta`
Executing (default): INSERT INTO `SequelizeMeta` (`name`) VALUES (?); "20230912142904-create-reader.js"
== 20230912142904-create-reader: migrated (0.060s)
CREATE TABLE IF NOT EXISTS `reader` (
`id` CHAR(10) NOT NULL ,
`password` VARCHAR(128) DEFAULT '000',
`slack_id` VARCHAR(50),
`line_id` VARCHAR(50),
`phone` CHAR(10) NOT NULL,
`email` VARCHAR(50) NOT NULL,
`creator_id` INTEGER NOT NULL,
`created_at` DATETIME NOT NULL,
`updated_at` DATETIME NOT NULL,
PRIMARY KEY (`id`),
FOREIGN KEY (`creator_id`) REFERENCES `user` (`id`)
) ENGINE=InnoDB;

比對一下原本想要的 SQL,除了後來覺得需要加的部份,其餘果然和原本想的一模一樣。讚!

CREATE TABLE reader (
id CHAR(10) NOT NULL,
password VARCHAR(128),
slack_id VARCHAR(50),
line_id VARCHAR(50),
phone CHAR(10),
email VARCHAR(50),
PRIMARY KEY (id),
created_at DATETIME,
updated_at DATETIME,
creator_id INT NOT NULL,
FOREIGN KEY (creator_id) REFERENCES user (id)
) ENGINE = InnoDB;

執行後才知道錯

寫了一個程式使用 model

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

async function main() {
const development = {
//...
}
// Option 3: Passing parameters separately (other dialects)
const sequelize = new Sequelize(...);

// 引用參考 (不用管順序)
require('./models/reader')(sequelize, DataTypes);
require('./models/user')(sequelize, DataTypes);

try {
await sequelize.authenticate();
} catch (error) {
console.error('Unable to connect to the database:', error);
}

try {
const chris = await sequelize.models.user.create({ name: 'chris', password: 'chris' });
await sequelize.models.reader.create({
name: 'mary',
phone: '0987654321',
email: 'mary@gmail.com',
creator_id: chris.id,
created_at: Date.now(),
updated_at: Date.now(),
});

console.log('Connection has been established successfully.');
} catch (error) {
console.log(error.message);
}
}
main()

執行之後,各種錯誤,再重新修改 model 的表示方式。

models/user.js

// ...
module.exports = (sequelize, DataTypes) => {
class user extends Model {
// ...
}
user.init({
// 加上 id 的定義,要給別人 reference 成 foreign key 用的
id: {
type: DataTypes.INTEGER, // 型別要正確
primaryKey: true // 要設定 primaryKey: true
},
name: DataTypes.STRING(50),
password: DataTypes.CHAR(128)
}, {
// ...
});
return user;
};

models/reader.js

'use strict';
const {
Model
} = require('sequelize');
const user = require('./user'); // 為了 foreign keys
module.exports = (sequelize, DataTypes) => {
class reader 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
}
}
reader.init({
// 加上 id 的定義,要給別人 reference 成 foreign key 用的
id: {
type: DataTypes.CHAR(10), // 型別要正確
primaryKey: true // 要設定 primaryKey: true
},
password: DataTypes.STRING,
slack_id: DataTypes.STRING,
line_id: DataTypes.STRING,
phone: DataTypes.STRING,
email: DataTypes.STRING,
creator_id: {
type: DataTypes.INTEGER,
// foreign keys 的設定
references: {
// This is a reference to another model
model: user,
// This is the column name of the referenced model
key: 'id',
}
},
}, {
sequelize,
modelName: 'reader',
underscored: true,
freezeTableName: true, // 請手動加入這一行
});
return reader;
};

設定之後,執行

$ node main.js 
Executing (default): SELECT 1+1 AS result
Executing (default): INSERT INTO `user` (`id`,`name`,`password`,`created_at`,`updated_at`) VALUES (?,?,?,?,?); null, "chris", "chris", "2023-09-12 15:29:09", "2023-09-12 15:29:09"
Executing (default): INSERT INTO `reader` (`id`,`phone`,`email`,`creator_id`,`created_at`,`updated_at`) VALUES (?,?,?,?,?,?); null, "0987654321", "mary@gmail.com", null, "2023-09-12 15:29:09", "2023-09-12 15:29:09"
Column 'id' cannot be null

修改一下 main.js

await sequelize.models.user.create({ name: 'chris', password: 'chris' });

// 要將 user 的資料從資料庫再撈一次,才會是包含所有在資料庫裡的內容。而不是只有在 JS 出現的而已。
const chris = await sequelize.models.user.findOne({
where: { name: 'chris' }
})

const mary = await sequelize.models.reader.create({
id: 'E123456789',
name: 'mary',
phone: '0987654321',
email: 'mary@gmail.com',
creator_id: chris.id,
created_at: Date.now(),
updated_at: Date.now(),
});

console.log('mary.password', mary.password)
$ node main.js 
Executing (default): SELECT 1+1 AS result
Executing (default): INSERT INTO `user` (`id`,`name`,`password`,`created_at`,`updated_at`) VALUES (?,?,?,?,?); null, "chris", "chris", "2023-09-12 15:41:46", "2023-09-12 15:41:46"
Executing (default): SELECT `id`, `name`, `password`, `created_at` AS `createdAt`, `updated_at` AS `updatedAt` FROM `user` AS `user` WHERE `user`.`name` = 'chris' LIMIT 1;
Executing (default): INSERT INTO `reader` (`id`,`phone`,`email`,`creator_id`,`created_at`,`updated_at`) VALUES (?,?,?,?,?,?); "E123456789", "0987654321", "mary@gmail.com", 18, "2023-09-12 15:41:46", "2023-09-12 15:41:46"
mary.password undefined
Connection has been established successfully.

可以發現 mary.password 是 undefined 也是相同的道理。

改一下 Model 的 password,加上 DefaultValue ,其實也是一樣的結果。

心得

Model 主要還是 JS 餵進去的資料為新增時回傳的物件。
並不會依照資料庫 (或 Model) 的預設值而自動幫你加上去,id 也是
除非在新增完之後,再從資料庫撈一次,該在資料表上的內容就會出現在 JS 物件中了。

而 sequelize 在 Model 上可以設定 DefaultValue 的設定,是為了搭配 .sync() 吧?從 Model 新增 Table 的方式。

尚未做的

未設定 Model 之間的關聯性

所以 console.log(mary.toJSON()) 印出來的 mary 會是這樣

{
id: 'E123456789',
phone: '0987654321',
email: 'mary@gmail.com',
creator_id: 19,
updatedAt: 2023-09-12T15:48:58.828Z,
createdAt: 2023-09-12T15:48:58.828Z
}