团队开发时,有人喜欢自己加timestamp:

    有人又喜欢自增主键,并且自定义表名:

    1. id: {
    2. type: Sequelize.INTEGER,
    3. autoIncrement: true,
    4. primaryKey: true
    5. },
    6. name: Sequelize.STRING(100)
    7. }, {
    8. tableName: 't_pet'
    9. });

    一个大型Web App通常都有几十个映射表,一个映射表就是一个Model。如果按照各自喜好,那业务代码就不好写。Model不统一,很多代码也无法复用。

    所以我们需要一个统一的模型,强迫所有Model都遵守同一个规范,这样不但实现简单,而且容易统一风格。

    我们首先要定义的就是Model存放的文件夹必须在models内,并且以Model名字命名,例如:Pet.jsUser.js等等。

    其次,每个Model必须遵守一套规范:

    1. 统一主键,名称必须是id,类型必须是STRING(50)
    2. 主键可以自己指定,也可以由框架自动生成(如果为null或undefined);
    3. 所有字段默认为NOT NULL,除非显式指定;
    4. 统一timestamp机制,每个Model必须有createdAtupdatedAtversion,分别记录创建时间、修改时间和版本号。其中,createdAtupdatedAtBIGINT存储时间戳,最大的好处是无需处理时区,排序方便。version每次修改时自增。

    所以,我们不要直接使用Sequelize的API,而是通过db.js间接地定义Model。例如,User.js应该定义如下:

    1. const db = require('../db');
    2. module.exports = db.defineModel('users', {
    3. email: {
    4. type: db.STRING(100),
    5. unique: true
    6. },
    7. passwd: db.STRING(100),
    8. name: db.STRING(100),
    9. gender: db.BOOLEAN
    10. });

    这样,User就具有emailpasswdnamegender这4个业务字段。idcreatedAtupdatedAtversion应该自动加上,而不是每个Model都去重复定义。

    所以,db.js的作用就是统一Model的定义:

    1. const Sequelize = require('sequelize');
    2. console.log('init sequelize...');
    3. var sequelize = new Sequelize('dbname', 'username', 'password', {
    4. host: 'localhost',
    5. dialect: 'mysql',
    6. pool: {
    7. max: 5,
    8. min: 0,
    9. idle: 10000
    10. }
    11. });
    12. const ID_TYPE = Sequelize.STRING(50);
    13. function defineModel(name, attributes) {
    14. var attrs = {};
    15. for (let key in attributes) {
    16. let value = attributes[key];
    17. if (typeof value === 'object' && value['type']) {
    18. value.allowNull = value.allowNull || false;
    19. attrs[key] = value;
    20. } else {
    21. attrs[key] = {
    22. type: value,
    23. allowNull: false
    24. }
    25. attrs.id = {
    26. type: ID_TYPE,
    27. primaryKey: true
    28. };
    29. attrs.createdAt = {
    30. type: Sequelize.BIGINT,
    31. allowNull: false
    32. };
    33. attrs.updatedAt = {
    34. type: Sequelize.BIGINT,
    35. allowNull: false
    36. };
    37. attrs.version = {
    38. type: Sequelize.BIGINT,
    39. allowNull: false
    40. };
    41. return sequelize.define(name, attrs, {
    42. tableName: name,
    43. timestamps: false,
    44. hooks: {
    45. beforeValidate: function (obj) {
    46. let now = Date.now();
    47. if (obj.isNewRecord) {
    48. if (!obj.id) {
    49. obj.id = generateId();
    50. }
    51. obj.createdAt = now;
    52. obj.updatedAt = now;
    53. obj.version = 0;
    54. } else {
    55. obj.updatedAt = Date.now();
    56. obj.version++;
    57. }
    58. }
    59. }
    60. });
    61. }

    Sequelize在创建、修改Entity时会调用我们指定的函数,这些函数通过hooks在定义Model时设定。我们在beforeValidate这个事件中根据是否是isNewRecord设置主键(如果主键为nullundefined)、设置时间戳和版本号。

    这么一来,Model定义的时候就可以大大简化。

    数据库配置

    接下来,我们把简单的config.js拆成3个配置文件:

    • config-default.js:存储默认的配置;
    • config-override.js:存储特定的配置;
    • config-test.js:存储用于测试的配置。

    例如,默认的config-default.js可以配置如下:

    config-override.js可应用实际配置:

    1. var config = {
    2. database: 'production',
    3. username: 'www',
    4. password: 'secret-password',
    5. host: '192.168.1.199'
    6. };
    7. module.exports = config;

    config-test.js可应用测试环境的配置:

    1. var config = {
    2. database: 'test'
    3. };
    4. module.exports = config;

    读取配置的时候,我们用config.js实现不同环境读取不同的配置文件:

    1. const defaultConfig = './config-default.js';
    2. // 可设定为绝对路径,如 /opt/product/config-override.js
    3. const overrideConfig = './config-override.js';
    4. const testConfig = './config-test.js';
    5. const fs = require('fs');
    6. if (process.env.NODE_ENV === 'test') {
    7. console.log(`Load ${testConfig}...`);
    8. config = require(testConfig);
    9. } else {
    10. console.log(`Load ${defaultConfig}...`);
    11. config = require(defaultConfig);
    12. try {
    13. if (fs.statSync(overrideConfig).isFile()) {
    14. console.log(`Load ${overrideConfig}...`);
    15. config = Object.assign(config, require(overrideConfig));
    16. }
    17. } catch (err) {
    18. console.log(`Cannot load ${overrideConfig}.`);
    19. }
    20. }
    21. module.exports = config;

    具体的规则是:

    1. 先读取config-default.js
    2. 如果不是测试环境,就读取config-override.js,如果文件不存在,就忽略。
    3. 如果是测试环境,就读取config-test.js

    这样做的好处是,开发环境下,团队统一使用默认的配置,并且无需config-override.js。部署到服务器时,由运维团队配置好config-override.js,以覆盖config-override.js的默认设置。测试环境下,本地和CI服务器统一使用config-test.js,测试数据库可以反复清空,不会影响开发。

    要使用Model,就需要引入对应的Model文件,例如:User.js。一旦Model多了起来,如何引用也是一件麻烦事。

    自动化永远比手工做效率高,而且更可靠。我们写一个model.js,自动扫描并导入所有Model:

    这样,需要用的时候,写起来就像这样:

    1. const model = require('./model');
    2. let
    3. Pet = model.Pet,
    4. User = model.User;
    5. var pet = await Pet.create({ ... });

    工程结构

    最终,我们创建的工程model-sequelize结构如下:

    1. model-sequelize/
    2. |
    3. +- .vscode/
    4. | |
    5. | +- launch.json <-- VSCode 配置文件
    6. |
    7. +- models/ <-- 存放所有Model
    8. | |
    9. | +- Pet.js <-- Pet
    10. | |
    11. | +- User.js <-- User
    12. |
    13. +- config.js <-- 配置文件入口
    14. |
    15. +- config-default.js <-- 默认配置文件
    16. |
    17. +- config-test.js <-- 测试配置文件
    18. |
    19. +- db.js <-- 如何定义Model
    20. |
    21. +- model.js <-- 如何导入Model
    22. |
    23. +- init-db.js <-- 初始化数据库
    24. |
    25. +- app.js <-- 业务代码
    26. |
    27. +- package.json <-- 项目描述文件
    28. |
    29. +- node_modules/ <-- npm安装的所有依赖包

    注意到我们其实不需要创建表的SQL,因为Sequelize提供了一个sync()方法,可以自动创建数据库。这个功能在开发和生产环境中没有什么用,但是在测试环境中非常有用。测试时,我们可以用sync()方法自动创建出表结构,而不是自己维护SQL脚本。这样,可以随时修改Model的定义,并立刻运行测试。开发环境下,首次使用sync()也可以自动创建出表结构,避免了手动运行SQL的问题。

    init-db.js的代码非常简单:

    1. const model = require('./model.js');
    2. model.sync();

    它最大的好处是避免了手动维护一个SQL脚本。

    读后有收获可以支付宝请作者喝咖啡,读后有疑问请加微信群讨论:

    建立Model - 图2