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

使用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的内置乐观锁逻辑,灵活性更强,适合高并发场景

注意事项

  1. 如果你用方案一,确保不要直接保存前端传入的实体对象——一定要先从数据库加载最新实体,再修改保存,否则TypeORM不会自动添加版本条件。
  2. 方案二中,不要直接把整个entity传入set(),因为entity里的version是旧值,我们需要手动设置version: () => "version + 1"来实现递增。
  3. 在NestJS中,你可以全局注册异常过滤器,统一处理OptimisticLockingFailedError,避免每个服务都重复写捕获逻辑。

内容的提问来源于stack exchange,提问作者jacobytes

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.29 16:37:48