摘自 2023/4 笔记
对象-关系映射(Object-Relational Mapping,简称ORM),用于实现面向对象编程语言里不同类型系统之间数据的转换。
面向对象是从软件工程基本原则(如耦合、聚合、封装)的基础上发展起来的,而关系数据库则是从数学理论发展而来的,两套理论存在显著的区别。为了解决这个不匹配的现象,对象关系映射技术应运而生。
简单的说,ORM是通过使用描述对象和数据库之间映射的元数据,将程序中的对象自动持久化到关系数据库中。
参考:https://en.wikipedia.org/wiki/Object%E2%80%93relational_mapping
为什么要使用ORM框架
- 提高开发效率,降低开发成本
- 使开发更加对象化
- 可以很方便地引入数据缓存等附加功能
Sequelize
Sequelize是一个基于 promise 的 Node.js ORM框架, 目前支持 Postgres, MySQL, MariaDB, SQLite 以及 Microsoft SQL Server. 它具有强大的事务支持, 关联关系, 预读和延迟加载,读取复制等功能。
- 安装
npm install --save egg-sequelize mysql2
- 引入插件,
config/plugin.js
文件引入egg-sequelize
exports.sequelize = {
enable: true,
package: 'egg-sequelize',
};
config/config.default.js
配置数据库连接
exports.sequelize = {
dialect: 'mysql',
host: '127.0.0.1',
port: 3306,
database: 'database',
username: 'username',
password: '*******',
define: {
freezeTableName: true,//禁止转换为复数
},
};
踩坑:默认情况下,当未提供表名时,Sequelize 会自动将模型名复数并将其用作表名,导致sql语句报错找不到对应的表,可以使用 freezeTableName: true
参数停止自动复数化
定义Model
以app/model/user.js
为例
sequelize.define(modelName, attributes, options)
,第一个参数为模型名;第二个为定义的模型字段,基本为表格内字段,也可以定义表格中不存在的字段;第三个参数为配置。
'use strict';
module.exports = (app) => {
const { STRING, INTEGER, DATE } = app.Sequelize;
const User = app.model.define('t_user', {
id: { type: INTEGER, primaryKey: true, autoIncrement: true },
name: STRING(30),
age: INTEGER
});
return User;
};
如果模型名和表名不一致,可以直接配置表名称:
sequelize.define('User', {
// ... (属性)
}, {
tableName: 'Employees'
});
如果需要修改返回字段,举例:数据库中字段为user_id
,想直接返回id
,可以修改映射关系
id: {
type: NUMBER,
field: 'user_id'
},
设置默认值
sequelize.define('User', {
name: {
type: DataTypes.STRING,
defaultValue: "Nick"
},
time: {
type: DataTypes.DATETIME,
defaultValue: DataTypes.NOW // 当前时间
}
});
配置校验方法
const User = sequelize.define('user', {
name: {
type: STRING,
validate: {
len: [3, 6]
}
}
});
定义数据表中不存在的字段,引入VIRTUAL
类型,定义getter和setter
const { STRING, VIRTUAL } = app.Sequelize;
const User = sequelize.define('user', {
firstName: STRING,
lastName: STRING,
fullName: {
type: VIRTUAL,
get() {
return `${this.firstName} ${this.lastName}`;
},
set(value) {
throw new Error('不要尝试设置 `fullName` 的值!');
}
}
});
另外,创建时间和更新时间可以交给sequelize
管理,这样在代码实现过程中,你不需要对创建时间和更新时间字段进行更新维护。配置options字段:
{
createdAt: 'create_time',
updatedAt: 'update_time'
}
踩坑:默认情况下,Sequelize 自动向每个模型添加 createdAt 和 updatedAt 字段。
如果你并不需要这两个字段,需要在Model层中配置timestamps: false
{
timestamps: false
}
如果只要其中一个
// 启用时间戳!
timestamps: true,
// 不想要 createdAt
createdAt: false,
// 想要 updatedAt 但是希望名称叫做 updateTimestamp
updatedAt: 'updateTimestamp'
模型查询
INSERT
// 创建一个新用户
const jane = await User.create({ firstName: "Jane", lastName: "Doe" });
批量创建
Model.bulkCreate
方法接收数组对象。
const users = await User.bulkCreate([
{ name: 'Jack Sparrow' },
{ name: 'Davy Jones' }
]);
默认情况下,bulkCreate
不会在要创建的每个对象上运行验证,可以通过配置validate: true
, 但这会降低性能.
UPDATE
update
方法第一个参数为更新字段,第二个传入被更新的记录条件
// 将所有没有姓氏的人更改为 "Doe"
await User.update({ lastName: "Doe" }, {
where: {
lastName: null
}
});
DELETE
// 删除所有名为 "Jane" 的人
await User.destroy({
where: {
firstName: "Jane"
}
});
可以使用 TRUNCATE
SQL,销毁所有内容。销毁后自增ID会重新从1开始
await User.destroy({
truncate: true
});
SELECT
findByPk
提供主键从表中仅获得一个条目
const project = await Project.findByPk(123);
findOne
获得符合条件的第一个条目
const project = await Project.findOne({ where: { title: 'My Title' } });
fineOrCreate
如果能找到一个满足查询参数的结果则返回,否则创建一个条目
const [user, created] = await User.findOrCreate({
where: { username: 'sdepold' }
})
findAll
返回符合条件所有条目
// 查询所有用户
const users = await User.findAll();
// 查询年龄为25的所有用户
const users = await User.findAll({
where: { age: 25 }
});
findAndCountAll
结合了 findAll 和 count 的便捷方法,在处理与分页有关的查询时非常有用
const { count, rows } = await Project.findAndCountAll({
where: {
title: {
[Op.like]: 'foo%'
}
},
offset: 10,
limit: 2
});
联表查询
Model层定义关联关系
- 一对一关系:
hasOne
和belongsTo
关联一起使用 - 一对多关系:
hasMany
和belongsTo
关联一起使用 - 多对多关系:两个
belongsToMany
调用一起使用
定义关联键
如果没有自定义外键,默认使用id进行关联。如果要使用其他键值,需要在Model中配置。
举例,系统中业务下有很多个模块,在模块表中记录的外键是project_id
,对应业务表中的id
app.model.Project.hasMany(app.model.Module, {
as: 'module',
foreignKey: 'project_id',
sourceKey: 'id',
});
app.model.Module.belongsTo(app.model.Project, {
as: 'project',
foreignKey: 'project_id',
sourceKey: 'id',
});
联表查询
可以在同一模型之间定义多个关联. 需要为它们定义不同的别名
举例,查询模块时将所属业务信息也返回给前端
await Module.findOne({
where: {
id
},
include: {
model: ctx.model.Project,
as: 'project'
}
})
事务
默认情况下,Sequelize 不使用事务. 但是,对于 Sequelize 的生产环境使用,你绝对应该将 Sequelize 配置为使用事务。
Sequelize 支持两种使用事务的方式:
- 非托管事务: 提交和回滚事务应由用户手动完成(通过调用适当的 Sequelize 方法)
- 托管事务: 如果引发任何错误,Sequelize 将自动回滚事务,否则将提交事务
官方非托管事务示例:
// 首先,我们开始一个事务并将其保存到变量中
const t = await sequelize.transaction();
try {
// 进行调用时将此事务作为参数传递:
const user = await User.create({
firstName: 'Bart',
lastName: 'Simpson'
}, { transaction: t });
await user.addSibling({
firstName: 'Lisa',
lastName: 'Simpson'
}, { transaction: t });
// 如果执行到此行,且没有引发任何错误.
// 我们提交事务.
await t.commit();
} catch (error) {
// 如果执行到达此行,则抛出错误.
// 我们回滚事务.
await t.rollback();
}
踩坑:如果按照这个教程编写事务代码,就会报错TypeError: Cannot read property ‘transaction‘ of undefined
。原因是这里的sequelize
需要从数据连接对象获取,而不是直接require('sequelize')
,官方文档这里没有写清楚,导致初次使用的人基本上都踩坑了。
解决:将数据库连接封装成一个单独的模块后导出实例给事务使用
const { Sequelize } = require('sequelize')
const sequelize = new Sequelize({
dialect: 'mysql', // support: mysql, mariadb, postgres, mssql
database: 'database',
host: '10.133.145.xxx',
port: 3306,
username: 'xxx',
password: 'xxx'
})
module.exports = {
sequelize
}
发表回复