You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.23 21:36:07