有面向对象编程基础的同学第一次使用 SQL 都会感到很不适应,在面向对象中所有的实体都是对象,而关系型数据库中实体通过表之间的关系表达

如果可以把关系型数据库中的 Relational 映射成面向对象中的 Object,这样在面向对象编程中就可以使用统一的模型编写代码了,这就是 ORM(Object-Relational Mapping)

Node.js 社区有很多流行的 ORM 框架,接下来在 egg.js 中通过 sequelize 改造之前的用户管理代码

安装配置 egg-sequelize

egg.js 中使用 sequelize 需要 egg-sequelize 和 mysql2 两个包

1. 安装

  1. npm install --save egg-sequelize mysql2

3. 开启插件

config/plugin.js 中引入 egg-sequelize 插件

  1. sequelize: {
  2. enable: true,
  3. package: 'egg-sequelize',
  4. }

3. 配置插件

config/config.default.js 中移除 mysql 相关配置,编写 sequelize 配置,大部分配置字段含义和 mysql 一致

  1. config.sequelize = {
  2. dialect: 'mysql',
  3. host: 'localhost',
  4. port: '3306',
  5. user: 'sunluyong',
  6. password: '123456',
  7. database: 'demo',
  8. define: {
  9. // 锁定表名和模型名一致,不自动转为复数
  10. freezeTableName: true,
  11. },
  12. };

sequelize 默认会开启表名推断模式,自动将模型名复数做为表名,可以在配置中关闭,因为在大部分场景 Table 就是用来表达多个数据实例,无需命名为复数

定义模型

模型是把关系转为对象的操作,通过定义对象的方式表达数据库 Table 结构,egg.js 约定在 app/model 目录下定义模型,会在请求中挂载到 this.ctx.model 对象
app/model/user.js

  1. module.exports = app => {
  2. // 获取数据类型
  3. const { INTEGER, STRING, TEXT, DATE } = app.Sequelize;
  4. // 定义模型
  5. const User = app.model.define('user', {
  6. id: { type: INTEGER, primaryKey: true, autoIncrement: true, },
  7. name: STRING(50),
  8. config: TEXT('tiny'),
  9. deleted: { type: INTEGER, defaultValue: 0, },
  10. created_at: DATE,
  11. updated_at: DATE,
  12. }, {
  13. // 禁用 created_at、updated_at 自动转换,使用 mysql 管理
  14. // 方便后续迁移 ORM 等需求
  15. timestamps: false,
  16. });
  17. return User;
  18. };

整体过程就是把 user 表中的字段使用对象属性描述,完整参考 sequelise 模型定义

  1. 字段数据类型:type,所有 sequelize 数据类型
  2. 是否为主键:primaryKey
  3. 是否自增长:autoIncrement
  4. 默认值:defaultValue(为了和 ORM 解耦一般使用数据库定义)

    默认 Sequelize 会自动向每个模型添加 createdAtupdatedAt 字段。 这是在 Sequelize 级别完成的,未使用 SQL触发器 完成,直接 SQL 查询这些字段不会自动更新。使用 timestamps: false 可以禁用此行为 ——时间戳

修改 user service

根据 MVP 模型,理论上 app/service/user.js 的修改不应该影响 controller 和 view,看一下 sequelize 如果完成 CRUD

get

  1. async get(id) {
  2. const user = await this.ctx.model.User.findByPk(id);
  3. return user;
  4. }

模型会被 egg.js 自动挂载在 this.ctx.model.User 下, findByPk 方法可以根据主键查找数据

list

  1. async list(pageSize, pageNo, orderBy = 'id', order = 'ASC') {
  2. const offset = pageSize * (pageNo - 1);
  3. const { count, rows } = await this.ctx.model.User.findAndCountAll({
  4. attributes: ['id', 'name', 'config'],
  5. where: {
  6. deleted: 0
  7. },
  8. order: [[orderBy, order],],
  9. limit: toInt(pageSize),
  10. offset
  11. });
  12. return {
  13. users: rows,
  14. pages: {
  15. pageNo,
  16. pageSize,
  17. total: count,
  18. },
  19. };
  20. }

相比于之前的拼接 sql,sequelize 提供了 findAndCountAll 方法来完成 select 和 count,当然也能通过独立的方法实现

  1. const users = await this.ctx.model.User.findAll({
  2. attributes: ['id', 'name', 'config'],
  3. where: {
  4. deleted: 0
  5. },
  6. order: [[orderBy, order],],
  7. limit: toInt(pageSize),
  8. offset
  9. });
  10. const total = await this.ctx.model.User.count({
  11. where: {
  12. id: {
  13. [this.app.Sequelize.Op.eq]: 0,
  14. }
  15. }
  16. });

此外 sequelize 还提供了几个有用的查询,看名字就知道含义,具体使用参考 模型查找

  1. findAll
  2. findByPk
  3. findOne
  4. findOrCreate
  5. findAndCountAll

复杂查询的使用方式可以参考 Model Querying - Basics - 模型查询(基础)

insert

  1. async insert(user) {
  2. const result = await this.ctx.model.User.create(user);
  3. return {
  4. insertId: result.dataValues.id,
  5. };
  6. }

平淡无奇的用法,几乎以假乱真的面向对象编程

update & soft delete

  1. async update(user) {
  2. const { id, ...attrs } = user;
  3. const [numberOfAffectedRows, affectedRows] = await this.ctx.model.User.update(attrs, {
  4. where: { id },
  5. returning: true, // needed for affectedRows to be populated
  6. plain: true, // makes sure that the returned instances are just plain objects
  7. });
  8. console.log(numberOfAffectedRows, affectedRows)
  9. return { affectedRows };
  10. }
  11. // 软删除
  12. async delete(id) {
  13. const [numberOfAffectedRows, affectedRows] = await this.ctx.model.User.update({ deleted: 1 }, {
  14. where: { id },
  15. returning: true,
  16. plain: true,
  17. });
  18. return { affectedRows };
  19. }

update 方法参数和返回值需要注意一下

  1. returning:是否返回操作结果,默认是 false(其实蛮奇怪的,一般都需要获取返回结果)
  2. plain:设置为 true 保证返回值为简单对象

方法的返回值是一个数组,affectedRows 是影响的行数,具体可以参考 inserting-updating-destroying

hard delete

  1. // 从表结构中移除
  2. async hardDelete(id) {
  3. const numAffectedRows = await this.ctx.model.User.destroy({
  4. where: { id }
  5. });
  6. return numAffectedRows;
  7. }

总结

改完 service 之后就可以运行 demo 了,第一次真正体会到 MVP 模式带来的好处,同时可以看出使用 ORM 后数据库操作和面向对象做了很好的结合,处理起来简单了很多

完整代码:https://github.com/Samaritan89/egg-demo/tree/v3

篇幅原因文章对 sequelize 只介绍了最基本的使用,感兴趣可以通过 sequelize 官网 深入学习

  1. sequelize API 文档
  2. sequelize 中文教程
  3. sequelize egg.js 官方教程