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

NestJS + TypeORM 软删除场景下仅对未删除记录应用@Unique装饰器的问题解决求助

我之前在做NestJS + TypeORM项目时也遇到过一模一样的问题——TypeORM自带的@Unique装饰器确实不支持带IS NULL这类条件的语法,不过有几个靠谱的解决方案,我给你详细说说:

方案一:数据库层面创建带条件的唯一索引(最推荐)

软删除场景下的唯一约束核心需求是:仅对未标记删除的记录生效,这正好可以用数据库的「部分唯一索引」来实现。TypeORM的@Index装饰器支持通过where参数传入SQL条件,完美适配这个需求:

@Entity('role')
// 给title创建唯一索引,仅对deletedDate为NULL的记录生效
@Index(['title'], { 
  unique: true,
  where: 'deletedDate IS NULL'
})
export class Role {
  @Column({ type: 'varchar', nullable: false })
  title: string;

  @DeleteDateColumn({ select: false })
  deletedDate?: Date;
}

注意事项:

  • 不同数据库对这个语法的支持:PostgreSQL原生支持部分索引;MySQL 8.0+支持这种带条件的唯一索引;如果是老版本MySQL,可能需要用函数索引变通(比如where deletedDate IS NULL依然可以生效)。
  • 数据库层面的约束是最可靠的,能彻底避免并发场景下的竞态问题(比如两个请求同时创建同名角色)。

方案二:应用层自定义验证器(兜底兼容)

如果你需要兼容老版本数据库,或者想在应用层提前做校验,可以用NestJS的class-validator自定义验证器,在创建/更新角色时检查是否存在未删除的同名记录:

第一步:创建自定义验证器

import { ValidatorConstraint, ValidatorConstraintInterface, ValidationArguments } from 'class-validator';
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Role } from './role.entity';

@ValidatorConstraint({ name: 'uniqueActiveRoleTitle', async: true })
@Injectable()
export class UniqueActiveRoleTitleValidator implements ValidatorConstraintInterface {
  constructor(
    @InjectRepository(Role) private readonly roleRepo: Repository<Role>,
  ) {}

  async validate(title: string, args: ValidationArguments) {
    // 查询是否存在未删除的同名角色
    const existingRole = await this.roleRepo.findOne({
      where: { title, deletedDate: null },
    });

    // 如果是更新操作,需要排除当前记录本身
    if (args.object['id']) {
      return !existingRole || existingRole.id === args.object['id'];
    }
    return !existingRole;
  }

  defaultMessage() {
    return '该角色名称已存在';
  }
}

第二步:在DTO中使用验证器

import { IsString, IsNotEmpty, Validate } from 'class-validator';
import { UniqueActiveRoleTitleValidator } from './unique-active-role-title.validator';

export class CreateRoleDto {
  @IsString()
  @IsNotEmpty()
  @Validate(UniqueActiveRoleTitleValidator)
  title: string;
}

注意事项:

  • 这个方案只能作为数据库约束的补充,不能完全替代——因为并发场景下可能出现两个请求同时通过应用层校验,最终插入重复数据的情况。

方案三:手动用查询构建器校验(简单场景适用)

如果你的项目逻辑比较简单,也可以在Service层创建/更新角色时,手动用TypeORM查询构建器检查是否存在未删除的同名记录:

// role.service.ts
async create(createRoleDto: CreateRoleDto) {
  // 检查是否存在未删除的同名角色
  const existingRole = await this.roleRepo
    .createQueryBuilder('role')
    .where('role.title = :title', { title: createRoleDto.title })
    .andWhere('role.deletedDate IS NULL')
    .getOne();

  if (existingRole) {
    throw new ConflictException('角色名称已存在');
  }

  return this.roleRepo.save(createRoleDto);
}

注意事项:

  • 同样存在并发竞态问题,建议和数据库层面的约束配合使用。

内容的提问来源于stack exchange,提问作者V.Tur

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.29 20:04:05