分类: Node

  • 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
    }
  • 本地开发debug npm包

    使用pnpm包管理

    1. 在本地npm包目录下执行

    pnpm link --global

    2. 如果是cli,可直接在其他项目目录下调用

    3. 如果需要在其他项目中引入npm包,在其他项目目录下执行

    pnpm link --global npm-package-name

    使用npm包管理

    1. 在npm包目录下执行

    npm link

    2. 在其他目录使用

    npm link 本地npm包绝对路径

    官方文档:https://docs.npmjs.com/cli/v9/commands/npm-link

目录