Node中使用ORM框架
- 2019 年 12 月 10 日
- 筆記
在正常的開發中,大部分都會使用MVC為主要的系統架構模式。而Model一般包含了複雜的業務邏輯以及數據邏輯,因為Model中邏輯的複雜度,所以我們有必要降低系統的耦合度。通常情況下,我們如果直接使用JDBC操作數據庫,業務邏輯和數據存取邏輯是混在一起的。我們一般一個功能的邏輯可能如下所示:
- 接收客戶端的參數,建立數據庫的連接。
- 根據功能組裝sql語句,然後創建Statement對象。
- 使用Connection對象執行sql語句,得結果集ResultSet。
- 循環讀取結果集的數據,然後根據數據進行業務邏輯處理。
- 如果還有進一步的需求,再組裝新的sql語句進行執行。
- 執行結束關閉數據庫連接。
可以看到上面業務邏輯和數據存取邏輯是緊密耦合在一起的,如果需要修改需求,那工作量則是成倍的增長。所以有必要將業務邏輯以及數據存取邏輯分離開來,所以產生了ORM這麼一個對象與數據之間的映射技術。簡單來說ORM就是通過實例對象的語法,完成對關係型數據庫操作的技術,是對象-關係映射的縮寫。而本篇文章主要介紹一個NodeJS環境下的ORM框架—Sequelize。
首先,我們基於腳手架快速搭建一個express項目,執行命令:
express testORM
進入項目並且安裝依賴:
npm install
首先,我們先改變一下項目目錄結構:
- 項目已有目錄routes下存放路由文件。
- 根目錄下創建config.js,存放常量參數。
- 根目錄下創建contonller文件夾,在contonller下創建db.js,裏面封裝Sequelize連接數據庫的操作。
- 根目錄下創建db文件夾,在contonller下創建pay_goods.js,裏面定義數據類型,封裝數據庫存取的操作。
- 根目錄下創建service文件夾,在service下創建pay_goods.js,裏面對數據進行業務邏輯處理。
- 根目錄下創建utils文件夾,用來存放全局方法。
首先要使用SequeLize,我們需要安裝sequelize和mysql2包。命令如下:
npm install --save-dev sequelize mysql2
首先在config.js中配置數據庫連接常量:
var CONFIG = module.exports; CONFIG.GM_DB = { host: '192.168.1.55', port: '3306', user: 'admin', password: 'admin', database: 'usuz', connectionLimit: 300, charset : 'utf8mb4', dialect: 'mysql' };
然後進入contonller/db.js初始化數據庫連接,這裡需要注意一點,如果數據庫涉及時間字段,日期會轉換成+00:00時區,如果需要轉化為北京市區,我們需要加上timezone屬性:
var Sequelize = require('sequelize'); var CONFIG = require('./../config.js'); //初始化數據庫連接 module.exports = new Sequelize(CONFIG.GM_ACCOUNT_DB.database, CONFIG.GM_ACCOUNT_DB.user, CONFIG.GM_ACCOUNT_DB.password, { host: CONFIG.GM_ACCOUNT_DB.host, dialect: CONFIG.GM_ACCOUNT_DB.dialect, pool: { max: CONFIG.GM_ACCOUNT_DB.connectionLimit, min: 0 }, timezone: '+08:00' });
然後進入db/pay_goods.js中,在這裡需要對不同數據表進行數據類型定義以及數據讀取操作。我們首先使用sequelize.define()針對pay_goods表定義數據類型,這裡需要使用freezeTableName屬性關閉表名複數形式,然後需要指定timestamps屬性為false,否則會自動添加createAt和updateAt兩個時間參數:
//拼團地址表 var pt_addr = sequelize.define('aok_oil_addr', { order_code: Sequelize.STRING, login_id: Sequelize.STRING, user_id: Sequelize.STRING, created_date: Sequelize.DATE, shipping_id: Sequelize.STRING, shipping_addr: Sequelize.STRING, post_code: Sequelize.INTEGER, app_sid: Sequelize.STRING, name: Sequelize.STRING, invoice_name: Sequelize.STRING, invoice_num: Sequelize.STRING, invoice_phone: Sequelize.STRING, invoice_addr: Sequelize.STRING }, { freezeTableName: true, timestamps: false }); //排隊表 var pt_group = sequelize.define('groupqueue', { account: Sequelize.STRING, app_sid: Sequelize.STRING, created_date: Sequelize.DATE, status: Sequelize.TINYINT, groupNum: Sequelize.TINYINT, is_used: Sequelize.TINYINT, checked_date: Sequelize.DATE, goods_type: Sequelize.TINYINT }, { freezeTableName: true, timestamps: false });
define()方法共存在三個參數:
- 參數1:表示映射的數據庫表名
- 參數2:對表中每一個對象進行數據類型定義。
- 參數3:對選填參數配置
這裡對於Sequelize中的數據類型直接貼下文檔中提供的:

數據類型定義需要注意一點,如果我們有插入操作,Sequelize默認會增加createdAt字段和updateAt字段,所以說如果我們不需要這兩個字段我們可以在參數3選填參數添加timestamps為false關閉添加這兩個參數的操作。
對映射的數據表定義好數據類型,接下來我們可以寫幾個簡單的數據庫存取操作。Sequelize提供的API是非常豐富的,一篇文章不可能一一講解,所以我就選幾個比較通用的API。首先我們通過定義的數據類型調用create()可以保存數據,這裡我們在外層將create()操作封裝成一個方法,可以接受來自routes層的參數:
//保存地址 exports.payFirstaddr = function(data) { return pt_addr.create(data); }
接下來進入utils/common.js實現一個獲取客戶端傳參的全局方法:
var paramAll = this.paramAll = function (req) { var parameter = {}; if (req.params) { for (var p in req.params) { parameter[p] = req.params[p]; } } if (req.body) { for (var p in req.body) { parameter[p] = req.body[p]; } } if (req.query) { for (var p in req.query) { parameter[p] = req.query[p]; } } return parameter; };
進入routes/users.js實現路由,調用全局方法paramAll()獲取客戶端傳參,然後參數處理等業務邏輯我們放在service下的pay_goods.js,所以調用service/pay_goods.js封裝的參數處理方法delScoreHH()對參數進行處理:
(function () { var pay_goods = this.pay_goods = function (data, cb) { var param = { key: CONFIG.HHKEY, time: parseInt(new Date().getTime() / 1000), num: data.num, u_id: data.u_id, type: 1 } param.token = paramToToken(param); PostHelper.baseRequest(CONFIG.HHDELURL, param, function (err, result) { if (err) { return cb(err); } if(result.code == false) { return cb(result.msg); } var order = { account: data.u_id, score: data.num, app_sid: data.app_sid, good_title: data.good_title, create_date: new Date(), real_money: data.real_money } return cb(null, order); }); } })();
參數處理完返回給路由層,然後調用db中我們剛才封裝的saveOrder()保存數據:
xlsMallRouters.hlShopRecharge = function (req, res) { var par = paramAll(req); if (!par.openid || !par.login_id || !par.user_id || !par.app_sid || !par.real_money || !par.comment || !par.shopNum || !par.shipping_id || !par.shipping_addr || !par.post_code || !par.name || !par.groupNum || !par.goods_type || (!par.type || par.type != 1)) { return res.json(new ERR('抱歉,參數不全!', 401)); } firstPay(par, function (err, result) { if (err) { return res.json(new ERR(err, 410)); } pt_order.saveOrder(GetFirstParam(par)).then(function (rest) { return res.json(new PKG(rest)); }).catch(function (payFirstOrderErr) { return res.json(new ERR(payFirstOrderErr), 410); }); }); } router.post('/hlShopRecharge', xlsMallRouters.hlShopRecharge);
上面就完成的實現了一個插入數據的API,我們不需要手動書寫sql語句。而且將業務邏輯和數據存取邏輯完全獨立。接下來我們可以再看幾個查詢語句:
//通過id查詢數據 exports.getDetailOrder = function(id) { return pt_order.findPk(id); } //查詢開團信息 exports.getMainGroupOrder = function(data) { return pt_order.findAll({ attributes: ['id', 'order_code', 'user_id', 'real_money', 'app_sid', 'pay_date', 'group_code', 'is_manager', 'bailment', 'groupNum', 'goods_type'], where: { app_sid: data.app_sid, user_id: data.user_id, type: 1, pay_status: 13, is_manager: 1 }, order: [['pay_date', 'DESC']] }); } //根據訂單號查詢訂單信息 exports.findOneOrder = function(data) { return pt_order.findOne({ attributes: ['order_code', 'oil_balance', 'user_id', 'app_sid', 'pay_status', 'score'], where: { order_code: data.out_trade_no } }); } exports.isOldUser = function(data) { return pt_order.findOne({ attributes: [[Sequelize.fn('count', Sequelize.col('*')), 'count']], where: { app_sid: data.app_sid, user_id: data.user_id, type: 1, pay_status: 13, is_used: 0 } }); }
可以看到我們查詢一共寫了4個典型的示例方法,我們來分別看看是查詢什麼樣的數據:
- findAll():查詢多條數據,傳入一個json對象,json對象中可以對查詢條件進行限制,比如我示例代碼中使用attributes傳入要查詢的數據列數組,使用where傳入where條件語句的參數限制,使用limit和offset參數可以進行分頁操作,使用order可以根據某個數據列進行排序操作等。
- findByPk():通過id查詢對應數據,id一般為主鍵,所以只會返回一條數據,而且參數只能傳入id。
- findOne():只能查詢一條語句,一樣可以指定findAll()中的各種條件,但是只會返回符合條件的第一條數據。可以使用Sequelize.fn指定查詢條數等複合函數的結果。
看完了查詢操作,接下來我們可以接着看看更新操作。我們來看看更新操作:
//通過訂單號更新訂單付款狀態 exports.codeUpdateStatus = function(data) { return pt_order.update({ pay_status: 13, pay_date: new Date(), update_date: new Date() }, { where: { order_code: data.out_trade_no } }) } //扣減平台積分 exports.deductPlatLvl = function (data) { return user_vip.update({ lvl: sequelize.literal('`lvl` -' + data.score * 100) }, { where: { uid: 1259929, app_sid: 'usuz', vip_sid: 'jf_shop_score' } }); }
可以看到我們查詢一共寫了2個典型的示例方法都是使用update方法實現更新數據,如果更新的值固定值就可以直接在json對象中直接指定需要更新的參數和值,但是如果是需要在字段原有值進行增減操作就需要使用sequelize.literal()進行操作。到這裡我們對於Sequelize的基礎操作就差不多了解了,接下來來看看Sequelize封裝sql如何鏈式調用多個數據庫操作,因為Sequelize是基於Promise的ORM框架,所以我們很簡單的使用鏈式調用數據庫讀取操作實現多個數據庫操作:
xlsMallRouters.payFirst = function (req, res) { var par = paramAll(req); if (!par.user_id || !par.app_sid || !par.real_money || !par.comment || !par.isMinPro || !par.shipping_id || !par.shipping_addr || !par.post_code || !par.name || !par.goods_type || !par.openid || !par.profit_type) { return res.json(new ERR('抱歉,參數不全!', 401)); } firstPay(par, function (err, result) { if (err) { return res.json(new ERR(err, 410)); } pt_order.payFirstOrder(GetFirstParam(par)).then(function (rest) { return pt_order.payFirstaddr(GetFirstAddrParam(par)); }).then(function (rest) { return res.json(new PKG(result)); }).catch(function (payFirstOrderErr) { return res.json(new ERR(payFirstOrderErr), 410); }); }); } router.post('/payFirst', xlsMallRouters.payFirst);
到這裡我們就可以實現MVC架構,將數據庫數據讀取操作封裝到db層,將路由操作封裝到routes層,將業務邏輯操作封裝到service下。有利於項目的不斷迭代開發。