项目总结,彻底掌握NodeJS中如何使用Sequelize
前言
sequelize是什么? sequelize是基于NodeJs的ORM框架,它适用于不同的数据库,如:Postgres、MySQL、SQLite、MariaDB,我们可以通过sequelize对数据库进行一系列的操作。通常我用它与MySQL一起使用。该文是我在使用sequelize做完项目后对sequelize的系统整理。
准备工作
一、创建数据库和表,方便学习过程中书写示例代码
创建数据库 lesson
CREATE DATABASE IF NOT EXISTS lesson DEFAULT CHARSET utf8 COLLATE utf8_general_ci;
创建商品表 goods(示例使用,字段从简)
CREATE TABLE IF NOT EXISTS goods(
id INT(20) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT \'商品id\',
name VARCHAR(64) NOT NULL COMMENT \'商品名称\',
title VARCHAR(200) NOT NULL COMMENT \'商品标题\',
descript TEXT COMMENT \'商品描述\',
num BIGINT UNSIGNED NOT NULL COMMENT \'商品库存\',
cateid INT(10) UNSIGNED NOT NULL COMMENT \'分类id\',
price DECIMAL(10,2) NOT NULL DEFAULT \'0.00\' COMMENT \'商品价格\',
create_time DATE COMMENT \'创建时间\',
update_time DATE COMMENT \'修改时间\'
)ENGINE=InnoDB;
创建分类表 categorys (示例使用,字段从简)
CREATE TABLE IF NOT EXISTS categorys(
id INT(20) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT \'分类id\',
name VARCHAR(64) NOT NULL COMMENT \'分类名称\',
create_time DATE COMMENT \'创建时间\',
update_time DATE COMMENT \'修改时间\'
)ENGINE=InnoDB;
二、使用koa框架起一个服务
要学习使用sequelize,我们要先用node搭建基础服务,暴露接口方便测试,直观的看到代码效果。
引入包
在使用sequelize之前,我们需要引入两个包:sequelize、mysql2
npm install --save sequelize mysql2
连接数据库
要连接数据库,您必须new一个Sequelize的实例,给Sequelize构造函数传递参数完成连接。
const DB = new Sequelize(database,username,password,{
host:\'\',
dialect:\'\'
})
- database:要连接的数据库name
- username:登录数据库时的用户名
- password:登录数据库时的密码
- options:选项配置参数,为一个对象,内含许多配置属性,如:
- host:数据库主机
- port:数据库端口号
- dialect:指定要连接哪种类新的数据库,如:mysql、postgres等
- define:为模型定义默认选项
- timezone:时区
增
让我们先快速开始一个示例,从示例中学习需要注意的点 (请始终记住 我们使用的时准备阶段创建的数据库和表)
const {Sequelize,Model} = require(\'sequelize\');
# 连接数据库
const connect = new Sequelize(\'lesson\',\'root\',\'abc123456\',{
host:\'localhost\',
port:3306,
dialect:\'mysql\'
})
# 创建模型
class Category extends Model{}
Category.init({
name:Sequelize.STRING
},{
sequelize:connect,
tableName:\'categorys\'
})
# 新增一点数据
Category.create({name:\'房产\'})
当我们运行代码新增数据时,会发现数据没有添加成功且控制台报错。
为什么会出现这个问题呢? 这是因为Sequelize为了方便开发者,它会自动帮开发者添加上创建时间和更新时间,但是它给这两个时间规定了默认字段为createdAt
和updatedAt
,如果不做任何配置,我们创建数据库时创建时间和修改时间字段必须与它默认规定的相同。
与这两个字段相关联的有一个timestamp配置属性,这个属性可以启用或不启用Sequelize为开发者自动添加时间,默认为true。
对上面代码做一点点更改,如下:
# 连接数据库
const connect = new Sequelize(\'lesson\',\'root\',\'abc123456\',{
host:\'localhost\',
port:3306,
dialect:\'mysql\',
+ define:{
+ timestamp:false
+ }
})
在实例化Sequelize时,加上define参数配置,可以作用于全局。 我们如上配置全局timestamp为false,即表示关闭自动添加时间,再次运行代码 我们会发现数据库添加了一条数据。
解决了上面添加数据失败的问题,但是没有创建时间,这显然不是我们所希望的。 我们再对代码做一些改动,添加上create_time字段如下:
# 创建模型
class Category extends Model{}
Category.init({
name:Sequelize.STRING,
+ update_time:{
+ type:Sequelize.DATE
+ }
},{
sequelize:connect,
tableName:\'categorys\'
})
# 新增一点数据
+ const create_time = new Date().getTime();
Category.create({name:\'房产\',create_time:create_time})
这时再打开数据库,我们可以看到有一条具有创建时间的数据已经被添加上去了
做到这里,我们知道了,不用Sequelize自动添加时间的方式,我们可以自己手动编码添加,而且数据库的时间字段可以是任意合法的字符。
当然,我们开启了timestamp,数据库的时间字段也可以是任意合法的字符。 这时候需要我们配置字段别名,这是很简单的,只需要添加两行代码就可以了,如下:
# 连接数据库
const connect = new Sequelize(\'lesson\',\'root\',\'abc123456\',{
host:\'localhost\',
port:3306,
dialect:\'mysql\',
+ define:{
+ timestamp:true,
+ createdAt:\'create_time\',
+ updatedAt:\'update_time\'
+ }
})
上述篇幅,我们已经知道如何连接数据库和向数据库中添加数据,但是有些业务场景,不仅仅只是将我们在输入框中的数据原样插入到数据库中,可能会对输入的数据做一些改动,如用户注册时,密码需要加密然后再插入到数据库中。
需要实现这个需求,我们需要学习Sequelize的Setters、Getters和Virtuals
Setters: setter是为模型中字段的set()函数,它接收字段的值。它作用在数据的添加阶段,在set()函数中,我们可以获取字段的值并对原始值做一系列的变化,然后再设置最终值。
Getters: getter是为模型中字段的get()函数,它作用在数据的获取阶段,在get()函数中,我们可以先对返回值做一系列的操作,再返回最终值。
Virtuals: virtuals是虚拟字段,它不存在与数据库中。开发者可以使用它将两个字段拼接返回。
接下来我们来看看实际使用的栗子,代码如下:
Category.init({
name:{
type:Sequelize.STRING,
set(val){
const newVal = val + \'sb\'; // 所有添加的数据 name后都加上sb字符
this.setDataValue(\'name\',newVal);
},
get() {
const rawValue = this.getDataValue(\'name\'); //获取字段值
return rawValue ? rawValue.toUpperCase() : null; //将字段值的字母转换成大写输出
}
},
vitName:{ // 这是一个虚拟字段
type:Sequelize.VIRTUAL,
get(){
return `${this.name}`
}
}
},{
sequelize:connect,
tableName:\'categorys\'
})
查
到现在,我们已经学习了Sequelize很多的知识点了,接下来我们继续学习Sequelize的查询、修改、删除等方法。 在学习之前 我们还是先将数据库的数据维护一份正式一点的数据。
分别是分类表和商品表 如下图:
Sequelize为开发者提供了查找方法,默认情况下,所有查找器方法的结果都是模型类的实例,而不是简单的javascript对象。这意味着在数据库返回结果后,Sequelize会自动将所有内容打包到适当的实例对象中。在某些情况下,当结果太多时,这种包装可能效率低下,可以通过{raw:true}选项配置禁用。
查询选择器:
- findAll:它生成一条标准的SELECT查询语句,在不受到条件语句限制的情况下从表中检索所有数据。
- findByPk:根据提供的主键,从表中检索处一条数据。
- findOne:如果不提供查询选项的话,它会检索到表中第一条数据。
- count:检索数据库中所有数据,得到数据总数。
- findAndCountAll:可以获得数据总量和数据列表。这在做分页数据时时非常方便的。
现在我们来查询表数据,实现一些需求。
所有商品:
router.get(\'/pro/list\',async (ctx,next) => {
const result = await Product.findAll();
ctx.body = {
msg:result
}
})
得到的结果为:
{
"msg": [
{
"id": 2,
"name": "工装七分裤",
"title": "优衣库 男装 工装七分裤(卷边) 425146 UNIQLO ",
"descript": "优衣库 男装 工装七分裤(卷边) 425146 UNIQLO 优衣库 男装 工装七分裤(卷边) 425146 UNIQLO ",
"num": 158,
"cateid": 2,
"price": "149.00",
"create_time": "2020-07-25",
"update_time": "2020-07-25"
},
{
"id": 3,
"name": "花花公子旗舰短袖t恤",
"title": "花花公子旗舰短袖t恤男2020新款夏季潮牌宽松男士半袖潮流打底衫T ",
"descript": "花花公子旗舰短袖t恤男2020新款夏季潮牌宽松男士半袖潮流打底衫T 花花公子旗舰短袖t恤男2020新款夏季潮牌宽松男士半袖潮流打底衫T ",
"num": 3000,
"cateid": 2,
"price": "269.00",
"create_time": "2020-07-25",
"update_time": "2020-07-25"
},
{
"id": 4,
"name": "华为nova",
"title": "Huawei/华为 nova6 SE超级快充4800万AI四摄大运存nova6se 华为手机华为官方旗舰店 ",
"descript": "Huawei/华为 nova6 SE超级快充4800万AI四摄大运存nova6se 华为手机华为官方旗舰Huawei/华为 nova6 SE超级快充4800万AI四摄大运存nova6se 华为手机华为官方旗舰店",
"num": 2500,
"cateid": 3,
"price": "1999.00",
"create_time": "2020-07-25",
"update_time": "2020-07-25"
},
{
"id": 5,
"name": "荣耀30",
"title": "华为旗下荣耀30新品5G手机50倍超稳远摄麒麟985芯片全新智能手机正品官方旗舰店",
"descript": "华为旗下荣耀30新品5G手机50倍超稳远摄麒麟985芯片全新智能手机正品官方旗舰店华为旗下荣耀30新品5G手机50倍超稳远摄麒麟985芯片全新智能手机正品官方旗舰店",
"num": 3512,
"cateid": 3,
"price": "3299.00",
"create_time": "2020-07-25",
"update_time": "2020-07-25"
},
{
"id": 6,
"name": "美的变频空调",
"title": "美的i青春大1.5匹空调智能挂机冷暖壁挂式官方旗舰店",
"descript": "美的i青春大1.5匹空调智能挂机冷暖壁挂式官方旗舰店美的i青春大1.5匹空调智能挂机冷暖壁挂式官方旗舰店",
"num": 4215,
"cateid": 4,
"price": "2999.00",
"create_time": "2020-07-25",
"update_time": "2020-07-25"
},
{
"id": 7,
"name": "格力Gree",
"title": "Gree/格力 KFR-35GW 大1.5匹空调挂机智能变频冷暖一级壁挂式品悦",
"descript": "Gree/格力 KFR-35GW 大1.5匹空调挂机智能变频冷暖一级壁挂式品悦Gree/格力 KFR-35GW 大1.5匹空调挂机智能变频冷暖一级壁挂式品悦",
"num": 4215,
"cateid": 4,
"price": "4199.00",
"create_time": "2020-07-25",
"update_time": "2020-07-25"
},
{
"id": 8,
"name": "郁香菲2020夏季新品",
"title": "郁香菲2020夏季新品 荷叶边两穿收腰长款连衣裙纯色垂感飘逸长裙 ",
"descript": "郁香菲2020夏季新品 荷叶边两穿收腰长款连衣裙纯色垂感飘逸长裙 郁香菲2020夏季新品 荷叶边两穿收腰长款连衣裙纯色垂感飘逸长裙 ",
"num": 4215,
"cateid": 5,
"price": "699.00",
"create_time": "2020-07-25",
"update_time": "2020-07-25"
},
{
"id": 9,
"name": "美背文胸",
"title": "运动内衣女无钢圈聚拢冰丝无痕背心式胸罩夏季薄款调整型美背文胸 ",
"descript": "运动内衣女无钢圈聚拢冰丝无痕背心式胸罩夏季薄款调整型美背文胸 运动内衣女无钢圈聚拢冰丝无痕背心式胸罩夏季薄款调整型美背文胸 ",
"num": 10000,
"cateid": 5,
"price": "99.00",
"create_time": "2020-07-25",
"update_time": "2020-07-25"
}
]
}
条件查询
查询
router.get(\'/pro/list\',async (ctx,next) => {
const result = await Product.findAll({
where:{
id:4
}
});
ctx.body = {
msg:result
}
})
结果:
{
"msg": [
{
"id": 4,
"name": "华为nova",
"title": "Huawei/华为 nova6 SE超级快充4800万AI四摄大运存nova6se 华为手机华为官方旗舰店 ",
"descript": "Huawei/华为 nova6 SE超级快充4800万AI四摄大运存nova6se 华为手机华为官方旗舰Huawei/华为 nova6 SE超级快充4800万AI四摄大运存nova6se 华为手机华为官方旗舰店",
"num": 2500,
"cateid": 3,
"price": "1999.00",
"create_time": "2020-07-25",
"update_time": "2020-07-25"
}
]
}
查询
router.get(\'/pro/list\',async (ctx,next) => {
const result = await Product.findOne({
where:{
id:4
}
});
ctx.body = {
msg:result
}
})
# 等同于
router.get(\'/pro/list\',async (ctx,next) => {
const result = await Product.findByPk(4);
ctx.body = {
msg:result
}
})
结果一样,都是
{
"msg": {
"id": 4,
"name": "华为nova",
"title": "Huawei/华为 nova6 SE超级快充4800万AI四摄大运存nova6se 华为手机华为官方旗舰店 ",
"descript": "Huawei/华为 nova6 SE超级快充4800万AI四摄大运存nova6se 华为手机华为官方旗舰Huawei/华为 nova6 SE超级快充4800万AI四摄大运存nova6se 华为手机华为官方旗舰店",
"num": 2500,
"cateid": 3,
"price": "1999.00",
"create_time": "2020-07-25",
"update_time": "2020-07-25"
}
}
注意:findAll和findOne、findByPk方法得到的结果是不同的,findAll得到的结果是数组包含着数据项,而findOne、findByPk得到的结果就是一条数据
分页查询
我们在项目中经常会需要分页查询,一次性查询所有的数据,无论是服务器还是前端页面显示都是不友好的。Sequelize为开发者提供了分页机制,我们只需配置offset
和limit
两个属性就可以实现分页。
- offset:偏移量,以0开始做偏移
- limit:限制条数
值得我们注意的是offset是以0开始做偏移的,通常前端分页会传递两个参数:pageCurrent
和pageSize
。 pageCurrent通常是以1开始的。所以后端接收到pageCurrent后需要减1传递给Sequelize做偏移。
router.get(\'/pro/list\',async (ctx,next) => {
const { pageCurrent,pageSize } = ctx.request.body;
let limit = pageSize ? pageSize : limit;
let offset = pageCurrent ? (pageCurrent - 1)*limit : offset;
const result = await Product.findAll({
offset,
limit
});
ctx.body = {
msg:result
}
})
指定字段
router.get(\'/pro/list\',async (ctx,next) => {
const result = await Product.findOne({
attributes:[\'name\',\'price\']
});
ctx.body = {
msg:result
}
})
结果
{
"msg": {
"name": "工装七分裤",
"price": "149.00"
}
}
排除字段
router.get(\'/pro/list\',async (ctx,next) => {
const result = await Product.findOne({
attributes:{
exclude:[\'descript\']
}
});
ctx.body = {
msg:result
}
})
结果
{
"msg": {
"id": 2,
"name": "工装七分裤",
"title": "优衣库 男装 工装七分裤(卷边) 425146 UNIQLO ",
"num": 158,
"cateid": 2,
"price": "149.00",
"create_time": "2020-07-25",
"update_time": "2020-07-25"
}
}
排序查询
配置order选项,order是一个数组,字段名和排序方式。 desc(降序),asc(升序)
router.get(\'/pro/list\',async (ctx,next) => {
const result = await Product.findAll({
order:[
[\'id\',\'desc\']
]
});
ctx.body = {
msg:result
}
})
关联查询
sequelize提供了四种类型的关联关系
- hasOne关联:与目标建立一对一关联关系,外键存在于目标模型中
- belongsTo关联:与目标建立一对一关联关系,外键存在于源模型中
- hasMany:与目标建立一对多关联关系,外键存在于目标模型中
- belongsToMany:与目标建立多对多关联关系,外键存在于源模型中
# 外键存在于源模型中
Product.belongsTo(Category,{
foreignKey:\'cateid\',
targetKey:\'id\'
})
# 外键存在于目标模型中
Product.hasOne(Category,{
as:\'cate\',
foreignKey:\'id\',
targetKey:\'cateid\'
})
上面两个代码示例, Product模型是源模型,Category模型是目标模型。cateid
是Product中定义的字段列,可以看到使用belongsTo
时外键foreignKey
的值是cateid
使用hasOne
时外键foreignKey
的值是id
,这个id是Category中的主键id。
示例代码:
Product.hasOne(Category,{
foreignKey:\'id\',
targetKey:\'cateid\'
})
router.get(\'/pro/list\',async (ctx,next) => {
const result = await Product.findOne({
include:{
model:Category
}
});
ctx.body = {
msg:result
}
})
得到结果:
{
"msg": {
"id": 2,
"name": "工装七分裤",
"title": "优衣库 男装 工装七分裤(卷边) 425146 UNIQLO ",
"descript": "优衣库 男装 工装七分裤(卷边) 425146 UNIQLO 优衣库 男装 工装七分裤(卷边) 425146 UNIQLO ",
"num": 158,
"cateid": 2,
"price": "149.00",
"create_time": "2020-07-25",
"update_time": "2020-07-25",
"Category": {
"id": 2,
"name": "男装",
"create_time": "2020-07-25",
"update_time": "2020-07-25"
}
}
}
商品信息中就有了该商品所属分类的信息,可以看到返回的商品信息字段是Category即是以定义的类名Category为字段,我们可以更改它。
Product.hasOne(Category,{
+ as:\'cate\',
foreignKey:\'id\',
targetKey:\'cateid\'
})
router.get(\'/pro/list\',async (ctx,next) => {
const result = await Product.findOne({
include:{
model:Category,
+ as:\'cate\'
}
});
ctx.body = {
msg:result
}
})
这样分类信息的字段就跟更改成了cate
改
Category.update(param,option)
- param:修改的参数集合,Object
- option:配置选项
router.post(\'/edit\',async (ctx,next) => {
await Category.update({
name:\'男装内裤\'
},{
where:{
id:2
}
})
})
上面代码 修改id等于2的那条数据,将name字段的值更改为\'男装内裤\'。
删
Category.destroy(option)
Category.destroy({
where:{
id:2
}
})