typeORM中@ManyToMany自关联保存时覆盖原有关注关系
TypeORM 用户关注系统关联被覆盖问题修正方案
问题根因
你遇到的关联被覆盖、旧记录被DELETE删除的问题,本质是三个代码错误叠加导致的:
- 实体自引用关联的方向、拥有方定义完全颠倒
- 查询用户实体时没有预先加载多对多关联集合
- 触发了TypeORM多对多保存机制的差集删除逻辑:当你调用
save时,TypeORM会把内存中关联字段的数组值作为该关系的最终状态,自动删除所有数据库中存在但不在这个数组里的旧关联记录
第一步:修正User实体的关联定义
原实体的错误点在于把@JoinTable注解放在了followers字段上,完全颠倒了关系语义:
following是用户主动关注的人,是关联关系的拥有方,需要通过@JoinTable指定中间表结构followers是关注当前用户的人(粉丝列表),是关系的反向端,不需要定义中间表
修正后的实体定义如下:
@Entity() export class User { @PrimaryGeneratedColumn() id!: number; @Column({ nullable: true }) name!: string; @Column() email!: string; @Column() password!: string; @OneToMany(() => Post, (post) => post.user) posts!: Post[]; @OneToMany(() => Comment, (comment) => comment.user) comments!: Comment[]; // 中间表定义:存所有关注关系 @ManyToMany(() => User, (user) => user.followers) @JoinTable({ name: "user_follow_relations", joinColumn: { name: "followerId", referencedColumnName: "id" }, // 关注动作的发起用户 inverseJoinColumn: { name: "followingId", referencedColumnName: "id" } // 被关注的目标用户 }) following!: User[]; // 反向关联:当前用户的粉丝列表 @ManyToMany(() => User, (user) => user.following) followers!: User[]; @Column({ default: () => "NOW()" }) createdAt!: Date; @Column({ default: () => "NOW()" }) @UpdateDateColumn() updatedAt!: Date; }
注意:重新定义实体后,需要同步删除旧的自动生成的
user_followers_user中间表,避免和新的关联结构冲突
第二步:修正关注接口的业务逻辑
原接口的错误点在于操作了语义错误的followers字段,且加载用户时没有获取已有关联集合,导致每次赋值都是只有单个元素的新数组,触发TypeORM的差集删除旧关联的逻辑。
这里提供两种修正方案:
方案1:和原逻辑适配的简单修正(适合小流量场景)
核心修正点:
- 查询当前用户时通过
relations参数加载完整的following关联集合 - 把目标用户追加到
following集合中,而不是重新构造数组覆盖 - 增加重复关注的校验
router.post("/subscriptions", async (req, res) => { const currentUserId = Number(req.headers.userid); const userToFollowId = Number(req.body.userToFollowId); // 基础边界校验:禁止关注自己 if (currentUserId === userToFollowId) { return res.status(400).send("Cannot follow yourself"); } const userRepository = getRepository(User); try { // 关键修正:加载当前用户的同时,取出完整的following关联集合 const currentUser = await userRepository.findOneOrFail({ where: { id: currentUserId }, relations: ["following"] }); const userToFollow = await userRepository.findOneOrFail(userToFollowId); // 校验是否已经关注过,避免重复插入 const isAlreadyFollowed = currentUser.following.some(u => u.id === userToFollowId); if (isAlreadyFollowed) { return res.status(200).send("Already following this user"); } // 正确操作:把目标用户追加到关注列表,不覆盖已有内容 currentUser.following.push(userToFollow); await userRepository.save(currentUser); res.status(201).send("Followed"); } catch (e) { console.log(e); res.status(500).send("Follow operation failed"); } });
方案2:高性能优化写法(适合生产环境)
不需要加载整个用户的全量关注集合,直接通过TypeORM提供的关联操作API添加关系,彻底避免全量关联对比带来的性能问题和误删风险:
router.post("/subscriptions", async (req, res) => { const currentUserId = Number(req.headers.userid); const userToFollowId = Number(req.body.userToFollowId); if (currentUserId === userToFollowId) { return res.status(400).send("Cannot follow yourself"); } const userRepository = getRepository(User); try { // 校验两个用户都存在 await userRepository.findOneOrFail(currentUserId); await userRepository.findOneOrFail(userToFollowId); // 直接在关联表中添加关系,不会触发旧关联的对比删除逻辑 await userRepository .createQueryBuilder() .relation(User, "following") .of(currentUserId) .add(userToFollowId); res.status(201).send("Followed"); } catch (e) { console.log(e); res.status(500).send("Follow operation failed"); } });
内容的提问来源于stack exchange,提问作者Stefan
相关产品推荐
相关产品推荐

