直接使用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
的作用就是統一Model的定義:
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);