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

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时出现逻辑判断错误:

  1. Label实体的name字段同时标记为主键和外键关联,导致TypeORM无法正确加载Domain关联,preload时查询Domain的参数为null;
  2. 关联关系识别错误后,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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.25 12:42:38