带你彻底了解sequelize 表关联 -- 多对多

145 阅读8分钟

带你彻底了解sequelize 表关联 -- 多对多

目录

[TOC]

在使用Sequelize进行数据库建模时,通常需要处理多个表之间的关联关系,特别是在涉及多对多(many-to-many)关系时。为了让你更好地理解如何使用这些概念,本文将通过一个具体的示例(用户与角色的关系)来详细讲解 Sequelize 中的 belongsToMany , through , foreignKey , otherKeyreferences 等概念。

1. 理解多对多关系

在数据库设计中,多对多关系是一种常见的关联类型,表示两个模型之间可以互相拥有多个实例。例如,用户(User)和角色(Role)之间就是典型的多对多关系:一个用户可以拥有多个角色,一个角色也可以被多个用户拥有。

为了在 Sequelize 中定义这种关系,我们需要使用 belongsToMany 方法。这个方法需要指定一个中间表(也叫连接表或 junction table),用来存储两个模型之间的关联关系。

2. 定义中间表模型

首先,我们定义了三个模型: UserRoleUserRole

  • User模型 :代表用户的信息。
  • Role模型 :代表角色的信息。
  • UserRole模型 :作为中间表,存储用户和角色之间的关联。

每个用户可以拥有多个角色,每个角色也可以被多个用户拥有。因此,这是一个典型的多对多关系。

为了表示这种关系,我们需要一个中间表: 用户角色表(UserRole) ,它包含了用户 ID和角色 ID,以表示某个用户与某个角色的关联

const { DataTypes } = require("sequelize");
const seq = require("../db/seq");
 
const UserRole = seq.define("st_UserRole", {
  users_id: {
    type: DataTypes.INTEGER,
    allowNull: false,
    comment: "用户ID",
    references: {
      model: "st_users",  // 关联的模型表名
      key: "id",          // 关联的主键
    },
  },
  role_id: {
    type: DataTypes.INTEGER,
    allowNull: false,
    comment: "角色ID",
    references: {
      model: "st_roles",  // 关联的模型表名
      key: "id",          // 关联的主键
    },
  },
});
 
module.exports = UserRol

这里, UserRole 定义了两个字段: users_idrole_id ,分别对应 UserRole 的主键。 references 属性用来指定外键关联的模型和字段。

references :该属性定义了外键约束的具体表和字段,确保 users_idrole_id 字段分别与 users 表和 roles 表的主键字段进行关联。

3.  定义关联关系

接下来,在 setupAssociations 函数中,我们使用 belongsToMany 方法来建立 UserRole 之间的多对多关系:

const setupAssociations = () => {
  User.belongsToMany(Role, {
    through: UserRole,          // 指定中间表
    foreignKey: "users_id",    // 本模型的外键
    otherKey: "role_id",       // 目标模型的外键
  });
 
  Role.belongsToMany(User, {
    through: UserRole,          // 指定中间表
    foreignKey: "role_id",     // 本模型的外键
    otherKey: "users_id",      // 目标模型的外键
  });
};
 
module.exports = setupAssociations;
  • through :指定中间表模型,这里是 UserRole
  • foreignKey :表示当前模型用于关联的外键字段。例如,在 User.belongsToMany(Role) 中, foreignKeyusers_id ,表示 UseridUserRole 中的字段。
  • otherKey :表示目标模型用于关联的外键字段。在 User.belongsToMany(Role) 中, otherKeyrole_id ,表示 RoleidUserRole 中的字段。
  • references :在定义 UserRole 模型时使用,用来指定外键关联的模型和具体字段。

4. 查询用户角色的方法

接下来,我们来看如何通过 Sequelize 的方法来获取一个用户所拥有的角色。在提供的代码中,有一个 getUserAssignedRoles 函数:

  async getUserAssignedRoles(id) {
    try {
      // 使用 findOne 替代 findByPk,并添加更多的查询选项
      const user = await User.findOne({
        where: { id },
        attributes: ["id"],
        include: [
          {
            model: Role,
            through: {
              attributes: [], // 不包含中间表字段
            },
            attributes: ["id", "role_name", "permission_key"], // 只返回需要的字段
            where: {
              status: 1, // 只查询启用的角色
            },
          },
        ],
      });
 
      return {
        assignedRoles: user?.st_Roles || [], // 使用可选链操作符
      };
    } catch (error) {
      console.error("Get user assigned roles error:", error);
      // 其他错误返回空数组
      return {
        assignedRoles: [],
      };
    }
  }
  1. User.findOne :根据用户的ID查找用户。
  2. attributes :指定返回的用户信息字段,这里只返回 id
  3. include :包含关联的角色信息。
  • model: Role :指定要包含的模型。
  • through: { attributes: [] } :指定不包含中间表 UserRole 的任何额外信息。
  • attributes: ["id", "role_name", "permission_key"] :只返回角色的指定字段。
  • where: { status: 1 } :添加条件,只查找状态为1(启用)的角色。
  1. 返回结果 :如果找到用户,返回其关联的角色数组;如果用户不存在或发生其他错误,返回空数组。

5. 官方文档的补充说明

参考 Sequelize 官方文档中关于 Advanced Many-to-Many Associations 的说明,我们可以了解到:

  • 通过表(Through Table) vs. 普通表(Normal Table) :在多对多关系中,通常需要一个中间表来存储关联信息。Sequelize 提供了自动生成这个中间表的选项,但也可以手动定义,以便增加更多的字段或索引。

  • 超级多对多关系(Super Many-to-Many Association) :通过在两个模型之间同时定义 belongsToMany 关联,并且在中间表中添加额外的字段,可以实现更复杂的数据结构和查询逻辑。

通过 Sequelize 的 belongsToManythroughforeignKeyotherKeyreferences 等功能,我们能够轻松地建立和管理多对多关系。理解这些概念有助于我们更好地设计和查询复杂的数据库模型,使得我们可以高效地管理表之间的关联数据。

核心概念总结:

  • belongsToMany :用于建立多对多关系。
  • through :指定多对多关系中的中间表。
  • foreignKey :指定当前模型在中间表中的外键字段。
  • otherKey :指定关联模型在中间表中的外键字段。
  • references :定义外键约束,确保字段和关联表的主键一致。

通过这些工具,Sequelize 可以自动处理多对多关系中的数据操作,大大简化了复杂的查询和数据插入工作。

foreignKey 和 otherKey 具体作用

当你使用 belongsToMany 设置多对多关联时,Sequelize 需要知道:

  • 当前模型在中间表中的外键字段是哪个。
  • 关联模型在中间表中的外键字段是哪个。

这时, foreignKeyotherKey 就用来解决这个问题。

1. foreignKey

foreignKey当前模型 在中间表中使用的外键字段。换句话说,它指定了在中间表中用来指向当前模型(比如用户模型)的字段。

举个例子:

User.belongsToMany(Role, {
  through: UserRole,  // 使用中间表 UserRole
  foreignKey: 'users_id',  // 'users_id' 在 UserRole 中指向 'users' 表的主键(通常是 id)
  otherKey: 'role_id',  // 'role_id' 在 UserRole 中指向 'roles' 表的主键(通常是 id)
});
  • 在这个例子中, User 模型的 foreignKey: 'users_id' 表示,在 UserRole 中, users_id 字段指向的是 users 表的主键( id )。
  • 也就是说, users_id 表示这个关联是哪个 用户

2. otherKey

otherKey关联模型 在中间表中的外键字段。它指定了在中间表中用来指向关联模型(比如角色模型)的字段。

在上面的代码中, otherKey: 'role_id' 表示,在 UserRole 中, role_id 字段指向的是 roles 表的主键( id )。也就是说, role_id 表示这个关联是哪个 角色

为什么需要这两个字段?

假设我们有以下两个模型:

  1. User :用户模型
  2. Role :角色模型

这两个模型通过 UserRole 表建立多对多关系。在 UserRole 表中,有两个外键字段:

  • 一个是 users_id ,它指向 users 表的主键。
  • 另一个是 role_id ,它指向 roles 表的主键。

如果你只定义了 foreignKey ,Sequelize 只能知道如何将当前模型( User )连接到中间表,但它不知道如何将关联模型( Role )连接到中间表。

所以, foreignKey 是当前模型的外键字段,而 otherKey 是关联模型的外键字段。它们一起工作,确保多对多关系能够正常运作。

总结

  • foreignKey :指定当前模型在中间表中的外键字段,通常指向当前模型(如 users )的主键。
  • otherKey :指定关联模型在中间表中的外键字段,通常指向关联模型(如 roles )的主键。

这两个字段帮助 Sequelize 正确地建立和查询多对多的关系。

关联表的增删改

其实关联关系搞定之后 关联表的操作余crud一样操作就行

  1. 验证用户存在 :确保要操作的用户存在。
  2. 处理特殊情况 :如果角色列表为空,直接删除所有关联。
  3. 查找有效角色 :确保传入的角色ID都是有效的。
  4. 比对当前关联 :找出需要删除和新增的角色关联。
  5. 执行删除和新增 :精准地删除不再需要的关联,批量新增新的关联。
  6. 返回结果 :操作成功返回 true ,失败则抛出错误。
  async saveUserRole(userId, roleIdList) {
    try {
      const user = await User.findByPk(userId);
      if (!user) {
        throw new Error("User not found");
      }
 
      // 如果传入空数组,直接删除所有角色关联
      if (roleIdList.length === 0) {
        await UserRole.destroy({
          where: {
            users_id: userId,
          },
        });
        return true; // 直接返回,表示操作成功
      }
 
      // 查找所有有效角色
      const roles = await Role.findAll({
        where: {
          id: roleIdList,
        },
      });
 
      if (!roles.length) {
        throw new Error("No valid roles found");
      }
 
      // 获取当前用户已关联的角色
      const currentUserRoles = await UserRole.findAll({
        where: { users_id: userId },
        attributes: ["role_id"],
      });
 
      // 提取当前用户的角色ID列表
      const currentRoleIds = currentUserRoles.map((role) => role.role_id);
 
      // 找出需要删除和新增的角色
      const rolesToDelete = currentRoleIds.filter(
        (roleId) => !roleIdList.includes(roleId)
      );
      const rolesToAdd = roleIdList.filter(
        (roleId) => !currentRoleIds.includes(roleId)
      );
 
      // 如果有需要删除的角色,先删除
      const deleteCount = rolesToDelete.length;
      if (deleteCount > 0) {
        await UserRole.destroy({
          where: {
            users_id: userId,
            role_id: rolesToDelete,
          },
        });
      }
 
      // 如果有需要新增的角色,进行插入
      const addCount = rolesToAdd.length;
      if (addCount > 0) {
        const roleAssociations = rolesToAdd.map((roleId) => ({
          users_id: userId,
          role_id: roleId,
        }));
        await UserRole.bulkCreate(roleAssociations);
      }
 
      // 返回更新成功
      return true;
    } catch (error) {
      console.error("Save user role error:", error);
      throw error;
    }
  }