Sequelize建立model及資料庫配置
直接使用Sequelize雖然可以,但是存在一些問題。團隊開發時,有人喜歡自己加timestamp,有人又喜歡自增主鍵,並且自定義表名。一個大型Web App通常都有幾十個對映表,一個對映表就是一個Model。如果按照各自喜好,那業務程式碼就不好寫。Model不統一,很多程式碼也無法複用。所以我們需要一個統一的模型,強迫所有Model都遵守同一個規範,這樣不但實現簡單,而且容易統一風格。
一、Model
我們首先要定義的就是Model存放的資料夾必須在models
內,並且以Model名字命名,例如:Pet.js
,User.js
等等。
其次,每個Model必須遵守一套規範:
- 統一主鍵,名稱必須是
id
STRING(50)
; - 主鍵可以自己指定,也可以由框架自動生成(如果為null或undefined);
- 所有欄位預設為
NOT NULL
,除非顯式指定; - 統一timestamp機制,每個Model必須有
createdAt
、updatedAt
和version
,分別記錄建立時間、修改時間和版本號。其中,createdAt
和updatedAt
以BIGINT
儲存時間戳,最大的好處是無需處理時區,排序方便。version
每次修改時自增。
所以,我們不要直接使用Sequelize的API,而是通過db.js
間接地定義Model。例如,User.js
應該定義如下:
const db = require(' ../db');
module.exports = db.defineModel('users', {
email: {
type: db.STRING(100),
unique: true
},
passwd: db.STRING(100),
name: db.STRING(100),
gender: db.BOOLEAN
});
這樣,User就具有email
、passwd
、name
和gender
這4個業務欄位。id
、createdAt
、updatedAt
和version
應該自動加上,而不是每個Model都去重複定義。
所以,db.js
const Sequelize = require('sequelize');
var sequelize = new Sequelize('dbname', 'username', 'password', {
host: 'localhost',
dialect: 'mysql',
pool: {
max: 5,
min: 0,
idle: 10000
}
});
const ID_TYPE = Sequelize.STRING(50);
function defineModel(name, attributes) {
var attrs = {};
for (let key in attributes) {
let value = attributes[key];
if (typeof value === 'object' && value['type']) {
value.allowNull = value.allowNull || false;
attrs[key] = value;
} else {
attrs[key] = {
type: value,
allowNull: false
};
}
}
attrs.id = {
type: ID_TYPE,
primaryKey: true
};
attrs.createdAt = {
type: Sequelize.BIGINT,
allowNull: false
};
attrs.updatedAt = {
type: Sequelize.BIGINT,
allowNull: false
};
attrs.version = {
type: Sequelize.BIGINT,
allowNull: false
};
return sequelize.define(name, attrs, {
tableName: name,
timestamps: false,
hooks: {
beforeValidate: function (obj) {
let now = Date.now();
if (obj.isNewRecord) {
if (!obj.id) {
obj.id = generateId();
}
obj.createdAt = now;
obj.updatedAt = now;
obj.version = 0;
} else {
obj.updatedAt = Date.now();
obj.version++;
}
}
}
});
}
我們定義的defineModel
就是為了強制實現上述規則。
Sequelize在建立、修改Entity時會呼叫我們指定的函式,這些函式通過hooks
在定義Model時設定。我們在beforeValidate
這個事件中根據是否是isNewRecord
設定主鍵(如果主鍵為null
或undefined
)、設定時間戳和版本號。
這麼一來,Model定義的時候就可以大大簡化。
二、資料庫配置
接下來,我們把簡單的config.js
拆成3個配置檔案:
- config-default.js:儲存預設的配置;
- config-override.js:儲存特定的配置;
- config-test.js:儲存用於測試的配置。
例如,預設的config-default.js
可以配置如下:
var config = {
dialect: 'mysql',
database: 'nodejs',
username: 'www',
password: 'www',
host: 'localhost',
port: 3306
};
module.exports = config;
而config-override.js
可應用實際配置:
var config = {
database: 'production',
username: 'www',
password: 'secret-password',
host: '192.168.1.199'
};
module.exports = config;
config-test.js
可應用測試環境的配置:
var config = {
database: 'test'
};
module.exports = config;
讀取配置的時候,我們用config.js
實現不同環境讀取不同的配置檔案:
const defaultConfig = './config-default.js';
// 可設定為絕對路徑,如 /opt/product/config-override.js
const overrideConfig = './config-override.js';
const testConfig = './config-test.js';
const fs = require('fs');
var config = null;
if (process.env.NODE_ENV === 'test') {
console.log(`Load ${testConfig}...`);
config = require(testConfig);
} else {
console.log(`Load ${defaultConfig}...`);
config = require(defaultConfig);
try {
if (fs.statSync(overrideConfig).isFile()) {
console.log(`Load ${overrideConfig}...`);
config = Object.assign(config, require(overrideConfig));
}
} catch (err) {
console.log(`Cannot load ${overrideConfig}.`);
}
}
module.exports = config;
具體的規則是:
- 先讀取
config-default.js
; - 如果不是測試環境,就讀取
config-override.js
,如果檔案不存在,就忽略。 - 如果是測試環境,就讀取
config-test.js
。
這樣做的好處是,開發環境下,團隊統一使用預設的配置,並且無需config-override.js
。
部署到伺服器時,由運維團隊配置好config-override.js
,以覆蓋config-override.js
的預設設定。
測試環境下,本地和CI伺服器統一使用config-test.js
,測試資料庫可以反覆清空,不會影響開發。
配置檔案表面上寫起來很容易,但是,既要保證開發效率,又要避免伺服器配置檔案洩漏,還要能方便地執行測試,就需要一開始搭建出好的結構,才能提升工程能力。
三、使用Model
要使用Model,就需要引入對應的Model檔案,例如:User.js
。一旦Model多了起來,如何引用也是一件麻煩事。自動化永遠比手工做效率高,而且更可靠。我們寫一個model.js
,自動掃描並匯入所有Model:
const fs = require('fs');
const db = require('./db');
let files = fs.readdirSync(__dirname + '/models');
let js_files = files.filter((f)=>{
return f.endsWith('.js');
}, files);
module.exports = {};
for (let f of js_files) {
console.log(`import model from file ${f}...`);
let name = f.substring(0, f.length - 3);
module.exports[name] = require(__dirname + '/models/' + f);
}
module.exports.sync = () => {
db.sync();
};
這樣,需要用的時候,寫起來就像這樣:
const model = require('./model');
let
Pet = model.Pet,
User = model.User;
var pet = await Pet.create({ ... });
最終,我們建立的工程model-sequelize
結構如下:
model-sequelize/
|
+- .vscode/
| |
| +- launch.json <-- VSCode 配置檔案
|
+- models/ <-- 存放所有Model
| |
| +- Pet.js <-- Pet
| |
| +- User.js <-- User
|
+- config.js <-- 配置檔案入口
|
+- config-default.js <-- 預設配置檔案
|
+- config-test.js <-- 測試配置檔案
|
+- db.js <-- 如何定義Model
|
+- model.js <-- 如何匯入Model
|
+- init-db.js <-- 初始化資料庫
|
+- app.js <-- 業務程式碼
|
+- package.json <-- 專案描述檔案
|
+- node_modules/ <-- npm安裝的所有依賴包
注意到我們其實不需要建立表的SQL,因為Sequelize提供了一個sync()
方法,可以自動建立資料庫。這個功能在開發和生產環境中沒有什麼用,但是在測試環境中非常有用。測試時,我們可以用sync()
方法自動創建出表結構,而不是自己維護SQL指令碼。這樣,可以隨時修改Model的定義,並立刻執行測試。開發環境下,首次使用sync()
也可以自動創建出表結構,避免了手動執行SQL的問題。
init-db.js
的程式碼非常簡單:它最大的好處是避免了手動維護一個SQL指令碼。
const model = require('./model.js');
model.sync();
console.log('init db ok.');
process.exit(0);