Node.js TypeScript TypeORM多对多保存覆盖旧关系而非新增
TypeORM多对多关联被覆盖问题
问题现象
遍历学生列表添加到教师关联集合时,后续添加的学生覆盖了之前的关联关系,数据库中最终仅保留最后一个学生与教师的关联,而非新增关联。
实体模型代码
@Entity() export class Student extends EntityBase { @Column({ length: 256 }) public firstName: string @Column({ length: 256 }) public lastName: string @Column({ unique: true, length: 256 }) public email: string @Column() public isSuspended: boolean @ManyToMany((type) => Teacher, (teacher) => teacher.students, { cascade: true }) @JoinTable() public teachers: Teacher[] constructor(first: string, last: string, email: string, isSuspended?: boolean) { super(); this.firstName = first; this.lastName = last; this.email = email; this.isSuspended = isSuspended ?? false; } } @Entity() export class Teacher extends EntityBase { @Column({ length: 256 }) public firstName: string @Column({ length: 256 }) public lastName: string @Column({ unique: true, length: 256 }) public email: string @ManyToMany((type) => Student, (student) => student.teachers) public students: Student[] constructor(first: string, last: string, email: string) { super(); this.firstName = first; this.lastName = last; this.email = email; } }
StudentRepository.AddTeacher方法
public async AddTeacher (student: Student, teacher: Teacher): Promise<Student | null> { if (teacher && student) { try { student.teachers.push(teacher); return await this.Update(student); } catch (e) { console.log(e); } } return null; }
业务逻辑代码
for (let i of request.Students) { let s: Student | null = await this._studentRepository.GetByEmail(i.email); if (s !== null && (t!.students.length === 0 || t!.students.find(ss => ss.email == s.email) == null)) { this._logger.Log(LogLevels.debug, `Adding student: ${JSON.stringify(s)} to teacher ${JSON.stringify(t)}`); if (!s.teachers.some(i => i.id === t.id)) { let student = await this._studentRepository.AddTeacher(s, t); this._logger.Log(LogLevels.debug, `student: ${JSON.stringify(student)}`); if (student !== null) count++; else { this._logger.Log(LogLevels.error, `Failed to add student ${s.email} to teacher ${t.email}`); errors.push(new Error("", `Failed to add student ${s.email} to teacher ${t.email}`)); } } else { this._logger.Log(LogLevels.error, `Skip existing Student ${i.email} <-> teacher ${t.email}`); errors.push(new Error("", `Skip existing Student ${i.email} <-> teacher ${t.email}`)); } } else if (s === null) { this._logger.Log(LogLevels.error, `Invalid student ${i.email}`); errors.push(new Error("", `Invalid student ${i.email}`)); } else { this._logger.Log(LogLevels.error, `Failed to add student ${i.email} to teacher ${t.email}`); errors.push(new Error("", `Failed to add student ${i.email} to teacher ${t.email}`)); } };
请求数据
{ "teacher": { "email": "teacher1@gmail.com" }, "students": [ "student1@gmail.com", "student2@gmail.com" ] }
日志信息
2024-07-07 13:44:38:4438 debug: Adding 2 students to teacher teacher1@gmail.com... 2024-07-07 13:44:38:4438 debug: Adding student: {"id":1,"created":"2024-07-07T05:44:33.716Z","modified":"2024-07-07T05:44:33.716Z","firstName":"One","lastName":"Student","email":"student1@gmail.com","isSuspended":false,"teachers":[]} to teacher {"id":1,"created":"2024-07-07T05:44:33.736Z","modified":"2024-07-07T05:44:33.736Z","firstName":"One","lastName":"Teacher","email":"teacher1@gmail.com","students":[]} 2024-07-07 13:44:38:4438 debug: student: {"id":1,"created":"2024-07-07T05:44:33.716Z","modified":"2024-07-07T05:44:33.716Z","firstName":"One","lastName":"Student","email":"student1@gmail.com","isSuspended":false,"teachers":[{"id":1,"created":"2024-07-07T05:44:33.736Z","modified":"2024-07-07T05:44:33.736Z","firstName":"One","lastName":"Teacher","email":"teacher1@gmail.com","students":[]}]} 2024-07-07 13:44:38:4438 debug: Adding student: {"id":2,"created":"2024-07-07T05:44:33.731Z","modified":"2024-07-07T05:44:33.731Z","firstName":"Two","lastName":"Student","email":"student2@gmail.com","isSuspended":false,"teachers":[]} to teacher {"id":1,"created":"2024-07-07T05:44:33.736Z","modified":"2024-07-07T05:44:33.736Z","firstName":"One","lastName":"Teacher","email":"teacher1@gmail.com","students":[]} 2024-07-07 13:44:38:4438 debug: student: {"id":2,"created":"2024-07-07T05:44:33.731Z","modified":"2024-07-07T05:44:33.731Z","firstName":"Two","lastName":"Student","email":"student2@gmail.com","isSuspended":false,"teachers":[{"id":1,"created":"2024-07-07T05:44:33.736Z","modified":"2024-07-07T05:44:33.736Z","firstName":"One","lastName":"Teacher","email":"teacher1@gmail.com","students":[]}]}
问题原因
直接通过修改student.teachers数组并调用save的方式维护多对多关联,可能因EntityManager缓存、关联未正确加载等问题,导致TypeORM错误地覆盖已有的关联记录。此外,当前代码未使用TypeORM推荐的关联维护方式,容易引发此类意外。
解决方案
方案1:使用TypeORM Relation API维护关联(推荐)
修改AddTeacher方法,使用TypeORM提供的relation接口直接操作关联表,避免手动维护数组:
public async AddTeacher(student: Student, teacher: Teacher): Promise<Student | null> { if (teacher && student) { try { // 使用Relation API添加多对多关联 await this.createQueryBuilder() .relation(Student, "teachers") .of(student) .add(teacher); return student; } catch (e) { console.error(e); return null; } } return null; }
该方式会直接在关联表中插入新记录,不会影响已存在的关联关系,是最可靠的多对多关联维护方式。
方案2:确保加载实体的关联数据
修改GetByEmail方法,在获取Student时加载已有的teachers关联,确保手动维护数组时能正确追加而非覆盖:
public async GetByEmail(email: string): Promise<Student | null> { return await this.findOne({ where: { email }, relations: ["teachers"] // 加载关联的teachers数据 }); }
此方案适用于需要手动维护关联数组的场景,能避免因关联未加载导致的覆盖问题。
方案3:检查Update方法实现
确保Update方法调用的是repository.save()而非update(),因为update()仅更新实体列,不会处理关联关系:
public async Update(student: Student): Promise<Student> { // 使用save方法处理实体及关联 return await this.save(student); }
内容的提问来源于stack exchange,提问作者khteh
相关产品推荐
相关产品推荐

