有面向对象编程基础的同学第一次使用 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 populatedplain: 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 官网 深入学习
