有面向对象编程基础的同学第一次使用 SQL 都会感到很不适应,在面向对象中所有的实体都是对象,而关系型数据库中实体通过表之间的关系表达
如果可以把关系型数据库中的 Relational 映射成面向对象中的 Object,这样在面向对象编程中就可以使用统一的模型编写代码了,这就是 ORM(Object-Relational Mapping)
Node.js 社区有很多流行的 ORM 框架,接下来在 egg.js 中通过 sequelize 改造之前的用户管理代码
安装配置 egg-sequelize
egg.js 中使用 sequelize 需要 egg-sequelize 和 mysql2 两个包
1. 安装
npm install --save egg-sequelize mysql2
3. 开启插件
在 config/plugin.js
中引入 egg-sequelize 插件
sequelize: {
enable: true,
package: 'egg-sequelize',
}
3. 配置插件
在 config/config.default.js
中移除 mysql 相关配置,编写 sequelize 配置,大部分配置字段含义和 mysql 一致
config.sequelize = {
dialect: 'mysql',
host: 'localhost',
port: '3306',
user: 'sunluyong',
password: '123456',
database: 'demo',
define: {
// 锁定表名和模型名一致,不自动转为复数
freezeTableName: true,
},
};
sequelize 默认会开启表名推断模式,自动将模型名复数做为表名,可以在配置中关闭,因为在大部分场景 Table 就是用来表达多个数据实例,无需命名为复数
定义模型
模型是把关系转为对象的操作,通过定义对象的方式表达数据库 Table 结构,egg.js 约定在 app/model 目录下定义模型,会在请求中挂载到 this.ctx.model
对象
app/model/user.js
module.exports = app => {
// 获取数据类型
const { INTEGER, STRING, TEXT, DATE } = app.Sequelize;
// 定义模型
const User = app.model.define('user', {
id: { type: INTEGER, primaryKey: true, autoIncrement: true, },
name: STRING(50),
config: TEXT('tiny'),
deleted: { type: INTEGER, defaultValue: 0, },
created_at: DATE,
updated_at: DATE,
}, {
// 禁用 created_at、updated_at 自动转换,使用 mysql 管理
// 方便后续迁移 ORM 等需求
timestamps: false,
});
return User;
};
整体过程就是把 user 表中的字段使用对象属性描述,完整参考 sequelise 模型定义
- 字段数据类型:type,所有 sequelize 数据类型
- 是否为主键:primaryKey
- 是否自增长:autoIncrement
- 默认值:defaultValue(为了和 ORM 解耦一般使用数据库定义)
默认 Sequelize 会自动向每个模型添加
createdAt
和updatedAt
字段。 这是在 Sequelize 级别完成的,未使用 SQL触发器 完成,直接 SQL 查询这些字段不会自动更新。使用timestamps: false
可以禁用此行为 ——时间戳
修改 user service
根据 MVP 模型,理论上 app/service/user.js 的修改不应该影响 controller 和 view,看一下 sequelize 如果完成 CRUD
get
async get(id) {
const user = await this.ctx.model.User.findByPk(id);
return user;
}
模型会被 egg.js 自动挂载在 this.ctx.model.User
下, findByPk
方法可以根据主键查找数据
list
async list(pageSize, pageNo, orderBy = 'id', order = 'ASC') {
const offset = pageSize * (pageNo - 1);
const { count, rows } = await this.ctx.model.User.findAndCountAll({
attributes: ['id', 'name', 'config'],
where: {
deleted: 0
},
order: [[orderBy, order],],
limit: toInt(pageSize),
offset
});
return {
users: rows,
pages: {
pageNo,
pageSize,
total: count,
},
};
}
相比于之前的拼接 sql,sequelize 提供了 findAndCountAll
方法来完成 select 和 count,当然也能通过独立的方法实现
const users = await this.ctx.model.User.findAll({
attributes: ['id', 'name', 'config'],
where: {
deleted: 0
},
order: [[orderBy, order],],
limit: toInt(pageSize),
offset
});
const total = await this.ctx.model.User.count({
where: {
id: {
[this.app.Sequelize.Op.eq]: 0,
}
}
});
此外 sequelize 还提供了几个有用的查询,看名字就知道含义,具体使用参考 模型查找
- findAll
- findByPk
- findOne
- findOrCreate
- findAndCountAll
复杂查询的使用方式可以参考 Model Querying - Basics - 模型查询(基础)
insert
async insert(user) {
const result = await this.ctx.model.User.create(user);
return {
insertId: result.dataValues.id,
};
}
update & soft delete
async update(user) {
const { id, ...attrs } = user;
const [numberOfAffectedRows, affectedRows] = await this.ctx.model.User.update(attrs, {
where: { id },
returning: true, // needed for affectedRows to be populated
plain: true, // makes sure that the returned instances are just plain objects
});
console.log(numberOfAffectedRows, affectedRows)
return { affectedRows };
}
// 软删除
async delete(id) {
const [numberOfAffectedRows, affectedRows] = await this.ctx.model.User.update({ deleted: 1 }, {
where: { id },
returning: true,
plain: true,
});
return { affectedRows };
}
update 方法参数和返回值需要注意一下
- returning:是否返回操作结果,默认是 false(其实蛮奇怪的,一般都需要获取返回结果)
- plain:设置为 true 保证返回值为简单对象
方法的返回值是一个数组,affectedRows 是影响的行数,具体可以参考 inserting-updating-destroying
hard delete
// 从表结构中移除
async hardDelete(id) {
const numAffectedRows = await this.ctx.model.User.destroy({
where: { id }
});
return numAffectedRows;
}
总结
改完 service 之后就可以运行 demo 了,第一次真正体会到 MVP 模式带来的好处,同时可以看出使用 ORM 后数据库操作和面向对象做了很好的结合,处理起来简单了很多
完整代码:https://github.com/Samaritan89/egg-demo/tree/v3
篇幅原因文章对 sequelize 只介绍了最基本的使用,感兴趣可以通过 sequelize 官网 深入学习