Node.js ORM框架Sequelize实践小结

摘自 2023/4 笔记

对象-关系映射(Object-Relational Mapping,简称ORM),用于实现面向对象编程语言里不同类型系统之间数据的转换。

面向对象是从软件工程基本原则(如耦合、聚合、封装)的基础上发展起来的,而关系数据库则是从数学理论发展而来的,两套理论存在显著的区别。为了解决这个不匹配的现象,对象关系映射技术应运而生。

简单的说,ORM是通过使用描述对象和数据库之间映射的元数据,将程序中的对象自动持久化到关系数据库中。

参考:https://en.wikipedia.org/wiki/Object%E2%80%93relational_mapping

为什么要使用ORM框架

  1. 提高开发效率,降低开发成本
  2. 使开发更加对象化
  3. 可以很方便地引入数据缓存等附加功能

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层定义关联关系

  • 一对一关系:hasOnebelongsTo 关联一起使用
  • 一对多关系:hasManybelongsTo 关联一起使用
  • 多对多关系:两个 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
}

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

目录