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
相关产品推荐
相关产品推荐

