用nodejs写服务端的小伙伴们对sequelize一定不会陌生的,它是node端著名的ORM框架,支持MySQL, MariaDB, SQLite和Microsoft SQL Server等数据库。通过这个库我们可以实现如下功能:
生成表:使用js定义好表结构,直接将表同步生成到数据库中,省去了手动写sql建表的烦恼。
CRUD:通过sequlize提供的方法去实现数据库的查询等操作,清晰明了,不需要写大段大段的原生sql语句。
迁移:如果需要在下一版本中,增加或者删除某个库中的字段,亦或是增加表或删除表,直接修改之前定义表结构的js文件,然后执行一个迁移脚本,就会自动生成一个数据库的升级和降级的脚本,直接运行即可。(本文不讲解如何实现迁移,需要的小伙伴请查看官方文档https://sequelize.org/v5/manual/migrations.html)
之前项目中用的是v4版本的sequelize(一直偷懒没有升级),在2019.03.13的时候sequelize正式发布的v5版本。最近正好有个新项目于是就用了v5版本的,熟悉了下新的方法,本文主要讲解sequelize结合mysql的在实际项目中的用法!
seuqelize v5官方文档地址:https://sequelize.org/v5/
通过npm安装sequelize和mysql(v5版本的sequelize需要node版本为6或者更高)
npm install --save sequelize
npm install --save mysql2
接下来我们会用Sequelize的构造方法来创建一个mysql的连接实例。
先来看看sequelize官方文档中构造方法的定义
public constructor(database: string, username: string, password: string, options: Object)
再来看看我们实际代码中对构造方法的调用:
const Sequelize = require('sequelize');
const TcDb = new Sequelize(database, username, password, {
host: DB_CONFIG.HOST,
port: DB_CONFIG.PORT,
dialect: 'mysql',
timezone: '+08:00',
logging: false,
pool: {
max: 5,
min: 0,
idle: 10000
},
define: {
freezeTableName: true,//禁止自动修改表名
timestamps: false,//不需要添加 createdAt 和 updatedAt 两个时间戳字段
}
});
解释几个必要参数,以及日常使用频率较高的参数
初始化完mysql的连接后,可以通过下面的代码来验证下是否连接成功:
TcDb.authenticate()
.then(() => {
console.log('Connection has been established successfully.');
})
.catch(err => {
console.error('Unable to connect to the database:', err);
});
关于更多连接配置参数的介绍请查阅官方文档:https://sequelize.org/v5/class/lib/sequelize.js~Sequelize.html#instance-constructor-constructor
一般情况下,一个表对应一个js文件,在js文件的开头我们先定义表结构,关于这张表的所有查询、更新、插入等数据库方法也会写在这个js文件里面。 接下来我们来看一个简单的用户表的model定义。
/**
* 用户表
* wx_user
*/
const {TcDb} = require('../db-info');
const Sequelize = require('sequelize');
const Model = Sequelize.Model;
class WxUserModel extends Model {
}
WxUserModel.init({
id: {type: Sequelize.INTEGER, primaryKey: true, autoIncrement: true, comment: '主键'},
openId: {type: Sequelize.STRING(32), comment: '公众号登录授权返回的openId', unique: true},
mobile: {type: Sequelize.STRING(50), comment: '加密后的手机号'},
realName: {type: Sequelize.STRING(100), comment: '真实姓名'},
belongBank: {type: Sequelize.INTEGER, comment: '所属网点'},
createTime: {
type: Sequelize.DATE,
defaultValue: Sequelize.literal('NOW()'),
comment: '创建时间'
},
updateTime: {
type: Sequelize.DATE,
defaultValue: Sequelize.literal('NOW()'),
comment: '更新时间'
},
}, {
sequelize: TcDb,
modelName: 'wx_user'
});
module.exports = WxUserModel;
第一步:创建一个用户表对应的class类,继承于Model。这个Model类就是ORM框架的纽带,用来将我们js中定义的表结构映射到Mysql中。
第二步:调用我们用户表Model的init()方法来定义我们的表结构。
public static init(attributes: Object, options: Object): Model
init()方法需要传入2个对象就能完成一个表的定义的,我们来分别看看两个参数的主要属性分别有什么:
【attributes】第一个参数对象里面存放的是表中每一列的定义,key是表中的列名,vaule也是一个对象,包含了该列的属性,常用的属性如下:
Sequelize.STRING // VARCHAR(255)
Sequelize.STRING(1234) // VARCHAR(1234)
Sequelize.STRING.BINARY // VARCHAR BINARY
Sequelize.TEXT // TEXT
Sequelize.TEXT('tiny') // TINYTEXT
Sequelize.INTEGER // INTEGER
Sequelize.BIGINT // BIGINT
Sequelize.BIGINT(11) // BIGINT(11)
Sequelize.FLOAT // FLOAT
Sequelize.FLOAT(11) // FLOAT(11)
Sequelize.FLOAT(11, 10) // FLOAT(11,10)
Sequelize.DOUBLE // DOUBLE
Sequelize.DOUBLE(11) // DOUBLE(11)
Sequelize.DOUBLE(11, 10) // DOUBLE(11,10)
Sequelize.DECIMAL // DECIMAL
Sequelize.DECIMAL(10, 2) // DECIMAL(10,2)
Sequelize.DATE // DATETIME for mysql / sqlite, TIMESTAMP WITH TIME ZONE for postgres
Sequelize.DATEONLY // DATE without time.
Sequelize.BOOLEAN // TINYINT(1)
【options】第二个参数对象包含的主要属性再来看看:
关于上面init()方法的2个参数对象的完整文档,请参考https://sequelize.org/v5/class/lib/model.js~Model.html#static-method-init
刚刚我们完成了js中表model的定义,这时候我们需要在数据库中创建这个表了。Model提供了一个sync()方法用来创建表。
const WxUserModel = require('./model/wx-user');
WxUserModel.sync({force: true})
.then((result) => {
console.log(result);
})
.catch((err) => {
console.error(err);
});
sycn()方法中传了一个参数force(是否强制创建表),false表示如果数据库已存在这个表则不执行任何操作;true表示如果数据库中存在这个表,会先将原来的表以及里面的数据删除再创建。
Sequelize的Model类为我们提供了对数据库CRUD操作的方法。在第三步的时候我们创建了一个WxUserModel继承于Model,Model类有很多自带的方法比如create()、findOne()、update()、destory()........
但是我们的实际项目中对数据库的操作不会是简单的增删改查,我们会对Model类提供的方法上添加很多自己的逻辑。比如查询用户列表的时候,可能涉及到很多过滤条件(年龄、名字以及分页信息等),所以我们一般会封装自己的方法。为了避免自定义的方法名和Model类的方法名重复,我的习惯是在自定义的方法前面加一个小写的c,表示是class方法,比如
WxUserModel.cCreateOne(param1, param2, param2)
WxUserModel.cFindAllByFilter(param1, param2, param2)
WxUserModel.cUpdateOne(param1, param2, param2)
WxUserModel.cDeleteOne(param1, param2, param2)
//Sequelize Model定义的插入方法
public static create(values: Object, options: Object): Promise
//自定义方法
WxUserModel.cCreateOne = function (openId, mobile, realName) {
return WxUserModel.create({openId: openId, mobile: mobile, realName: realName});
};
//方法调用
let result = await WxUserModel.cCreateOne('abcdefghij', '18888888888', 'wyk');
//Sequelize Model定义的插入方法
public static findOne(options: Object): Promise
//自定义方法
WxUserModel.cFindOneByName = function (realName) {
return WxUserModel.findOne({
where: {realName: realName}
});
};
//方法调用
let result = await WxUserModel.cFindOneByName('wyk');
//Sequelize Model定义的插入方法
public update(values: Object, options: Object): Promise
//自定义方法,updateDict是我们需要更新成的新属性的对象
WxUserModel.cUpdateOneByOpenId = function (openId, updateDict) {
updateDict.updateTime = new Date();
return WxUserModel.update(updateDict, {
where: {openId: openId}
})
};
//方法调用
let result = await WxUserModel.cUpdateOneByOpenId('abcdefghij', {name:'newName', mobile: '19999999999'});
//Sequelize Model定义的插入方法
public destroy(options: Object): Promise
//自定义方法
WxUserModel.cDeleteOneByOpenId = function (openId) {
return WxUserModel.destroy({
where: {openId: openId}
})
};
//方法调用
let result = await WxUserModel.cDeleteOneByOpenId('abcdefghij');
上面举得例子都是比较基础的用法,比如我们插入时候用的create()方法,它实际上有2个参数,第一个参数是我们插入的数据对象,第二个可选参数对象包含了如下属性:
options | Object |
|
Build and query options |
options.raw | boolean |
|
If set to true, values will ignore field and virtual setters. |
options.isNewRecord | boolean |
|
Is this new record |
options.include | Array |
|
An array of include options - Used to build prefetched/included model instances. See |
options.fields | string[] |
|
An optional array of strings, representing database columns. If fields is provided, only those columns will be validated and saved. |
options.silent | boolean |
|
If true, the updatedAt timestamp will not be updated. |
options.validate | boolean |
|
If false, validations won't be run. |
options.hooks | boolean |
|
Run before and after create / update + validate hooks |
options.logging | Function |
|
A function that gets executed while running the query to log the sql. |
options.benchmark | boolean |
|
Pass query execution time in milliseconds as second argument to logging function (options.logging). |
options.transaction | Transaction |
|
Transaction to run query under |
options.searchPath | string |
|
An optional parameter to specify the schema search_path (Postgres only) |
options.returning | boolean | Array |
|
Appends RETURNING |
太多了吧!!具体就不展开了,详情请参考create()的文档 https://sequelize.org/master/class/lib/model.js~Model.html#static-method-create,find()、update()、destroy()的文档里面都有。
事务在mysql中是必不可少的。举个经典的例子,当用户购买某个产品服务时支付成功了,我们需要对数据库进行的操作有:置订单的状态、服务表插入记录、修改用户属性等等等。这些操作要么全部成功,要么全部失败,不能说其中2个成功,其他操作失败了。所以我们就需要把每一个逻辑单元中的所有操作放在一个事务里,如果所有数据库操作执行完成后没有异常,那就提交这次事务;如果出现异常,事务则执行回滚。
Sequelize中事务的文档请参考:https://sequelize.org/v5/manual/transactions.html
Sequelize为我们提供了2种使用事务方法
//TcDb为第二步中创建的数据库连接对象
return TcDb.transaction(t => {
//回调中务必返回一个promise对象
return WxUserModel.create({
openId: 'openId1'
}, {transaction: t}).then(user => {
return WxUserModel.create({
openId: 'openId2'
}, {transaction: t});
});
}).then(result => {
//执行完成,事务已自动提交callback
}).catch(err => {
//出现异常,事务已回滚
});
比如有一个逻辑操作,需要创建一个用户A并且更新用户B的手机号,这2个操作需要放在事务中来保证一致性。我们要先对第五步中的创建方法和更新方法进行一点小改造:传入的参数增加一个transaction对象。
//创建
WxUserModel.cCreateOne = function (t, openId, mobile, realName) {
return WxUserModel.create(
{openId: openId, mobile: mobile, realName: realName},
{transaction: t}
);
};
//更新
WxUserModel.cUpdateOneByOpenId = function (t, openId, updateDict) {
updateDict.updateTime = new Date();
return WxUserModel.update(updateDict, {
where: {openId: openId},
transaction: t
})
};
上面2个方法都增加了一个参数t(实际项目中每一个CRUD方法都需要传入t参数)。参数t是什么呢?他是一个数据库事务对象,通过TcDb.transaction()返回的promise中得到的。一般会在数据库连接对象(TcDb)上再封装一个getTransaction()方法,用来获取事务对象t,以及管理事务流程(commit和callback操作)。
/**
* 封装开启数据库事务的方法
*
* @param callback
* @returns {Promise}
*/
TcDb.getTransaction = async (callback) => {
let t = null;
try {
t = await TcDb.transaction();
await callback(t);
t.commit();
} catch (err) {
if (t)
t.rollback();
console.error(err);
//try-catch只是为了能够在异常的时候rollback,如果有异常,可抛出
//throw new Error(err);
}
};
最后将获取事务方法和带事务参数插入和更新方法连起来使用。
async function dbTest() {
await TcDb.getTransaction(async (t) => {
let result = await WxUserModel.cCreateOne(t, 'openId1', '18888888888', 'wyk');
console.log(result);
let result2 = await WxUserModel.cUpdateOneByOpenId(t, 'abcdefghij', {name: 'newName', mobile: '19999999999'});
console.log(result2);
});
}
dbTest();
未完待续.....