使用TypeORM结合NestJS时,如何在保存实体前验证@VersionColumn值以避免并发更新冲突
解决TypeORM + PostgreSQL乐观锁(版本验证)问题
你遇到的这个并发更新覆盖问题,本质是需要实现乐观锁机制——确保只有当客户端携带的版本号与数据库最新版本一致时,更新才会生效,否则直接拒绝。其实TypeORM本身就支持乐观锁,或者你也可以用QueryBuilder实现原子更新来彻底避免间隙问题,下面给你两种可行的解决方案:
方案一:利用TypeORM内置乐观锁机制
TypeORM的@VersionColumn其实自带乐观锁逻辑,但前提是你要基于从数据库加载的实体进行修改和保存。当你调用save()时,TypeORM会自动生成带版本条件的UPDATE语句:UPDATE ... WHERE id = ? AND version = ?,如果此时数据库中的版本已经被其他请求更新,这个语句会影响0行数据,TypeORM会抛出OptimisticLockingFailedError,你可以捕获这个异常来拒绝操作。
修改你的代码如下:
import { OptimisticLockingFailedError } from "typeorm"; import { ConflictException, NotFoundException } from "@nestjs/common"; async update(id: number, updateDto: FooDto): Promise<boolean> { // 先查询最新的实体记录 const entity = await this._configurationRepository.findOneBy({ id }); if (!entity) { throw new NotFoundException("目标记录不存在"); } // 先验证客户端传入的版本号是否匹配当前数据库版本 if (updateDto.version !== entity.version) { throw new ConflictException("版本不匹配,请刷新后重试"); } // 将DTO的更新字段赋值给实体 Object.assign(entity, updateDto); try { await this._configurationRepository.save(entity); return true; } catch (error) { // 捕获乐观锁失败的异常,这说明在查询和保存之间,记录被其他请求更新了 if (error instanceof OptimisticLockingFailedError) { throw new ConflictException("记录已被其他用户更新,请刷新后重试"); } // 抛出其他未知异常 throw error; } }
为什么这个方案安全?
你可能担心“查询-保存”之间的间隙,但实际上save()生成的UPDATE语句是原子性的——即使在查询后有其他请求更新了版本,这个UPDATE会因为版本不匹配而执行失败,不会覆盖最新数据,最终通过异常告诉你操作无效。
方案二:用QueryBuilder实现原子更新(无查询间隙)
如果你不想先查询实体,想彻底消除“查询-保存”的间隙,可以直接用QueryBuilder构造一个原子更新语句,把版本验证、字段更新、版本递增放在同一个SQL操作里,完全避免并发问题。
代码示例:
import { ConflictException } from "@nestjs/common"; async update(entity: Foo): Promise<Foo | null> { const updateResult = await this._configurationRepository .createQueryBuilder() .update(Foo) .set({ // 这里列出你需要更新的字段,注意不要包含version name: entity.name, description: entity.description, // 手动递增版本号 version: () => "version + 1" }) // 仅当ID和版本号完全匹配时才执行更新 .where("id = :id AND version = :version", { id: entity.id, version: entity.version }) // 返回更新后的完整记录(PostgreSQL支持RETURNING) .returning("*") .execute(); // 如果没有行被更新,说明版本不匹配或记录不存在 if (updateResult.raw.length === 0) { throw new ConflictException("版本不匹配,更新失败"); } // 返回更新后的实体 return updateResult.raw[0]; }
这个方案的优势:
- 完全原子操作,没有任何中间间隙,性能也更高(减少一次查询)
- 不需要依赖TypeORM的内置乐观锁逻辑,灵活性更强,适合高并发场景
注意事项
- 如果你用方案一,确保不要直接保存前端传入的实体对象——一定要先从数据库加载最新实体,再修改保存,否则TypeORM不会自动添加版本条件。
- 方案二中,不要直接把整个
entity传入set(),因为entity里的version是旧值,我们需要手动设置version: () => "version + 1"来实现递增。 - 在NestJS中,你可以全局注册异常过滤器,统一处理
OptimisticLockingFailedError,避免每个服务都重复写捕获逻辑。
内容的提问来源于stack exchange,提问作者jacobytes
相关产品推荐
相关产品推荐

