直接使用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);