1. 程式人生 > 實用技巧 >Sequelize建立model及資料庫配置

Sequelize建立model及資料庫配置

  直接使用Sequelize雖然可以,但是存在一些問題。團隊開發時,有人喜歡自己加timestamp,有人又喜歡自增主鍵,並且自定義表名。一個大型Web App通常都有幾十個對映表,一個對映表就是一個Model。如果按照各自喜好,那業務程式碼就不好寫。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應該定義如下:

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就具有emailpasswdnamegender這4個業務欄位。idcreatedAtupdatedAtversion應該自動加上,而不是每個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設定主鍵(如果主鍵為nullundefined)、設定時間戳和版本號。

  這麼一來,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;

  具體的規則是:

  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,就需要引入對應的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);