NestJS+TypeORM中Repository.save执行插入而非更新的问题
问题:Label表更新操作执行INSERT而非UPDATE导致主键重复错误
环境
NestJS 9 + TypeORM 0.3.12 + PostgreSQL,Domain表CRUD功能正常,但Label表调用Repository.save()时执行插入而非更新,触发主键重复约束错误。
数据模型实体代码
domain.entity.ts
import { OneToMany, PrimaryColumn, Relation } from 'typeorm'; import { Column, CreateDateColumn, Entity, UpdateDateColumn, VersionColumn, } from 'typeorm'; import { DomainType } from '../enum/enums'; import { Label } from './label.entity'; @Entity() export class Domain { @PrimaryColumn('varchar', { length: 30 }) name: string; @Column('varchar', { length: 100 }) description: string; @Column({ default: DomainType.STRING }) type: string; @Column('varchar', { length: 254, default: '' }) inputmask: string; @Column('varchar', { length: 254, default: '' }) displayMask: string; @CreateDateColumn({ type: 'timestamptz' }) creationDate: Date; @UpdateDateColumn({ type: 'timestamptz' }) lastUpdateDate: Date; @VersionColumn() version: number; @OneToMany(() => Label, (label) => label.name, { cascade: ['remove'] }) metadata: Relation<Label>; labels: Label[]; }
label.entity.ts
import { JoinColumn, ManyToOne, OneToMany, PrimaryColumn } from 'typeorm'; import { Column, CreateDateColumn, Entity, UpdateDateColumn, VersionColumn, } from 'typeorm'; import { Domain } from './domain.entity'; import { LabelI18n } from './labelI18n.entity'; @Entity() export class Label { @PrimaryColumn('varchar', { length: 30 }) @ManyToOne(() => Domain) @JoinColumn({ name: 'name', referencedColumnName: 'name' }) // koppel kolom 'name' aan kolom name uit tabel domain name: string; @PrimaryColumn('varchar', { length: 30 }) label: string; @Column('varchar', { length: 20 }) shortDescription: string; @Column('varchar', { length: 50 }) longDescription: string; @CreateDateColumn({ type: 'timestamptz' }) creationDate: Date; @UpdateDateColumn({ type: 'timestamptz' }) lastUpdateDate: Date; @VersionColumn() version: number; @OneToMany(() => LabelI18n, (labelI18n) => labelI18n.name, { cascade: ['remove'], }) labelsI18n: LabelI18n[]; }
labelI18n.entity.ts
import { JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm'; import { Column, CreateDateColumn, Entity, UpdateDateColumn, VersionColumn, } from 'typeorm'; import { Label } from './label.entity'; @Entity() export class LabelI18n { @PrimaryColumn('varchar', { length: 30 }) @ManyToOne(() => Label) @JoinColumn([ { name: 'name', referencedColumnName: 'name' }, { name: 'label', referencedColumnName: 'label' }, ]) name: string; @PrimaryColumn('varchar', { length: 30 }) label: string; @PrimaryColumn('varchar', { length: 10 }) language: string; @Column('varchar', { length: 20 }) shortDescription: string; @Column('varchar', { length: 50 }) longDescription: string; @CreateDateColumn({ type: 'timestamptz' }) creationDate: Date; @UpdateDateColumn({ type: 'timestamptz' }) lastUpdateDate: Date; @VersionColumn() version: number; }
服务类更新方法代码
@Injectable() export class DomainPrivateService { constructor( @InjectRepository(Domain) private readonly domainRepository: Repository<Domain>, @InjectRepository(Label) private readonly labelRepository: Repository<Label>, @InjectRepository(LabelI18n) private readonly labelI18nRepository: Repository<LabelI18n>, ) {} // ...其他方法省略 async updateLabel( name: string, labelId: string, updateLabelDto: UpdateLabelDto, ) { console.log('updateLabel '); const label = await this.labelRepository.preload({ name: name, label: labelId, ...updateLabelDto, }); if (!label) { throw new NotFoundException( 'Label ' + name + '/' + labelId + ' not found.', ); } return await this.labelRepository.save(label); } }
问题日志
执行更新请求时,preload的SQL日志显示:查询Label表的参数正确('dsca10','label2'),但查询Domain表的参数为null;后续save执行INSERT语句,触发Label表主键PK_1dedf7625a4373e6cc875df760e的重复约束错误。
问题分析与解决方案
核心问题
TypeORM中不能将@PrimaryColumn和@ManyToOne装饰器放在同一个字段上,这种混合配置会导致ORM无法正确识别主键与关联关系的边界,进而在preload和save时出现逻辑判断错误:
- Label实体的
name字段同时标记为主键和外键关联,导致TypeORM无法正确加载Domain关联,preload时查询Domain的参数为null; - 关联关系识别错误后,TypeORM无法判断当前Label实体是否已存在于数据库,最终将
save操作误判为INSERT而非UPDATE。
具体修复步骤
1. 修正Label实体的关联配置
将主键字段与关联字段分开定义,明确边界:
import { JoinColumn, ManyToOne, OneToMany, PrimaryColumn } from 'typeorm'; import { Column, CreateDateColumn, Entity, UpdateDateColumn, VersionColumn, } from 'typeorm'; import { Domain } from './domain.entity'; import { LabelI18n } from './labelI18n.entity'; @Entity() export class Label { // 单独定义主键字段 @PrimaryColumn('varchar', { length: 30 }) name: string; @PrimaryColumn('varchar', { length: 30 }) label: string; // 单独定义ManyToOne关联,通过name字段关联Domain的name主键 @ManyToOne(() => Domain) @JoinColumn({ name: 'name', referencedColumnName: 'name' }) domain: Domain; @Column('varchar', { length: 20 }) shortDescription: string; @Column('varchar', { length: 50 }) longDescription: string; @CreateDateColumn({ type: 'timestamptz' }) creationDate: Date; @UpdateDateColumn({ type: 'timestamptz' }) lastUpdateDate: Date; @VersionColumn() version: number; // 修正反向关联指向Label实体,而非name字段 @OneToMany(() => LabelI18n, (labelI18n) => labelI18n.labelEntity, { cascade: ['remove'], }) labelsI18n: LabelI18n[]; }
2. 修正LabelI18n实体的关联配置
同样将主键与关联字段分离,确保复合主键关联正确:
import { JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm'; import { Column, CreateDateColumn, Entity, UpdateDateColumn, VersionColumn, } from 'typeorm'; import { Label } from './label.entity'; @Entity() export class LabelI18n { @PrimaryColumn('varchar', { length: 30 }) name: string; @PrimaryColumn('varchar', { length: 30 }) label: string; @PrimaryColumn('varchar', { length: 10 }) language: string; // 单独定义ManyToOne关联,指向Label的复合主键 @ManyToOne(() => Label) @JoinColumn([ { name: 'name', referencedColumnName: 'name' }, { name: 'label', referencedColumnName: 'label' }, ]) labelEntity: Label; @Column('varchar', { length: 20 }) shortDescription: string; @Column('varchar', { length: 50 }) longDescription: string; @CreateDateColumn({ type: 'timestamptz' }) creationDate: Date; @UpdateDateColumn({ type: 'timestamptz' }) lastUpdateDate: Date; @VersionColumn() version: number; }
3. 修正Domain实体的反向关联
确保Domain的OneToMany关联指向Label的domain字段:
// 在Domain实体中修改OneToMany部分 @OneToMany(() => Label, (label) => label.domain, { cascade: ['remove'] }) labels: Label[]; // 移除冗余的metadata字段 // metadata: Relation<Label>;
验证修复
修正后,preload会正确加载Label实体及其关联的Domain,save操作时TypeORM能通过name和label复合主键判断实体已存在,从而执行UPDATE语句而非INSERT,解决主键重复错误。
内容的提问来源于stack exchange,提问作者user3178125
相关产品推荐
相关产品推荐

