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下。有利于项目的不断迭代开发。