如何理解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安装
# sequelizeyarn add sequelize# 对应的数据库驱动:mysql为例yarn add mysql2配置:连接数据库
// 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)
// 使用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尝试写一个接口,使用刚才定义好的模型插入数据
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工具来自动生成模型代码
具体的逻辑是,我们手动在数据库中创建好表结构,然后运行命令,生成模型代码
# 安装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调用脚本,并补充参数
yarn models -d test -h 主机地址 -u 用户名 -p 端口号 -x '密码' -e mysql --cm p --lang esm这样直接会生成模型代码,并且会自动同步数据库表结构,覆盖原来的模型代码,还有创一个init-models.js文件
import _sequelize from 'sequelize'const DataTypes = _sequelize.DataTypesimport _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来获取
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对象
这样做的好处是什么呢?因为后面还可能有很多需求,比如多表关联查询,或者事务操作
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对象即可
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
// ...// 出现或者的话 就不能单纯只用这个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.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
import _sequelize from 'sequelize'const DataTypes = _sequelize.DataTypesimport _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这个属性下,方便前端使用
// ...// 学生和桌子一对一关系模型models.Student.hasOne(models.Desk, { foreignKey: 'student_id', as: 'deskDetail',})
models.Desk.belongsTo(models.Student, { foreignKey: 'student_id', as: 'studentDetail',})
// ...接着就可以在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