本文分享 sequelize 的项目实践经验,如果你还不了解 sequelize,可以先看文档
https://www.sequelize.com.cn/
sequelize 是一个可以使用对象的方式操作数据库的 第三方工具库,是一个基于 promise 的 Nodejs ORM
ORM全称是:Object Relational Mapping(对象关系映射),其主要作用是在编程中,把面向对象的概念跟数据库中表的概念对应起来。其实我觉得就是用 mongo 的方式操作 mysql 等关系型数据库
用ORM的好处就是你不用操作表,不用写sql语句,在程序中用面向对象的思路,直接操作对象即可。比如要插入一条语句,直接user.save()即可。对应的 ORM 会生成一条 sql 语句
优点是简化了curd,缺点是牺牲了速度,orm需要解析表结构
下面就主要介绍 sequelize 的具体用法,分为
1、数据库初始化
2、定义表结构 model
3、查询
4、创建
5、更新
6、删除
7、事务
重头在增删改查这几个部分,重点记录下使用的场景和踩得坑,怎么用还是得看文章
1 数据库初始化
首要工作就是使用 sequelize 连接上数据库,如下
代码语言:javascript复制const Sequelize = require('sequelize’);
const MysqlConnect = new Sequelize(‘数据库名', ‘账号', '密码', {
define: {
timestamps: false, // 全局默认不要 createAt 和 updateAt,自动管理时间
},
dialect: 'mysql', // 数据库类型,| 'mariadb' | 'postgres' | 'mssql'
host: ’10.10.10.10', // ip
port: 8800, // 端口
logging: (msg) => { // 日志信息,打印出每个操作生成的具体的 sql语句
console.log('msg', msg);
},
timezone: ' 08:00', // 时区,在中国就是 8
});
其中 timezone 需要设置 8,因为中国是 东八区, 如果没有设置,默认是 UTC 时间,那么你 2022-1-2 22:00 插入一条数据,但是具体显示的时间变成 2022-1-2 14:00
以前我还踩过一个时区的坑,然后才去学习总结了一下
【踩坑】服务器和本地相差8小时
2 定义数据表结构 model
连接上数据库之后,需要对里面的表做一个映射,表里面有什么字段,字段有什么属性,统统都要列出来
因为 sequelize 不知道表都有什么字段,需要你告诉他,如果你没有列出来某个字段,但是实际操作了这个字段的话,会报错
简单看下模型的定义
代码语言:javascript复制function PersonModel(sequelize, DataTypes) {
return sequelize.define(
"person", // 给模型自定义个名字,通常是表名的驼峰写法
{
id: {
type: DataTypes.BIGINT, // 数据类型,有 String、Date 等等
allowNull: true, // 是否允许为空
primaryKey: true, // 是否主键
autoIncrement: true, // 是否自增
comment: "id", // 备注
},
},
{
sequelize,
tableName: "person", // 表名称
}
);
};
更多 DateTypes 类型请看
https://sequelize.org/master/variable/index.html#static-variable-DataTypes
定义完模型之后,需要把 模型和 之前 初始化连接的数据库 给 关联起来,才可以使用
代码语言:javascript复制const Person = MysqlConnect.import(MyModel);
之后我们就是用这个 关联之后的对象,直接进行CRUD的操作
Person.create() 等等
下面记录一些 定义model 时 多会用到的配置
1、自动生成model
复杂的数据表如果要一个个人工去定义,那可太麻烦了,可有使用
https://github.com/sequelize/sequelize-auto
这个库帮我们自动生成model,它直接拉取数据库表的信息,然后生成对应的model
2、定义设置器(get,set)
有的数据出库入库的时候,需要做一些额外的处理,这时就适合放到 设置器去做,比如
2.1 时间格式化
代码语言:javascript复制function MyModel(sequelize, DataTypes) {
return sequelize.define(
"person",
{
create_time: {
type: DataTypes.DATE,
get() {
const v = this.getDataValue('create_time');
if(v instanceof Date && v.getTime()){
return moment(v).format('YYYY-MM-DD HH:mm:ss')
}
return null
},
},
}
);
};
2.2 对象or 数组序列化
代码语言:javascript复制function MyModel(sequelize, DataTypes) {
return sequelize.define(
"person",
{
list: {
type: DataTypes.STRING(1024),
get() {
const v = this.getDataValue('list');
return typeof v === 'string' && v ? JSON.parse(v) : [];
},
},
}
);
}
3 查询
查询是最基础的操作,也是我们比较常用的,也没什么特殊要讲的
列举几个查询常用的查询操作
1、分页
2、排序
3、分组
4、限定查询字段
5、查出不重复数量
1、分页
代码语言:javascript复制const Person = MysqlConnect.import(MyModel);
const page = 1;
const pageSize = 10;
const result = await Person.findAll({
limit: pageSize,
offset: page * pageSize,
});
2、排序
代码语言:javascript复制const Person = MysqlConnect.import(MyModel);
const result = await Person.findAll({
order: [
['type', 'DESC'],
['id', 'ASC'],
],
});
优先满足按 type 降序,如果 type 一样, 那么按 id 升序。
3、分组
以 type 对数据进行分组,相同值的放到一组,反正和 mysql 是一样的
代码语言:javascript复制const Person = MysqlConnect.import(MyModel);
const result = await Person.findAll({
group: 'type',
});
只使用 group ,只会查出每个分组的第一条数据
通常都是用来做统计,换成调用 findAndCountAll 来获得各个分组数量
代码语言:javascript复制const Person = MysqlConnect.import(MyModel);
const result = await Person.findAndCountAll({
group: 'type',
});
4、查询字段
代码语言:javascript复制const Person = MysqlConnect.import(MyModel);
const result = await Person.findAll({
attribute: ['name', 'id’], // 只查出某些字段
attributes: { exclude: ['id'] }, // 不需要某些字段
attributes: ['id', ['name', 'label_name']], // 重写字段名称,name 改成 label_name
});
** 查出所有属性
通常我们不设置 attribute,默认查出所有属性
但是 sequelize 只会查出 model 中定义有的字段
比如 model 定义只有 id 和 name 两个字段,那么 sequelize 生成的 sql 语句 只查出这两个字段
代码语言:javascript复制SELECT `id`, `name` FROM `person`
就算表里面还有其他字段,你也无法查出来,除非你更新model
后期维护的时候,表新增字段,model 不一定会及时同步更新,要想还能查出所有字段
代码语言:javascript复制const result = await Person.findAll({
attribute: [‘*’], // 查出所有字段
});
5、查出某字段不重复的数量
代码语言:javascript复制const result = await Person.findAndCountAll({
// 查询某字段不重复的【数量】,而不是记录,默认是id,如果需要其他字段就写 定义 col
distinct: true,
// 查出 name 不重复的数量
col: 'name',
});
注意这里是查出数量,不是数据,比如下面以name 为重复字段的,虽然有 6条数据,但是 counts 是 2
3.1 查询条件
查询条件用 sequelize 之后可以简化很多,使用各种逻辑操作符组合的方式,轻轻松松就能写出复杂的查询语句
详细的逻辑操作符可以看具体文档
https://www.sequelize.com.cn/core-concepts/model-querying-basics#操作符
难度不大,根据业务逻辑组合即可,比如这样
代码语言:javascript复制const result = await Person.findAll({
where: {
// 一个搜索框搜索多个字段
[Op.or]: {
phone: {
[op.like]: `%${search}%`,
},
name: {
[op.like]: `%${search}%`,
},
address: {
[op.like]: `%${search}%`,
},
},
// 搜索在有效期内的
[Op.And]: {
bg_time: { [Op.lt]: sequelize.fn('NOW') },
end_time: { [Op.gt]: sequelize.fn('NOW') },
},
},
});
3.2 查询 - 关联表
简单描述下
通常不同类型的数据存放不同的表,但是不同表的数据之间是有关联的,比如 用户表和 评论表,所以需要表与表之间建立联系
常用的三种关联类型就 一对一,一对多,多对多
建立这种关联通常是通过外键的形式,比如在 a 表中 存放 b 表中有关联的数据的id,这样a表就和b 表的数据产生了关联
1一对一
A表的每一条数据 只能和 B 表的一条数据产生关联,B 表也是,一般很少用到一对一的情况,因为通常这样都可以直接放在一张表里,所以这里的应用就是 当表结构特别庞大的时候可能,会进行拆分表,此时一对一就有作用了
在 sequelize 中需要把两张表的 model 手动关联起来,这样他才知道这两张表的关系,从而可以一次性把两张表的数据都查出来
比如一个人只有一个身份证
代码语言:javascript复制function IdCardModel(sequelize, DataTypes) {
return sequelize.define(
“idCard",{
cardId: { type: DataTypes.INT },
}
);
};
const IdCard = MysqlConnect.import(IdCardModel);
Person.hasOne(IdCard);
Person.findAll({
include: IdCard, // 直接传入model
});
这样在查人的时候,把另一张表的身份证信息一起查出来
代码语言:javascript复制[{
id:1212,
name: "xxxxxx",
idCard:{ cardId:22222 }
}]
因为表关联需要外键,但是上面明显没有写明外键,所以 sequelize 会推断外键在 IdCard 中为 personId,如果没有就会报错 [找不到 idCard.personId 这个字段]
自定义外键,在 IdCard 中的外键为 user_id 和 Person 表产生关联
代码语言:javascript复制Person.hasOne(IdCard, {
foreignKey: 'user_id',
});
重命名 IdCard 表数据 在 Person 信息中的字段名,即 下面数据中 idCard 这个字段名,通常 sequelize 默认会以 idCard 定义的model 名作为字段名
代码语言:javascript复制[{
id:1212,
name: "xxxxxx",
idCard:{ cardId:22222 }
}]
代码语言:javascript复制Person.hasOne(IdCard, {
foreignKey: 'user_id',
as: 'card',
});
Person.findAll({
include: [
{
model: IdCard,
as: 'card', // 如果在关联的时候使用了as 重命名,那么include 的时候也需要as
},
],
});
[{
id:1212,
name: "xxxxxx",
Card:{ cardId:22222 }
}]
上面是通过 Person 表找到 IdCard 表,如果反之,则需要反过来关联一次
代码语言:javascript复制IdCard.belongsTo(Person, {
foreignKey: 'user_id',
});
IdCard.findAll({
include:Person
});
这样数据就变成
代码语言:javascript复制[{
cardId:1212,
person:{
id:1212,
name: "xxxxxx”,
}
}]
因为当进行关联的时候,只有源模型知道 两个模型的关系(前面的是源模型,后面的是目标模型)
另一个模型不知道之间的关系的,所以另一个模型在查询的时候就无法查出关联的表的数据,所以需要反过来关联一次
2一对多
一对多的场景就有很多,比如一个视频有多条评论,一个商品有多个类型,一个用户有多个收货地址
同样需要把表进行关联
代码语言:javascript复制person.hasMany(personComment, {
foreignKey: "user_id",
as: "comment",
});
查出用户所有的评论
代码语言:javascript复制person.findAll({
include:personComment
});
如果想通过评论查出用户信息,同样使用 belongsTo 关联即可
3多对多
多对多比较特殊一点,因为此时外键放在哪张表都无法满足,所以需要一张专门的外键表
比如 一个活动有多个标签,一个标签属于多个活动,此时就需要一张表额外存放 活动 和标签的对应关系
详细内容了解 sequelize 文档
https://www.sequelize.com.cn/core-concepts/assocs#多对多关系
首先定义 表的model,那么这里就要定义三个表的model 了,包括一张 act_label 的外键表
代码语言:javascript复制function ActModel(sequelize, DataTypes) {
return sequelize.define(
'act',
{
name: { type: DataTypes.STRING(40) }
},
{ sequelize, tableName: 'act'},
);
};
function LabelModel(sequelize, DataTypes) {
return sequelize.define(
"label",
{
name: { type: DataTypes.STRING(40), }
},
{ sequelize, tableName: 'label'},
);
};
function ActLabelModel(sequelize, DataTypes) {
return sequelize.define(
"act_label",
{
id: { type: DataTypes.BIGINT },
act_id: { type: DataTypes.BIGINT },
label_id: { type: DataTypes.BIGINT },
},
{ sequelize, tableName: ‘act_label'},
);
};
然后进行关联即可
代码语言:javascript复制const act = MysqlConnect.import(ActModel);
const label = MysqlConnect.import(LabelModel);
const act_label = MysqlConnect.import(ActLabelModel);
act.belongsToMany(label, {
through: {
model: act_label,
// 当模型中不存在主键时,Belongs-to-Many 将创建一个唯一键. 可以使用 uniqueKey 参数覆盖此唯一键名.
// 若不希望产生唯一键, 可以使用 unique: false 参数.
unique: false,
},
foreignKey: 'act_id',
otherKey: 'label_id',
as: 'labels',
});
act.findAll({
include: [
{
model: label,
as: 'labels',
through: {
attributes: [], // 不要关系表 act_label 产生的数据
},
},
],
});
使用场景
1、给include 关联的表的数据排序
代码语言:javascript复制await person.findAll({
// 以关联表 comments 的 id 排序
order: [['comments', 'id', 'DESC']],
include: [
{
model: personComment,
as:"comments"
},
],
});
2、查询有子表数据的数据
也是比较常见的场景,比如返回有评论的用户数据,感知活跃的用户之类的
代码语言:javascript复制await person.findAll({
include: [
{
model: personComment,
as:"comments",
required: true // 表示该 comment 数据是否必须,如果为空那么整个person对象都不返回
},
],
});
这里涉及的是数据库的一些概念,left-join、inner-join、right-join 等
加了 required 的设置,就是 inner-join,只查出 两表的有交集的数据
去掉 required ,就是 left-join,查出所有 A 表数据并包含B表的部分数据
具体可以看这篇文章
https://segmentfault.com/a/1190000017369618
3、限制子表数据的总数量
这里不是说限制单个person 的 comments 返回数量,比如这样
代码语言:javascript复制await person.findAll({
include: [
{
model: personComment,
limit:10
},
],
});
而是限制返回的数据 所有 person 的 comment 的总数量,开整
代码语言:javascript复制await person.findAll({
limit: 5,
// 加了这个false之后,limit 就变成限制 comments 的数量
subQuery: false,
include: personComment,
});
返回的数据是,总共有5条comments, person 有多少个无所谓
这里碰到的一个场景是,有一个管理页面翻页是以 子表数据为 准的,但是还是以父表作为主体存在
4 创建
1单个创建
代码语言:javascript复制await person.create({
name: `神仙朱--`,
},);
2批量创建
代码语言:javascript复制await person.bulkCreate([
{
name: `hoho1`,
},
{
name: `hoho2`,
},
]);
批量创建的一个使用场景是 可以避免创建重复的数据,直接插入数据
如果数据不存在,就插入,如果存在,就更新
怎么判定这个数据是否存在?
通过主键或者 唯一索引
比如表中已经存在 id 为 1 的数据,此时你再插入 id 为1 的数据,那么就只会进行更新,不会再插入
下面介绍几个添加的场景
1、限定插入的字段
2、限定更新的字段
3、关联表创建
4、自动管理时间
限定插入的字段
有的字段是不需要手动插入的,避免被误操,我们可以限定字段,比如通常 updateTime 添加的时候是不用增加的
代码语言:javascript复制await person.bulkCreate(
[
{
name: `hoho1`,
sex: 1,
salary: 100,
},
{
name: `hoho2`,
sex: 2,
salary: 100,
},
],
{
// 插入数据时,只插入 name 和 sex
fields: ['name', 'sex'],
},
);
限定更新的字段
如果插入重复的数据 触发更新操作的时候,我们可以限定更新的字段,比如通过 createUser 和 createTime 是不用更新的
代码语言:javascript复制await person.bulkCreate(
[
{
name: `hoho1`,
sex: 1,
salary: 100,
},
{
name: `hoho2`,
sex: 2,
salary: 100,
},
],
{
// 更新数据时,只更新 name 和 sex,就算插入 salary 也不更新
updateOnDuplicate: ['name', 'sex'],
},
);
除个别字段外, fields 和 updateOnDuplicate 通常要保持一致,需要更新的字段,不仅要放在 updateOnDuplicate 中,更要放在 fields 中
如果数据表字段很多,那岂不是要一个个写完?因为 fields 和 updateOnDuplicate 都是白名单,所以需要明确把字段写上去
但是我们可以通过 lodash 排除个别字段即可
代码语言:javascript复制const _ = require('lodash’);
const fields = Object.keys(person.tableAttributes);
const fieldsCreate = _.without(fields, 'updateTime');
const fieldsUpdate = _.without(fields, 'createUser', 'createTime');
await person.bulkCreate(
[
{
name: `hoho1`,
sex: 1,
salary: 100,
},
],
{
fields: fieldsCreate,
updateOnDuplicate: fieldsUpdate,
},
);
关联创建
我们可以通过关联表的形式创建,一次性插入两张表的数据,不用分开插入
代码语言:javascript复制await person.create(
{
name: `hoho`,
comment: [
{ content: ‘hoho-content’ },
],
},
{
include: [
{
model: person_comment,
as: 'comment',
},
],
},
);
自动管理时间
通常像 create_time,update_time 这种时间,自己维护的话会比较麻烦,我们可以把这部分数据交给 sequelize 去做,它可以自动帮我们管理时间
主要在 model 定义的时候开启这个配置
代码语言:javascript复制function PersonModel(sequelize, DataTypes) {
return sequelize.define(
'person',
{
list: {
type: DataTypes.STRING(1024),
},
},
{
sequelize,
tableName: 'person', // 表名称
timestamps: true,
},
);
}
注意,它只是帮我们管理这个字段,我们需要自己创建 createAt 和 updateAt 这个两个字段,它并不会帮我们创建
如果你不想每张表都写这个定义,直接整个数据都统一配置,那么在初始化的时候配置即可
代码语言:javascript复制const Sequelize = require('sequelize’);
const MysqlConnect = new Sequelize(‘数据库名', ‘账号', '密码', {
define: {
timestamps: true, // 整个库默认自动管理时间,自动更新 createAt 和 updateAt
},
host: ’10.10.10.10', // ip
port: 8800, // 端口
timezone: ' 08:00', // 时区,在中国就是 8
});
访问文档了解更多
https://www.sequelize.com.cn/core-concepts/model-basics#时间戳
5 更新
更新就比较简单,没有什么特殊的点
批量更新
加上查询条件即可
代码语言:javascript复制await person.update(
{
name: '修改过',
},
{
where: {
status: 3,
},
},
);
单个更新
查询出来的每个数据对象,都包含了update 方法,通常利用这个把数据查出来之后,进行批量差别更新
代码语言:javascript复制const result = await person.findAll();
const list = result.map((item, index) => {
return item.update({
sex: index,
});
});
const resultList = await Promise.all(list);
或者需要额外做一些业务逻辑来做一些过滤, 在查询条件无法满足的时候
比如说,视频可以打标签,每次编辑视频的时候,都可以新增标签,也可以修改原有标签,也可以删除标签
此时我们就可以先把 该视频的所有 标签查出来,然后加上各种逻辑分支进行处理
代码语言:javascript复制const { labels, videoId } = request.body;
const videoList = await LabelsModel.findAll({
video_id: videoId,
});
labels.forEach((la) => {
const [item] = _.remove(videoList, (x) => x.name === la.name);
// 更新
if (item) {
item.update(la);
}
// 不存在就添加
else {
LabelsModel.create(la);
}
});
// 多余的删除
if (labels.length) {
labels.forEach((item) => {
LabelsModel.destory({
name: item.name,
});
});
}
6 删除
删除主要有下面3个使用场景
1、直接删除
2、软删除
3、关联表删除
直接删除
支持范围删除
代码语言:javascript复制await person.destroy({
where: {
id: 11111
},
});
软删除
数据很重要,一般我们执行删除,不会直接把数据从数据库中抹掉,而是设置删除标志位,我们一般是用 delFlag 这个字段作为是否删除的依据
sequelize 可以帮我们完成软删除
不需要我们设立标志位,也不需要我们在查询的时候过滤掉删除的数据
代码语言:javascript复制function PersonModel(sequelize, DataTypes) {
return sequelize.define(
'person',
{
id: { type: DataTypes.STRING(1024),},
},
{
sequelize,
tableName: 'person', // 表名称
paranoid: true,
},
);
}
但是我们需要在表中加上 deleteAt 这个字段,当我们删除的时候,sequelize 便会帮我们记录删除的时间
更多请了解
https://www.sequelize.com.cn/core-concepts/paranoid
关联表删除
我们希望有关联的表,在我们对一方进行删除的时候,另一方也会自动跟着删除,不需要我们删除两遍
1、使用 sequelize 的方式
因为 sequelize 不支持关联表删除,如果想完成这个操作,只能通过钩子函数的方式
钩子需要在model 中定义
代码语言:javascript复制function PersonModel(sequelize, DataTypes) {
return sequelize.define(
'person',
{
id: { type: DataTypes.STRING(1024) },
},
{
sequelize,
tableName: 'person', // 表名称
paranoid: true,
// beforeDestroy 和 afterDestroy hook 只会在具有 onDelete: 'CASCADE' 和 hooks: true 的关联上被调用
afterDestroy: async function (instance, options) {
return instance.getComment().then((c) => {
return Promise.all(c.map((i) => i.destroy()));
});
},
},
);
}
然后在进行model 关联的配置 onDelete = CASCADE,如下
代码语言:javascript复制person.hasMany(person_comment, {
foreignKey: 'user_id',
as: 'comment',
onDelete: 'CASCADE',
hooks: true,
});
然后只需要通过如下的 person 操作 , 该 person 下 的comment 也会被一并删除
代码语言:javascript复制await person.destroy({
where: {
id: 10,
},
});
其中 onDelete 的值和 sql 一样的,释义如下
1. CASCADE: 从父表中删除或更新对应的行,同时自动删除或更新子表中匹配的行。ON DELETE CANSCADE和ON UPDATE CANSCADE都被InnoDB所支持。
2. SET NULL: 从父表中删除或更新对应的行,同时将子表中的外键列设为空。注意,这些在外键列没有被设为NOT NULL时才有效。ON DELETE SET NULL和ON UPDATE SET SET NULL都被InnoDB所支持。
3. NO ACTION: InnoDB拒绝删除或者更新父表。
4. RESTRICT: 拒绝删除或者更新父表。指定RESTRICT(或者NO ACTION)和忽略ON DELETE或者ON UPDATE选项的效果是一样的。
5. SET DEFAULT: InnoDB目前不支持。
2、数据库自带外键约束
只要在数据库表中定义了两表关联的外键,那么当删除父表数据时,子表关联的数据也会被自动删除。这个操作不需要经过 sequelize,完全从 数据库层面配置
下面就是添加外键的 sql 语句,给 comment 加上外键 user_id ,关联 person 表的id
代码语言:javascript复制alter table `comment` add CONSTRAINT `useid_ref`
FOREIGN KEY (`user_id`)
REFERENCES `person` (`id`)
ON DELETE NO ACTION ON UPDATE NO ACTION
当 person 被删除的时候,它关联的comment 也会被自动删除
这也是比较推荐的方式
7 事务
数据库中我觉得是比较重要的一个功能了,凡是涉及到多个sql 操作的都必须开启事务
数据库事务是各种数据项进行各种操作时,这些操作要么都执行,要么都不执行。
比如说一起执行4个数据库操作,其中三个成功了,一个失败了,那么那三个就会回退成未操作的情况
比如下面这样,只要有一个 create 或者 destory 失败了,那么本次事务就全部失败,没有发生任何数据变动
代码语言:javascript复制const updateVideo = (transaction) => {
const promiseArr = [];
if (notExit) {
// 每个操作都要传入 本次事务的 transaction
promiseArr.push(video.create({ name: 1 }, { transaction }));
} else {
promiseArr.push(video.destory({ name: 1 }, { transaction }));
}
return Promise.all(promiseArr);
};
await sequelize.transaction((t) => {
return Promise.all([updateVideo(t)]);
});
看下开启一个事务失败后的日志是怎么样的
代码语言:javascript复制Executing (fe92f7fa-be8e-419b-a848-7b31e54d957b): START TRANSACTION;
Executing (fe92f7fa-be8e-419b-a848-7b31e54d957b): INSERT INTO `person` (`id`,`name`) VALUES (NULL,'new--2');
Executing (fe92f7fa-be8e-419b-a848-7b31e54d957b): INSERT INTO `person` (`id`,`name`) VALUES (NULL,'new—2');
// 因为我设置了name 为唯一索引,所以当我插入第二条同名数据时,就会报错,然后就开始回退,连同前一条数据被回滚
Executing (fe92f7fa-be8e-419b-a848-7b31e54d957b): ROLLBACK;
最后
鉴于本人能力有限,难免会有疏漏错误的地方,请大家多多包涵, 如果有任何描述不当的地方,欢迎后台联系本人