NestJS + TypeORM场景下如何统一处理用户名与邮箱字段重复冲突
TypeORM + Nest 唯一约束冲突统一处理方案
前置准备:Entity 唯一约束定义
首先在实体中声明单字段、组合唯一约束,建议手动指定索引名方便后续错误解析:
import { Entity, Column, Unique, PrimaryGeneratedColumn } from 'typeorm'; @Entity() // 手动指定组合唯一约束的索引名 @Unique('UQ_USER_USERNAME_EMAIL', ['username', 'email']) export class User { @PrimaryGeneratedColumn() id: number; // 单字段唯一约束,手动指定索引名 @Column({ unique: 'UQ_USER_USERNAME' }) username: string; @Column({ unique: 'UQ_USER_EMAIL' }) email: string; @Column() password: string; }
方案1:局部 try/catch 处理
TypeORM 触发唯一约束冲突时会抛出 QueryFailedError,可以在 Service 层的写操作中统一捕获处理,支持同时覆盖单字段、组合字段重复场景:
import { Injectable, BadRequestException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository, QueryFailedError } from 'typeorm'; import { User } from './user.entity'; @Injectable() export class UserService { constructor( @InjectRepository(User) private userRepository: Repository<User>, ) {} async create(userData: Partial<User>) { try { const user = this.userRepository.create(userData); return await this.userRepository.save(user); } catch (err) { // 仅处理TypeORM查询失败错误 if (err instanceof QueryFailedError) { const driverError = err.driverError; // 根据数据库类型判断唯一约束错误码,示例为MySQL,PostgreSQL替换为'23505',SQLite替换为'SQLITE_CONSTRAINT_UNIQUE' if (driverError.code === 'ER_DUP_ENTRY') { // 从错误信息中匹配触发冲突的索引名 const uniqueKey = driverError.sqlMessage.match(/for key '([^']+)'/)?.[1]; // 根据索引名返回对应提示 switch (uniqueKey) { case 'UQ_USER_USERNAME': throw new BadRequestException('用户名已存在'); case 'UQ_USER_EMAIL': throw new BadRequestException('邮箱已被注册'); case 'UQ_USER_USERNAME_EMAIL': throw new BadRequestException('用户名和邮箱组合已存在'); default: throw new BadRequestException('数据唯一约束冲突'); } } } // 非唯一约束错误继续抛出 throw err; } } }
方案2:全局异常过滤器统一处理
如果不想在每个Service中重复写try/catch逻辑,可以通过Nest全局异常过滤器统一处理所有唯一约束冲突:
- 首先创建异常过滤器文件:
import { ExceptionFilter, Catch, ArgumentsHost, BadRequestException } from '@nestjs/common'; import { QueryFailedError } from 'typeorm'; import { Response } from 'express'; @Catch(QueryFailedError) export class TypeOrmUniqueConstraintFilter implements ExceptionFilter { catch(exception: QueryFailedError, host: ArgumentsHost) { const ctx = host.switchToHttp(); const response = ctx.getResponse<Response>(); const driverError = exception.driverError; // 判断是否为唯一约束错误 const isUniqueError = driverError.code === 'ER_DUP_ENTRY' // MySQL || driverError.code === '23505' // PostgreSQL || driverError.code === 'SQLITE_CONSTRAINT_UNIQUE'; // SQLite if (isUniqueError) { // 索引和提示的映射关系可以单独抽为常量文件统一维护 const keyMap = { 'UQ_USER_USERNAME': '用户名已存在', 'UQ_USER_EMAIL': '邮箱已被注册', 'UQ_USER_USERNAME_EMAIL': '用户名和邮箱组合已存在' }; const uniqueKey = driverError.sqlMessage.match(/for key '([^']+)'/)?.[1]; const message = keyMap[uniqueKey] || '数据唯一约束冲突'; throw new BadRequestException(message); } // 非唯一约束的查询错误返回默认信息 response.status(500).json({ statusCode: 500, message: '数据库操作失败' }); } }
- 注册全局过滤器,在
main.ts中添加:
import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { TypeOrmUniqueConstraintFilter } from './filters/typeorm-unique.filter'; async function bootstrap() { const app = await NestFactory.create(AppModule); app.useGlobalFilters(new TypeOrmUniqueConstraintFilter()); await app.listen(3000); } bootstrap();
注册完成后所有路由的数据库唯一约束冲突都会被自动捕获处理,无需额外编写try/catch逻辑。
注意事项
- 不同数据库的错误码需要对应调整,上述示例中已标注主流数据库的唯一约束错误码
- 手动指定唯一索引名可以避免TypeORM自动生成的索引名不固定导致解析失败的问题
- 组合唯一约束的判断逻辑和单字段完全一致,仅需要在映射关系中添加对应索引名的提示即可
内容的提问来源于stack exchange,提问作者Hamza Zahidul Islam
相关产品推荐
相关产品推荐

