Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

a better solution for unit of work implementation. #1

Open
khalvai opened this issue Dec 30, 2024 · 9 comments
Open

a better solution for unit of work implementation. #1

khalvai opened this issue Dec 30, 2024 · 9 comments

Comments

@khalvai
Copy link

khalvai commented Dec 30, 2024

Hi there
Really appreciate what you have done, i think there are some bad practices in code and it can be improved for i.e:

import { EntityManager } from 'typeorm'
export abstract class SaveReportOutPort {
  abstract save(order: ReportEntity, manager?: EntityManager): Promise<ReportEntity>
}

here

This abstraction might not be ideal because it relies on TypeOrm properties. If we decide to switch to a different ORM, we would need to modify the abstraction accordingly.

I have solved this problem with abstraction and independent from ORMs:
https://github.com/khalvai/unti-of-work/blob/main/src/common/prisma.unit-of-work.ts

and it is usage:

await this.unitOfWork.transaction(async () => {
  await this.paymentRepository.create()

  await this.postRepository.updateMany('PLACEHOLDER')
})

in :
https://github.com/khalvai/unti-of-work/blob/main/src/app.service.ts

@zhuravlevma
Copy link
Owner

zhuravlevma commented Jan 1, 2025

@khalvai Hi, thanks for the feedback )
your implementation is interesting and I've seen it and even tried it. The only negative I see is that the transaction is tied to a single context.

For example, imagine that we have such a code

await Promise.all([ this.unitOfWork.transaction(...), this.unitOfWork.transaction(...) ])

Since you have a global transaction per request, they will compete and bugs are possible.
Or did I make a mistake somewhere and your implementation takes into account such cases?

@zhuravlevma
Copy link
Owner

@khalvai
By the way, using asyncLocalStorage.run might help solve the problem.
This way, each unitOfWork call will have its own isolated context and store. I'll try to tweak it a bit when I have some free time.

@khalvai
Copy link
Author

khalvai commented Jan 1, 2025

Hi, thanks for you response.

You know in this senario:

await this.unitOfWork.transaction(...)
await this.unitOfWork.transaction(...)

we won't have any problem, because after each transaction we will clear the context:

 async transaction<T>(fn: () => Promise<T>): Promise<T> {
    return await this.prisma.$transaction(async (tx) => {
      RequestContextService.setTransactionConnection(tx);

      const t = await fn();

      RequestContextService.cleanTransactionConnection();

      return t;
    });
    }

I have not tried AsyncLocalStorage, but i guess we must instatiate an object of that class for each unti of work...

@zhuravlevma
Copy link
Owner

zhuravlevma commented Jan 1, 2025

@khalvai
No, I meant Promise.all (race...)

when you have transactions running in parallel within the same query, then the context will be cleared uncontrollably and there may be bugs

  1. T1 run and set transaction for request object
  2. T2 run in parallel and set transaction for request object
  3. T1 execute and clean transaction
  4. T2 error, transaction not found :(

If you make async local storage, it will remember the transaction not for the request, but for the local call. in other words, parallelism within a single request will be possible.

if all requests work in a row with await, then your scheme will work fine and it's good.

@khalvai
Copy link
Author

khalvai commented Jan 1, 2025

yeah, if it creates a local sotrage for each call( transaction call) then we won't have problem with async calls..
thanks.

@zhuravlevma
Copy link
Owner

@khalvai
I have finalized the current implementation

@khalvai
Copy link
Author

khalvai commented Jan 5, 2025

@zhuravlevma
Yeah, I see, what you have done, good job :

return await this.asyncLocalStorage.run(queryRunner, async () => {}
how this method uses the transaction?

  async save(report: ReportEntity): Promise<ReportEntity> {
    const reportOrm = ReportMapper.mapToOrm(report);

    const savedReport = await this.typeormClient.client
      .getRepository(ReportOrmEntity)
      .save(reportOrm);

    return ReportMapper.mapToDomain(savedReport);
  }

i think you are using it:

return this.uow.transaction(async () => {
       const updatedWh = await this.saveWhPort.saveWarehouse(warehouse);
       await this.saveReport.save(report);
       return updatedWh;
     });

@zhuravlevma
Copy link
Owner

@khalvai
the transaction object is stored inside the method this.asyncLocalStorage.run
all methods that will be called inside the run tree will receive a transaction object.

each transaction call will save its own transaction

@khalvai
Copy link
Author

khalvai commented Jan 10, 2025

@zhuravlevma
Hi, sorry for late responsem, i was busy with deployment sprint.

i meant, how exactly this code, using transaction connect?

  async saveWarehouse(warehouse: WarehouseEntity): Promise<WarehouseEntity> {
    const warehouseORM = WarehouseMapper.mapToORM(warehouse);

    const whOrm = await this.typeormClient.client
      .getRepository(WarehouseOrmEntity)
      .save(warehouseORM);

    return WarehouseMapper.mapToDomain(whOrm);
  }

we aimed to run this method within a transaction with other method.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants