15 mins
从0认识node之ORM框架

如何理解ORM框架:object relational mapping(对象-关系-映射)

ORM框架就是将关系型数据库映射成面向对象模型,方便我们以操作对象的方式操作数据库

  • ORM框架的内部逻辑会实现sql防注入,对数据库操作只需要调用框架提供的对应api即可,不需要担心原生sql的问题
  • 提高开发效率,不用写繁琐重复的sql语句
  • 调试sql会比较麻烦,但是很多框架都支持写原生sql
  • 复杂的sql语句,如果很难通过框架语法写出来,故可以用原生sql

node主流的ORM框架h2

TypeORM、Prisma、Sequelize

我们着重说一下Sequelize,它是一种基于promise的node的ORM框架,支持多种数据库(MySQL、PostgreSQL、SQLite、MSSQL等),有强大的事务支持,满足我们基本的操作要求,以及简单的关联查询,涉及到较为复杂的多表关联查询大部分还是用SQL

如果我们开发的项目是单纯使用js开发的,那么使用Sequelize是一个不错的选择

Sequelize安装

Terminal window
# sequelize
yarn add sequelize
# 对应的数据库驱动:mysql为例
yarn add mysql2

配置:连接数据库

./config/sequelize.js
// const {Sequelize} = require('sequelize')
import { Sequelize } from 'sequelize'
// 要连接的我们数据库的名字
// 用户名
// 密码
// 对象:「host,dialect(数据库类型:可能是mysql),port」
const sequelize = new Sequelize('test', 'root', '6cT!@xk4', {
host: '38.175.195.93',
dialect: 'mysql',
port: 3307,
})
// 测试成功与否,可以使用authenticate()方法
// 核心:把 await 包在 async 函数里,避免顶层 await
// async function testConnection() {
// try {
// await sequelize.authenticate();
// console.log('数据库连接成功');
// } catch (error) {
// console.error('数据库连接失败:', error);
// }
// }
// // 执行函数
// testConnection();
// 或者异步立即执行函数
;(async function () {
try {
await sequelize.authenticate()
console.log('数据库连接成功')
} catch (error) {
console.error('数据库连接失败:', error.message)
}
})()

配置部分总结:

  • package.json 加 "type": "module" + 安装 mysql2 依赖,这是代码能运行的前提;
  • 异步立即执行函数写法正确,ES5/ES6 版本均可,推荐箭头函数版更简洁
  • 连接失败优先排查:端口放行、远程授权、数据库存在与否这三个问题

定义数据表模型同步和初始操作h2

创建数据表:通过Sequelize框架创建数据库的表,开发效率低,创建字段比在可视化工具中慢,还不清晰

所以一般只会在数据库可视化工具中完成表的创建

模型是Sequelize的本质

模型是代表数据库中表的抽象,在Sequelize中,它是一个Model的扩展类

该模型告诉Sequelize有关它代表的实体的几件事,例如数据库中的表的名称,以及它具有的列(及其数据类型)

Sequelize中的模型有一个名称,此名称不必与它在数据库中表示的名称相同,通常,模型具有单数名称(比如User),而表具有复数名称(比如Users)

./models/Account.js
// 使用Sequelize来创建数据库表
// 引入Sequelize和数据类型
import { Sequelize, DataTypes } from 'sequelize'
// 引入sequelize实例,记得在../config/sequelize里抛出
import sequelize from '../config/sequelize.js'
const Account = sequelize.define(
'Account',
{
// 定义字段/模型属性
id: {
autoIncrement: true,
primaryKey: true,
allowNull: false,
type: DataTypes.BIGINT,
},
username: {
type: DataTypes.STRING(255),
allowNull: false,
unique: true,
comment: '用户名',
},
age: {
type: DataTypes.BIGINT,
allowNull: true,
comment: '年龄',
},
password: {
type: DataTypes.STRING(255),
allowNull: false,
comment: '密码',
},
hobby: {
type: DataTypes.STRING(255),
allowNull: true,
comment: '爱好',
},
gmt_create: {
type: DataTypes.DATE,
allowNull: true,
comment: '创建时间',
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
},
gmt_modified: {
type: DataTypes.DATE,
allowNull: true,
comment: '修改时间',
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
},
},
{
// 定义其他参数类型
tableName: 'account',
timestamps: false,
}
)
// 同步模型到数据库表
// Account.sync() 如果表不存在,则创建该表,否则,不执行任何操作
// Account.sync({ force: true }) 如果表存在,则先删除表,再创建表
// Account.sync({ alter: true }) 如果表存在,则检查表与模型是否匹配,如果不匹配,则执行相应的更改以使表匹配模型
// ;(async () => {
// await Account.sync({ alter: true })
// console.log('成功同步')
// })()
export default Account

尝试写一个接口,使用刚才定义好的模型插入数据

app.js
import express from 'express'
import cors from 'cors'
import Account from './models/Account.js'
const app = express()
const PORT = 8081
app.use(cors())
app.get('/account', async (req, res) => {
try {
// 这里会插入数据,包括 age 和 hobby
await Account.create({
username: 'felix',
password: '123456',
age: 18,
hobby: 'coding',
})
res.send('添加成功')
} catch (error) {
console.error('插入失败:', error)
res.status(500).send('添加失败: ' + error.message)
}
})
app.listen(PORT, () => {
console.log(`服务启动在: http://127.0.0.1:8081`)
})

提高模型同步的开发效率h2

我们刚才体会到手动写模型同步的效率问题,因此,我们可以使用sequelize-auto工具来自动生成模型代码

具体的逻辑是,我们手动在数据库中创建好表结构,然后运行命令,生成模型代码

Terminal window
# 安装
yarn add sequelize-auto
# 运行命令
node_modules/sequelize-auto/bin/sequelize-auto -d test -h 主机地址 -u 用户名 -p 端口号 -x '密码' -e mysql --cm p --lang esm

可以简化一下操作,在package.json中添加脚本,注意最后加上了--lang esm,是为了生成ES模块代码

"scripts": {
"models": "node_modules/sequelize-auto/bin/sequelize-auto"
}

然后通过yarn调用脚本,并补充参数

Terminal window
yarn models -d test -h 主机地址 -u 用户名 -p 端口号 -x '密码' -e mysql --cm p --lang esm

这样直接会生成模型代码,并且会自动同步数据库表结构,覆盖原来的模型代码,还有创一个init-models.js文件

./models/init-models.js
import _sequelize from 'sequelize'
const DataTypes = _sequelize.DataTypes
import _Account from './account.js'
import _Nightclub from './nightclub.js'
export default function initModels(sequelize) {
const Account = _Account.init(sequelize, DataTypes)
const Nightclub = _Nightclub.init(sequelize, DataTypes)
return {
Account,
Nightclub,
}
}

现在我们需要更新一下app.js,因为我们不能再直接import Account模型了,需要通过init-models.js来获取

app.js
import express from 'express'
import cors from 'cors'
// import Account from './models/Account.js'
import models from './models/init-models.js'
import sequelize from './config/sequelize.js'
const app = express()
const PORT = 8081
app.use(cors())
app.get('/account', async (req, res) => {
try {
// 不能再用Account了
// await Account.create({
// username: 'felix',
// password: '123456',
// age: 18,
// hobby: 'coding'
// })
// 需要将sequelize传入models,再取到Account
await models(sequelize).Account.create({
username: 'felix1',
password: '123456',
age: 18,
hobby: 'coding',
})
res.send('添加成功')
} catch (error) {
console.error('插入失败:', error)
res.status(500).send('添加失败: ' + error.message)
}
})
app.listen(PORT, () => {
console.log(`服务启动在: http://127.0.0.1:8081`)
})

其实,我们不太建议在app.js里放入数据的逻辑,可以在sequelize.js里调用init-models.js,然后除了导出原来的sequelize对象,还导出models对象

这样做的好处是什么呢?因为后面还可能有很多需求,比如多表关联查询,或者事务操作

./config/sequelize.js
import { Sequelize } from 'sequelize'
import initModels from '../models/init-models.js' // 新增
// 要连接的我们数据库的名字
// 用户名
// 密码
// 对象:「host,dialect(数据库类型:可能是mysql),port」
const sequelize = new Sequelize('test', 'root', '6cT!@xk4', {
host: '38.175.195.93',
dialect: 'mysql',
port: 3307,
})
// 或者异步立即执行函数
;(async function () {
try {
await sequelize.authenticate()
console.log('数据库连接成功')
} catch (error) {
console.error('数据库连接失败:', error.message)
}
})()
const models = initModels(sequelize) // 新增
// 这是一个默认导出 (export default)
// 它导出的是一个整体的对象 { Account: ..., Nightclub: ..., sequelize: ... }
export default { ...models, sequelize } // 修改

然后在app.js里直接引入models对象即可

app.js
import express from 'express'
import cors from 'cors'
// import Account from './models/Account.js'
// import models from './models/init-models.js' // 修改
import models from './config/sequelize.js' // 修改
const { Account } = models // 先引入models,再从里面解构出我们需要的 Account
const app = express()
const PORT = 8081
app.use(cors())
app.get('/account', async (req, res) => {
try {
// 直接使用sequelize.js封装好的models对象Account
await Account.create({
username: 'felix2',
password: '123456',
age: 19,
hobby: 'coding & badminton',
})
res.send('添加成功')
} catch (error) {
console.error('插入失败:', error)
res.status(500).send('添加失败: ' + error.message)
}
})
app.listen(PORT, () => {
console.log(`服务启动在: http://127.0.0.1:8081`)
})

刚才我们一直都是用插入数据举例,现在有了数据,我们来试试查询

import express from 'express'
import cors from 'cors'
import models from './config/sequelize.js'
const { Account } = models
const app = express()
const PORT = 8081
app.use(cors())
app.get('/insert-account', async (req, res) => {
try {
await Account.create({
username: 'felix3',
password: '123456',
age: 19,
hobby: 'coding & badminton',
})
res.send('添加成功')
} catch (error) {
console.error('插入失败:', error)
res.status(500).send('添加失败: ' + error.message)
}
})
// 查修用户之类的
app.get('/select-account', async (req, res) => {
try {
const account = await Account.findAll({
where: { username: 'felix3' },
raw: true,
})
res.send(account)
} catch (error) {
console.error('查询失败:', error)
res.status(500).send('查询失败: ' + error.message)
}
})
app.listen(PORT, () => {
console.log(`服务启动在: http://127.0.0.1:8081`)
})

但是现在where里面只有一个条件,如果有多个条件并且是或者的话,就可以使用Op了,详见Sequelize Operators

app.js
// ...
// 出现或者的话 就不能单纯只用这个where了,得用Op操作符
import { Op } from 'sequelize'
app.get('/select-account-multichoice', async (req, res) => {
try {
const account = await Account.findAll({
where: {
[Op.or]: [{ username: 'felix' }, { age: 19 }],
},
raw: true,
})
res.send(account)
} catch (error) {
console.error('查询失败:', error)
res.status(500).send('查询失败: ' + error.message)
}
})
// ...

更新和删除操作

app.js
// ...
app.get('/update-account', async (req, res) => {
try {
await Account.update(
{
username: '王二麻子',
},
{
where: {
id: 3,
},
}
)
res.send('更新成功')
} catch (error) {
console.error('更新失败:', error)
res.status(500).send('更新失败: ' + error.message)
}
})
app.get('/delete-account', async (req, res) => {
try {
const account_just_been_deleted = await Account.destroy({
where: {
[Op.and]: [{ username: 'felix3' }, { age: 20 }],
},
})
console.log(account_just_been_deleted)
res.send('删除成功')
} catch (error) {
console.error('删除失败:', error)
res.status(500).send('删除失败: ' + error.message)
}
})

一对多关系表关联查询,常规操作是用两层循环,但是ORM框架可以直接通过include来实现关联查询

我们来做一个尝试,现在我们手动创建两张表Student和Desk,Desk表的外键为student_id,关联Student表的id字段

Desk:
id|color |student_id|
--+------+----------+
1|red | 3|
2|yellow| 4|
3|blue | 2|
4|black | 1|
Student:
id|username|
--+--------+
1|范老大 |
2|范老二 |
3|范老三 |
4|范老四 |

然后照常用yarn models -d test -h 主机地址 -u 用户名 -p 端口号 -x '密码' -e mysql --cm p --lang esm调用脚本,生成模型代码,并更新了init-models.js

init-models.js
import _sequelize from 'sequelize'
const DataTypes = _sequelize.DataTypes
import _Desk from './Desk.js'
import _Student from './Student.js'
import _Account from './account.js'
import _Nightclub from './nightclub.js'
export default function initModels(sequelize) {
const Desk = _Desk.init(sequelize, DataTypes)
const Student = _Student.init(sequelize, DataTypes)
const Account = _Account.init(sequelize, DataTypes)
const Nightclub = _Nightclub.init(sequelize, DataTypes)
return {
Desk,
Student,
Account,
Nightclub,
}
}

然后需要在./config/sequelize.js定义学生和桌子的关联关系

因为当你定义了关联关系后,Sequelize才能做到以下几点:

  • 自动生成Join语句:当你等会写include: [Student]时,seqeulize只有在知道belongsTo的情况下,才懂得生成LEFT OUTER JOIN Student ON Desk.student_id = Student.id
  • 别名映射(as):在我们配置里写的as: 'studentDetail',告诉sequelize查询出来的学生数据,不要叫Student,而要挂载在desk.studentDetail这个属性下,方便前端使用
./config/sequelize.js
// ...
// 学生和桌子一对一关系模型
models.Student.hasOne(models.Desk, {
foreignKey: 'student_id',
as: 'deskDetail',
})
models.Desk.belongsTo(models.Student, {
foreignKey: 'student_id',
as: 'studentDetail',
})
// ...

接着就可以在app.js里写关联查询的接口了

app.js
// ...
app.get('/student-desk', async (req, res) => {
try {
const records = await Desk.findAll({
include: [
{
model: Student,
as: 'studentDetail',
},
],
})
res.send({
msg: '关联查询成功',
data: records,
})
} catch (error) {
console.error('关联查询失败:', error)
res.status(500).send('关联查询失败: ' + error.message)
}
})
// ...

总结一下,Sequelize一共有四种标准的关联关系,构成了所有复杂的业务逻辑:

关联方法含义对应的生活场景外键在哪里?(重点)
hasOne一对一 (拥有)一个学生有一个档案外键在对方表里
belongsTo一对一 (属于)一个档案属于一个学生外键在自己表里
hasMany一对多 (拥有)一个班级有多个学生外键在对方表里
belongsToMany多对多一个学生选多门课需要一张中间表

先定义模型:必须先有 Student 和 Desk 的类定义(init-models.js 做的就是这一步)。

后建立关系:在模型加载完之后,立马执行 hasOne/belongsTo。

最后使用:只有关系建立好了,你才能在 app.js 里用 include 进行连表查询。

Comments