NestJS / TypeORM数据库事务

16
假设我们有两个服务A和B。 服务A有一个函数执行以下操作:
  1. 验证数据
  2. 调用服务B的函数,对数据库进行更改
  3. 做一些其他事情
  4. 对数据库进行更改
现在,假设其中一个步骤3或4失败了。 由于服务B对数据库进行了更改,这些更改仍然存在。
在这种情况下,有没有办法回滚数据库?我考虑过数据库事务,但是我找不到在nest js中执行该操作的方法,尽管TypeOrm支持它,但它看起来并不自然适合nest。 如果不行,我现在“卡住”了,因为服务B进行了更改,而不应该由A进行更改。
非常感谢。
5个回答

17

有许多解决方案可用,它们都应该基于SQL事务管理。

个人认为最简单的方法是在执行与数据库相关的代码时使用相同的EntityManager实例。然后你可以使用类似以下方式:

getConnection().transaction(entityManager -> {
    service1.doStuff1(entityManager);
    service2.doStuff2(entityManager);
});

您可以从EntityManager实例中生成一个QueryRunner,在执行ORM操作之外的原始SQL时,它将被包装在同一事务中。您还需要从EntityManager中生成Repository实例,否则它们将在主事务之外执行代码。


28
虽然这是一个解决方案,但明显存在一个问题:每个服务方法都必须接受一个EntityManager参数。如果这些服务使用其他可注入的项,比如数据访问对象(DAO),那么服务方法将需要进一步将EntityManager实例传递下去(例如到DAO方法)。我认为这是一个显而易见和冲动的解决方案——一个有效的解决方案!但它会增加大量样板代码,并且不太适合DI和IoC。 - benbotto
@avejidah 在 TypeScript Node 中没有 ThreadLocal,因此您无法存储一个上下文实例,在其中保留对实体管理器的引用以及所有服务。Singleton 也不起作用,因为每个请求都有一个实体管理器(由于事件队列在 node 中一起处理多个请求)。唯一的方法是将实体管理器存储在 NestJS 执行上下文的某种方式中,但这样会失去手动实体管理器使用和手动事务管理所需的功能。 - zenbeni
2
我个人会保留参数,并在方法上使用装饰器来注入一个新的实体管理器(如果它为空),这样我就不必担心初始化它。对我来说,保持显式事务管理比代码糖更重要,我个人并不觉得它是样板代码。 - zenbeni
1
@zenbeni,你能分享一段代码片段吗?你使用的是哪个装饰器?也许是@TransactionManager - Alfonso
如果我使用nestjsx/crud,有什么建议吗? - Tan Dat

12

typeorm-transactional使用CLS(Continuation Local Storage)来处理和传播不同存储库和服务方法之间的事务。

@Injectable()
export class PostService {
  constructor(
    private readonly authorRepository: AuthorRepository,
    private readonly postRepository: PostRepository,
  ) {}

  @Transactional() // will open a transaction if one doesn't already exist
  async createPost(authorUsername: string, message: string): Promise<Post> {
    const author = await this.authorRepository.create({ username: authorUsername });
    return this.postRepository.save({ message, author_id: author.id });
  }
}

这是最优雅的方法来完成它。 - flux
这,在我看来,是最简洁的方法。 - undefined

10

由于我需要使用悲观锁,以下是我解决此问题的方法。

我认为这是使用“Nest”处理事物的最佳方式,因为您只需请求NestJS注入一个 Typeorm Connection 实例即可开始工作。

@Injectable()
class MyService {
  // 1. Inject the Typeorm Connection
  constructor(@InjectConnection() private connection: Connection) { }

  async findById(id: number): Promise<Thing> {
    return new Promise(resolve => {
      // 2. Do your business logic
      this.connection.transaction(async entityManager => {
        resolve(
          await entityManager.findOne(Thing, id, {
            lock: { mode: 'pessimistic_write' },
          }),
        );
      });
    });
  }
}

只需将您需要的任何其他逻辑放置在.transaction块内,然后您就可以开始了。

注意: 您必须使用.transaction方法提供的entityManager,否则它将无法工作。


谢谢!我不知道如何注入连接。 - Selast Lambou

2
在这种情况下,您必须为两个数据库操作使用相同的事务管理器。不幸的是,我没有一个示例存储库,但我在Node中发现了使用Continuation Local Storage (CLS)的潜在解决方案:https://github.com/typeorm/typeorm/issues/1895。这适用于Express.js,但您可以在每个请求上下文中创建TransactionManager实例(例如,在nest中间件中)并将其存储。然后,您将能够在整个服务方法调用中重复使用此事务管理器,前提是它们使用上面链接中的@Transaction装饰器实现进行注释。如果函数链中没有错误,则事务管理器将提交所有所做的更改。否则,管理器将回滚任何更改。希望这有所帮助!

0
function assignChildToParent(nestedObj, parentIdKey) {
  const parentMap = {};

  for (const prop in nestedObj) {
    if (nestedObj.hasOwnProperty(prop)) {
      const obj = nestedObj[prop];
      const parentId = obj[parentIdKey];

      parentMap[prop] = { ...obj, children: {} };

      if (parentId !== null && parentMap.hasOwnProperty(parentId)) {
        parentMap[parentId].children[prop] = parentMap[prop];
      }
    }
  }

  const result = {};

  for (const prop in parentMap) {
    if (parentMap.hasOwnProperty(prop) && parentMap[prop][parentIdKey] === null) {
      result[prop] = parentMap[prop];
    }
  }

  return result;
}

const nestedObj = {
  obj1: { name: 'Parent 1', parentId: null },
  obj2: { name: 'Parent 2', parentId: 'obj1' },
  obj3: { name: 'Parent 3', parentId: null }
};

const parentIdKey = 'parentId';

const result = assignChildToParent(nestedObj, parentIdKey);
console.log(result);

目前你的回答不够清晰,请编辑并添加更多细节,以帮助其他人理解它如何回答问题。你可以在帮助中心找到有关如何编写好答案的更多信息。 - Community

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接