Sequelize同User模型好友关系单向生效问题解决方案
Sequelize 自关联好友关系实现方案
问题背景
技术栈为Express + Sequelize + PostgreSQL,部署于Heroku,开发聊天应用时通过Friendship中间表实现User模型与自身的好友关联,存在问题:创建好友申请后,仅发起添加的用户侧可查询到好友记录,被添加用户的好友列表为空。调整关联配置后可部分满足需求,但实现不规范。
问题根因
原关联配置为单向关联:仅定义了*用户作为好友申请发起方(对应friendships表user字段)时的关联关系,没有覆盖用户作为好友申请接收方(对应friendships表friend字段)*时的关联关系。Sequelize查询好友列表时,只会匹配friendships.user = 当前用户ID的记录,被添加用户作为接收方时,其ID存储在friend字段中,自然无法查询到对应记录。后续调整的双关联方向逻辑正确,但别名定义模糊、未区分业务场景,导致实现不完善。
规范实现步骤
1. 修正模型关联配置
替换原有模糊的关联定义,按业务场景拆分双向关联,同时补全中间表的关联映射,避免别名冲突:
const { Op } = require('sequelize') // 后续查询需要用到操作符,提前导入 // 关联1:我作为发起方,添加的所有好友/发送的申请 User.belongsToMany(User, { as: 'sentFriendRequests', through: Friendship, foreignKey: 'user', // 关联中间表存发起方ID的user字段 otherKey: 'friend' // 关联中间表存接收方ID的friend字段 }) // 关联2:我作为接收方,收到的所有好友申请/好友 User.belongsToMany(User, { as: 'receivedFriendRequests', through: Friendship, foreignKey: 'friend', // 关联中间表存接收方ID的friend字段 otherKey: 'user' // 关联中间表存发起方ID的user字段 }) // 补全中间表与用户表的关联,方便后续查询申请双方信息 Friendship.belongsTo(User, { as: 'initiator', foreignKey: 'user' }) Friendship.belongsTo(User, { as: 'receiver', foreignKey: 'friend' })
2. 数据库层兜底优化
在friendships表的迁移文件中增加约束和索引,从数据库层面避免异常数据、提升查询速度:
// 创建完friendships表后追加以下代码 // 加联合唯一约束,避免同一对用户重复生成好友申请 await queryInterface.addConstraint('friendships', { fields: ['user', 'friend'], type: 'unique', name: 'unique_user_friend_relation' }) // 加联合索引,提升好友列表、申请列表的查询效率 await queryInterface.addIndex('friendships', ['user', 'friend', 'status'])
3. 修正添加好友接口逻辑
原有逻辑缺少自添加校验、重复关系校验,且存在敏感信息泄露风险,补全后代码如下:
export const addFriend = async (req: Request, res: Response) => { try { const { friendId } = req.body; const userId = req.decodedToken?.id; // 禁止添加自己为好友 if (userId === friendId) { return res.status(400).json({ error: '不能添加自己为好友' }) } // 校验目标用户是否存在 const targetUser = await User.findByPk(friendId, { attributes: { exclude: ['passwordHash'] }, }); if (!targetUser) { return res.status(404).json({ error: '目标用户不存在' }) } // 校验是否已存在待确认/已通过的好友关系,避免重复申请 const existRelation = await Friendship.findOne({ where: { status: ['PENDING', 'ACCEPTED'], [Op.or]: [ { user: userId, friend: friendId }, { user: friendId, friend: userId } ] } }) if (existRelation) { return res.status(400).json({ error: '已存在好友申请或好友关系' }) } // 创建待确认好友申请 await Friendship.create({ user: userId, friend: friendId, status: 'PENDING', }); return res.status(200).json({ status: 'success' }); } catch (e) { res.status(500).json({ error: '服务端创建好友申请失败' }); console.log(e); } };
4. 好友列表查询实现
按业务场景分别查询即可,无需在好友通过时反向插入冗余数据:
- 查我发送的待确认申请:关联
sentFriendRequests,过滤中间表status='PENDING' - 查我收到的待确认申请:关联
receivedFriendRequests,过滤中间表status='PENDING' - 查已通过的双向好友列表:合并两个方向
status='ACCEPTED'的查询结果,去重后返回
已通过好友列表示例代码:
const getAcceptedFriendList = async (userId: string) => { // 查询我发起的已通过好友关系 const sentRes = await User.findByPk(userId, { attributes: { exclude: ['passwordHash'] }, include: [{ model: User, as: 'sentFriendRequests', attributes: { exclude: ['passwordHash'] }, // 敏感字段必须排除 through: { where: { status: 'ACCEPTED' } } }] }) // 查询我收到的已通过好友关系 const receivedRes = await User.findByPk(userId, { attributes: { exclude: ['passwordHash'] }, include: [{ model: User, as: 'receivedFriendRequests', attributes: { exclude: ['passwordHash'] }, // 敏感字段必须排除 through: { where: { status: 'ACCEPTED' } } }] }) // 合并两个列表即为完整的双向好友列表 return [ ...sentRes.sentFriendRequests, ...receivedRes.receivedFriendRequests ] }
注意:所有返回用户信息的接口必须排除
passwordHash字段,示例响应中出现密码哈希属于严重安全问题,必须修复。
内容的提问来源于stack exchange,提问作者AK96
相关产品推荐
相关产品推荐

