sequelize多对多关联实现

363 阅读5分钟

一,多对多,中间表设置外键关联

例如,角色表是roles和权限是permissions,中间表是role_permissions,中间表需要设置外键关联

// Roles的model实现
module.exports = app => {
  const DataTypes = app.Sequelize;

  const Model = app.model.define('roles', {
    id: {
      type: DataTypes.INTEGER(11),
      allowNull: false,
      primaryKey: true,
      autoIncrement: true
    },
   ...
  }, {
    tableName: 'roles'
  });
 
  Model.associate = function() {
    // 和权限表关联
    Model.belongsToMany(app.model.Permissions, {
      through: app.model.RolePermissions, // 中间表
      foreignKey: 'roleId',
      otherKey: 'permissionId',
    });
    // 和用户表关联
    Model.belongsToMany(app.model.Users, {
      through: app.model.UserRoles, // 中间表
      foreignKey: 'roleId',
      otherKey: 'userId',
    });
  }
  return Model;
};

角色roles列表查询时,用include关联权限表,角色下带出各自的权限码

 const { page, pageSize, roleName } = query;
 const tableQuery = {
      where: {},
      distinct: true, //这一句可以去重,主表是14条,count就是14, 有时解决获取总数量的问题,需要distinct,raw一起使用
      include: [
        {
          model: ctx.model.Permissions,
          as: 'permissions',
          // where: {},
          attributes: ['id', 'permissionName', 'permissionCode', 'type', 'parentId'],
          through: { // 注意:添加后,不会再返回关联的其他数据
            attributes: []
          }
        },
        {
          model: ctx.model.Users,
          as: 'users',
          // where: {},
          attributes: ['id', 'username', 'deptName'],
          through: { // 添加后,不会再返回关联的其他数据
            attributes: []
          }
        }
      ],
      offset: (page - 1) * pageSize,
      limit: pageSize
    };
    if (roleName) {
      tableQuery.where.roleName = {
        [Op.substring]: roleName
      };
    }
    try {
      const { count, rows } = await model.findAndCountAll(tableQuery);
      ctx.success({ total: count, list: rows, page, pageSize });
    } catch (error) {
      ctx.error(4002, error);
    }

多对多表关联时,给角色添加多个权限的关联数据,直接使用setXXX方法

// 角色的权限配置
async joinPermissions() {
    const { ctx, app } = this;
    const reqBody = ctx.request.body;
    const rule = {
      id: { // 角色id
        type: 'number',
        required: true
      },
      permissionIds: {
        type: 'array',
        required: true
      }
    };
    ctx.valid(rule, reqBody);
    // 全量更新,简单粗暴,每次都会在中间表重新生成记录,之前的记录会删除
    try {
      const rolesTarget = await ctx.model.Roles.findByPk(reqBody.id);
      // 如果设置了关联关系 belongsToMany,会自动生成setXXXX,getXXX方法
      await rolesTarget.setPermissions(reqBody.permissionIds);
      ctx.success({ id: reqBody.id });
    } catch (error) {
      ctx.error(4002, error);
    }
   // 增量更新 看下面方法
}

上面的角色添加多个权限还可以 增量 更新中间表,有兴趣看的可以看下面的方法,其实是比较生硬地先查中间表的全部,然后过滤已经添加的,剩下的重新添加进中间表

const permissionIds = reqBody.permissionIds.map(item => Number(item));
const options = {
  middleModel: 'RolePermissions', // 中间model名称
  oneIdName: 'roleId', // 一对多,一在中间表的key
  moreIdName: 'permissionId', // 一对多,多在中间表的key
  reqIds: permissionIds, // 一对多 ,多的id数组集合
  id: reqBody.id, // 一对多,一方的id
}
// 作增量更新中间表,需要先过滤部分数据,前端的permissionIds是全量提交的
const result = await updateMiddleTable(ctx, options);
if (result.isOk) {
  ctx.success({ id: reqBody.id });
} else {
  ctx.error(4002, '请检查权限id是否不存在');
}
-------------------------------增量更新中间表的方法--------------------------
async function updateMiddleTable(ctx, options) {
  // const { ctx, app } = this; 如果整合在help可以用这句
  // 开启事务
  const transaction = await ctx.model.transaction();
  try {
    const { id, reqIds, middleModel, oneIdName, moreIdName } = options
    const middleModelTable = ctx.model[middleModel];
    const oldMiddleData = await middleModelTable.findAll({
      raw: true,
      where: {
        roleId: id
      }
    });
    console.log("🚀 ~ updateMiddleTable ~ oldMiddleData:", oldMiddleData)
    const addMiddleIds = []
    const removeMiddleIds = []

    reqIds.forEach(item => {
      let index = -1
      if (oldMiddleData && oldMiddleData.length > 0) {
        index = oldMiddleData.findIndex(it => it[moreIdName] === item)
      }
      console.log("🚀 ~ RolesController ~ joinPermissions ~ index:", item, index)
      if (index === -1) {  // 不存在则新增
        addMiddleIds.push(item)
      } else { // 存在则是不用新增
        // 把保留的剔除,剩下是需要删除的
        oldMiddleData.splice(index, 1)
      }
    })
    oldMiddleData.forEach(item => { // 剩下是需要删除的
      removeMiddleIds.push(item.id)
    })
    console.log("🚀 ~ RolesController ~ joinPermissions ~ removeMiddleIds:", removeMiddleIds)
    console.log("🚀 ~ RolesController ~ joinPermissions ~ addMiddleIds:", addMiddleIds)
    if (removeMiddleIds.length > 0) {
      await middleModelTable.destroy({
        where: {
          id: removeMiddleIds
        }
      });
    }
    if (addMiddleIds.length > 0) {
      for (let i = 0; i < addMiddleIds.length; i++) {
        let middleItem = {}
        middleItem[oneIdName] = id
        middleItem[moreIdName] = addMiddleIds[i]
        await middleModelTable.create(middleItem);
      }
    }
    await transaction.commit(); // 提交事务
    return {
      isOk: true
    }
  } catch (error) {
    await transaction.rollback(); // 回滚事务
    return {
      isOk: false,
      error
    }
  }
}

多级关联查询时,include嵌套include,例如查询用户的权限,需要通过用户的角色再关联到权限,如下:

async getUserInfo() {
    const { ctx, app } = this;
    const model = ctx.model.User;
    const id = ctx.helper.tokenInfo.userId;
    try {
      const user = await model.findOne({
        attributes: {
          exclude: ['deleted_at']
        },
        where: { id },
        include: [
          {
            model: ctx.model.Roles,
            required: false, // 解决:添加了where条件,当关联数据为空时,导致整个结果为null
            where: {
              type: 'admin'
            },
            attributes: ['id', 'roleName' ],
            through: {
              attributes: []
            },
            include: [
              {
                model: ctx.model.Permissions,
                required: false,
                attributes: ['id', 'permissionName', 'permissionCode', 'parentId'],
                through: {
                  attributes: []
                }
              }
            ]
          }
        ]
      });
      let allPermissions = []
      if (user) {
        // 用户多个角色下的权限重组
        user.dataValues.roles.forEach(item => {
          allPermissions = allPermissions.concat(item.permissions)
        })
        delete user.dataValues.roles;
        const permissions = ctx.helper.getUniqueAry(allPermissions) // 数组对象去重,返回数组
        user.dataValues.permissions = permissions.map(item => item.permissionCode)
        ctx.success(user);
      } else {
        ctx.error(4002, '找不到该用户信息');
      }
    } catch (error) {
      ctx.error(4002, error);
    }
  }

二,多对多,大家都共用一个中间表

注意:多个表共用同一个中间表时,该中间表不用设置外键

例如标签和文章,还可以标签和课程,用type区分是不同的中间表。下面是中间表字段

image.png

然而model上声明关联关系时需要注意,用scope区分:

// 文章的model中
Model.associate = function () {
    Model.belongsToMany(model.Tags, {
      through: {
        model: model.ResourceTag,
        unique: false,
        scope: {
          type: 'resource'
        }
      },
      foreignKey: 'resourceId',
      // otherKey: 'tagId',
      constraints: false
    });
};
//  课程course的model中
Model.associate = function () {
    Model.belongsToMany(model.Tags, {
      through: {
        model: model.ResourceTag,
        unique: false,
        scope: {
          type: 'course'
        }
      },
      foreignKey: 'resourceId',
      // otherKey: 'tagId',
      constraints: false
    });
};

1,列表上关联查询时,也是使用include关联相关表就好,例如查询文章列表和各自对应的标签是什么

async list() {
    const { ctx, app, service } = this;
    const query = ctx.query;
    const rule = {
      page: { type: 'number', required: true },
      pageSize: { type: 'number', required: true }
    };
    if (!isNaN(query.page)) {
      query.page = +query.page;
    }
    if (!isNaN(query.pageSize)) {
      query.pageSize = +query.pageSize;
    }
    ctx.valid(rule, ctx.query);
    const search = {
      order: [
        ['releaseTime', 'DESC'],
        ['created_at', 'DESC'],
      ],
      distinct: true,
      include: [
        {
          model: ctx.model.Tags,
          attributes: ['label', 'id', 'moduleId', 'parentId', 'typeId'],
          through: {
            attributes: []
          }
        }
      ],
      attributes: {
        exclude: ['deleted_at', 'content']
      },
      limit: query.pageSize,
      offset: (query.page - 1) * query.pageSize,
      where: {}
    };
    try {
      const { rows, count } = await model.findAndCountAll(search); 
      ctx.success({
        page: query.page,
        pageSize: query.pageSize,
        list: rows,
        total: count
      });
    } catch (error) {
      ctx.error(4002, error);
    }

2,查询文章的所有标签 分组统计数量,并倒序返回(查询标签,include文章表)

async tagsCountlist() {
    const { ctx, app } = this;
    const query = ctx.query;
    const rule = {
      page: {
        type: 'number',
        required: true
      },
      pageSize: {
        type: 'number',
        required: true
      },
      label: {
         type: 'string',
        required: true
      }
    };
    if (!isNaN(query.page)) {
      query.page = +query.page;
    }
    if (!isNaN(query.pageSize)) {
      query.pageSize = +query.pageSize;
    }
    ctx.valid(rule, query);
    const model = ctx.model.Tags;
    const { page, pageSize } = query;
    const tableQuery = {
      where: {},
      order: [
        ['updated_at', 'DESC']
      ],
      offset: (page - 1) * pageSize,
      limit: pageSize,
      include: [
        {
          model: ctx.model.Resources,
          where: {},
          attributes: [],
          through: { // 去掉后, resources 数据不会再返回
            attributes: []
          }
        }
      ];
    };
    if (label) {
      tableQuery.where.label = {
        [Op.substring]: label
      };
    }
    try {
        // 下面count查询速度较快,但实现不了按更新时间 倒序返回
        // const res = await model.count({
        //   attributes: ['id', 'label'],
        //   group: 'id',
        //   ...tableQuery
        // })
        // console.log('res', res)

        // TODO该查询时间比上面的count长一些
        const includeObj = tableQuery.include[0]
        delete includeObj.through // 去掉through属性,不再返回resources相关信息
        tableQuery.include = [{ ...includeObj } ]
        const res = await model.findAll({
          subQuery: false, // 将嵌套查询转换为子查询,避免多次查询。
          attributes: ['id', 'label', [fn('COUNT', col('resources.id')), 'resourceCount']],
          group: 'id',
          ...tableQuery
        })
        ctx.success(res)
        // 返回结果: [ {id: 111111, label: '热门', resourceCount: 100, ... }
    } catch (error) {
      ctx.error(4002, error);
    }
  }