生产环境TypeORM+PostgreSQL报唯一约束冲突错误,应用未崩溃
错误信息
[Nest] 41 - 2022年11月3日 上午9:44:20 ERROR [ExceptionsHandler] duplicate key value violates unique constraint "UQ_167efd0746b6a9808b96f2e0d66"
QueryFailedError: duplicate key value violates unique constraint "UQ_167efd0746b6a9808b96f2e0d66"
at QueryFailedError.TypeORMError [as constructor] (/app/src/error/TypeORMError.ts:7:9)
at new QueryFailedError (/app/src/error/QueryFailedError.ts:9:9)
at PostgresQueryRunner. (/app/src/driver/postgres/PostgresQueryRunner.ts:247:19)
at step (/app/node_modules/typeorm/node_modules/tslib/tslib.js:144:27)
at Object.throw (/app/node_modules/typeorm/node_modules/tslib/tslib.js:125:57)
at rejected (/app/node_modules/typeorm/node_modules/tslib/tslib.js:116:69)
at runMicrotasks ()
at processTicksAndRejections (node:internal/process/task_queues:96:5)
核心原因
这个错误是PostgreSQL抛出的,说明插入/更新的数据违反了表的唯一约束(UQ_167efd0746b6a9808b96f2e0d66)——某个字段(或字段组合)的值已存在于表中,但该字段被设定为唯一值。
首先要定位约束对应的表和字段:
登录PostgreSQL执行以下SQL:
SELECT conrelid::regclass AS table_name, conname AS constraint_name, pg_get_constraintdef(c.oid) AS constraint_def FROM pg_constraint c WHERE conname = 'UQ_167efd0746b6a9808b96f2e0d66';
执行后就能拿到约束关联的表和具体字段(比如user表的email字段)。
常见场景与解决方法
1. 业务逻辑重复触发
检查代码中是否存在重复执行插入/更新的逻辑:比如接口被重复调用、定时任务重复跑、消息队列重复消费等。
- 解决:在业务层加幂等校验,比如给请求加唯一标识,或者插入前先查询目标值是否存在,不存在再执行插入。
2. 自增ID序列不一致
如果是主键自增场景,可能是手动插入过主键值,导致PostgreSQL的序列值和表中最大ID不匹配,后续自动生成的ID已存在。
- 解决:重置序列值(以
user表的id主键为例):
SELECT setval('user_id_seq', (SELECT MAX(id) FROM user));
3. TypeORM实体约束定义错误
检查实体类的唯一约束是否符合业务需求:比如误将非唯一字段设为唯一,或者组合约束定义错误。
- 单字段唯一示例:
@Entity() export class User { @Column({ unique: true }) email: string; }
- 组合字段唯一示例:
@Entity() @Unique(['name', 'phone']) export class User { @Column() name: string; @Column() phone: string; }
4. 高并发插入冲突
高并发场景下,多个请求同时插入相同唯一值,导致其中一个请求失败。
- 解决:
- 使用PostgreSQL的
INSERT ... ON CONFLICT语法,TypeORM中可通过save方法配置:
await userRepository.save(user, { conflictPaths: ['email'], // 指定唯一约束字段 overwrite: ['name', 'phone'] // 冲突时更新指定字段 });- 业务层加分布式锁,确保同一时间只有一个请求处理该唯一值的插入。
- 使用PostgreSQL的
临时日志优化
如果不想让错误日志刷屏,可以在NestJS中做全局异常过滤,捕获唯一约束错误并返回友好响应:
@Catch(QueryFailedError) export class QueryFailedFilter implements ExceptionFilter { catch(exception: QueryFailedError, host: ArgumentsHost) { const ctx = host.switchToHttp(); const response = ctx.getResponse(); if (exception.message.includes('duplicate key value violates unique constraint')) { response.status(HttpStatus.CONFLICT).json({ message: '该数据已存在', error: 'Conflict', statusCode: 409, }); } else { response.status(HttpStatus.INTERNAL_SERVER_ERROR).json({ message: '数据库操作失败', error: 'Internal Server Error', statusCode: 500, }); } } }
然后在主模块注册过滤器:
@Module({ providers: [ { provide: APP_FILTER, useClass: QueryFailedFilter, }, ], }) export class AppModule {}
内容的提问来源于stack exchange,提问作者gaiyaobed

