paint-brush
Las formas más convenientes de escribir transacciones dentro de la pila Nest.js + TypeORMpor@alphamikle
16,279 lecturas
16,279 lecturas

Las formas más convenientes de escribir transacciones dentro de la pila Nest.js + TypeORM

por Mike Alfa17m2021/02/28
Read on Terminal Reader
Read this story w/o Javascript

Demasiado Largo; Para Leer

Si tiene problemas con las transacciones en Nest.js, este artículo lo ayudará.

Company Mentioned

Mention Thumbnail
featured image - Las formas más convenientes de escribir transacciones dentro de la pila Nest.js + TypeORM
Mike Alfa HackerNoon profile picture

En muchos casos, los desarrolladores deben usar transacciones cuando realizan varias operaciones en el servidor. Por ejemplo, una transferencia de dinero u otro valor medible, y mucho más.

Con tales operaciones, realmente no quiero recibir un error que interrumpa el proceso y viole la integridad de los datos.

¿Qué es una "transacción" de todos modos? Wikipedia dice :

"Una transacción de base de datos simboliza una unidad de trabajo realizada dentro de un sistema de gestión de base de datos (o un sistema similar) contra una base de datos, y tratada de manera coherente y confiable independientemente de otras transacciones. Una transacción generalmente representa cualquier cambio en una base de datos".

Las transacciones en un entorno de base de datos tienen dos propósitos principales:

  1. Proporcionar unidades de trabajo confiables que permitan la recuperación correcta de fallas y mantener una base de datos consistente incluso en casos de falla del sistema, cuando la ejecución se detiene (total o parcialmente) y muchas operaciones sobre una base de datos permanecen sin completar, con un estado poco claro.
  2. Proporcionar aislamiento entre programas que acceden a una base de datos al mismo tiempo. Si no se proporciona este aislamiento, los resultados de los programas posiblemente sean erróneos.

Ahora, considere una situación en la que puede ocurrir un error, lo que puede tener consecuencias muy desagradables si no utiliza las transacciones.

Hice un pequeño proyecto en el que hay dos entidades:

  • Usuario
  • Cartera

Los usuarios pueden transferir dinero entre sí. Al transferir, se verifica la suficiencia de la cantidad en el saldo del remitente, así como muchos otros controles. Si ocurre una situación en la que el dinero ha sido debitado del saldo del remitente pero no transferido a la cuenta del destinatario, o viceversa, veremos a una persona muy triste y enojada, o no veremos a una muy feliz ( depende de la monto de la transferencia ).

Genial, con el hecho de que las transacciones son importantes y deben resolverse ( esperemos que todos estén de acuerdo con esto ). Pero, ¿cómo los aplicas?

Primero, veamos las opciones para consultas con errores y sin errores que ocurrirán si usa PostgreSQL.

El conjunto habitual de consultas sin errores:

 // ... SELECT "User" . "id" AS "User_id" , "User" . "name" AS "User_name" , "User" . "defaultPurseId" AS "User_defaultPurseId" FROM "user" "User" WHERE "User" . "id" IN ($ 1 ) START TRANSACTION UPDATE "purse" SET "balance" = $ 2 WHERE "id" IN ($ 1 ) COMMIT SELECT "Purse" . "id" AS "Purse_id" , "Purse" . "balance" AS "Purse_balance" , "Purse" . "userId" AS "Purse_userId" FROM "purse" "Purse" WHERE "Purse" . "id" IN ($ 1 ) START TRANSACTION UPDATE "purse" SET "balance" = $ 2 WHERE "id" IN ($ 1 ) COMMIT

Por cierto, no escribí esta solicitud a mano, sino que la saqué de los registros de ORM, pero refleja la esencia.

Todo es bastante simple y directo. Para construir las consultas se utilizó TypeORM , al que volveremos un poco más adelante.

La configuración de ORM y Postgres está configurada de forma predeterminada, por lo que cada operación se realizará en su propia transacción, pero para aprovechar esta ventaja, debe escribir una consulta en la que toda la lógica asociada con la base de datos se llevará a cabo a la vez.

A continuación se muestra un ejemplo de la ejecución de múltiples consultas ejecutadas en una transacción:

 START TRANSACTION // ... SELECT "Purse" . "id" AS "Purse_id" , "Purse" . "balance" AS "Purse_balance" , "Purse" . "userId" AS "Purse_userId" FROM "purse" "Purse" WHERE "Purse" . "id" IN ($ 1 ) UPDATE "purse" SET "balance" = $ 2 WHERE "id" IN ($ 1 ) SELECT "Purse" . "id" AS "Purse_id" , "Purse" . "balance" AS "Purse_balance" , "Purse" . "userId" AS "Purse_userId" FROM "purse" "Purse" WHERE "Purse" . "id" IN ($ 1 ) UPDATE "purse" SET "balance" = $ 2 WHERE "id" IN ($ 1 ) COMMIT

La diferencia clave con el ejemplo anterior de solicitudes es que, en este caso, todas las solicitudes se ejecutan en una transacción y, por lo tanto, si ocurre un error en algún momento, la transacción completa se revertirá con todas las solicitudes dentro de ella.

Más o menos así:

 START TRANSACTION // ... SELECT "Purse" . "id" AS "Purse_id" , "Purse" . "balance" AS "Purse_balance" , "Purse" . "userId" AS "Purse_userId" FROM "purse" "Purse" WHERE "Purse" . "id" IN ($ 1 ) UPDATE "purse" SET "balance" = $ 2 WHERE "id" IN ($ 1 ) ROLLBACK

Y aquí, por cierto, está el código que produjo todas las consultas SQL anteriores. Contiene una bandera, cuando se establece, se produce un error en el momento más inoportuno:

 // ... async makeRemittance(fromId: number , toId: number , sum: number , withError = false , transaction = true ): Promise <RemittanceResultDto> { const fromUser = await this .userRepository.findOne(fromId, { transaction }); const toUser = await this .userRepository.findOne(toId, { transaction }); if (fromUser === undefined ) { throw new Error (NOT_FOUND_USER_WITH_ID(fromId)); } if (toUser === undefined ) { throw new Error (NOT_FOUND_USER_WITH_ID(toId)); } if (fromUser.defaultPurseId === null ) { throw new Error (USER_DOES_NOT_HAVE_PURSE(fromId)); } if (toUser.defaultPurseId === null ) { throw new Error (USER_DOES_NOT_HAVE_PURSE(toId)); } const fromPurse = await this .purseRepository.findOne(fromUser.defaultPurseId, { transaction }); const toPurse = await this .purseRepository.findOne(toUser.defaultPurseId, { transaction }); const modalSum = Math .abs(sum); if (fromPurse.balance < modalSum) { throw new Error (NOT_ENOUGH_MONEY(fromId)); } fromPurse.balance -= sum; toPurse.balance += sum; await this .purseRepository.save(fromPurse, { transaction }); if (withError) { throw new Error ( 'Unexpectable error was thrown while remittance' ); } await this .purseRepository.save(toPurse, { transaction }); const remittance = new RemittanceResultDto(); remittance.fromId = fromId; remittance.toId = toId; remittance.fromBalance = fromPurse.balance; remittance.sum = sum; return remittance; } // ...

¡Multa! Nos salvamos de pérdidas o de usuarios muy molestos ( al menos en temas relacionados con transferencias de dinero ).

Formas Alternativas

¿Que sigue? ¿Qué otras formas hay de escribir una transacción? Dio la casualidad de que la persona cuyo artículo estás leyendo actualmente ( este soy yo ) realmente ama un marco maravilloso cuando tiene que escribir un backend.

El nombre de este marco es Nest.js. Funciona en la plataforma Node.js y el código que contiene está escrito en TypeScript. Este gran marco tiene soporte, casi listo para usar, para el mismísimo TypeORM.

Cuál (¿o cuál?) a mí, da la casualidad, también me gusta mucho. No me gustó solo una cosa: un enfoque bastante confuso, como me parece, demasiado complicado para escribir transacciones.

Este es el ejemplo oficial para escribir transacciones:

 import { getConnection } from 'typeorm' ; await getConnection().transaction( async transactionalEntityManager => { await transactionalEntityManager.save(users); await transactionalEntityManager.save(photos); // ... });

Segunda forma de crear transacciones a partir de la documentación:

 @Transaction () save(user: User, @TransactionManager () transactionManager: EntityManager) { return transactionManager.save(User, user); }

En general, el punto de este enfoque es el siguiente: necesita obtener un

 transactionEntityManager: EntityManager
- una entidad que le permitirá ejecutar consultas dentro de una transacción. Y luego use esta entidad para todas las acciones con la base. Suena bien, siempre y cuando no tenga que lidiar con el uso de este enfoque en la práctica.

Para empezar, no me gusta mucho la idea de inyectar dependencias directamente en los métodos de las clases de servicio, así como el hecho de que los métodos están escritos de esta manera quedan aislados en cuanto al uso de las dependencias inyectadas en el servicio. sí mismo.

Todas las dependencias necesarias para que el método funcione deberán colocarse en él. Pero lo más molesto es que si su método llama a otros servicios integrados en el suyo, entonces debe crear los mismos métodos especiales en esos servicios de terceros. y pasar

 transactionEntityManager
en ellos.

Al mismo tiempo, debe tenerse en cuenta que si decide utilizar el enfoque a través de decoradores, cuando transfiera el

 transactionEntityManager
de un servicio al segundo, y el método del segundo servicio también estará decorado; en el segundo método, recibirá el
 transactionEntityManager
que no se pasa como una dependencia, y la que crea el decorador, lo que significa dos transacciones diferentes, lo que significa usuarios desafortunados.

Empezar con ejemplos

A continuación se muestra el código para una acción de controlador que maneja las solicitudes de los usuarios:

 // ... @Post ( 'remittance-with-typeorm-transaction' ) @ApiResponse ({ type : RemittanceResultDto, }) async makeRemittanceWithTypeOrmTransaction( @Body () remittanceDto: RemittanceDto) { return await this .connection.transaction( transactionManager => { return this .appService.makeRemittanceWithTypeOrmV1(transactionManager, remittanceDto.userIdFrom, remittanceDto.userIdTo, remittanceDto.sum, remittanceDto.withError); }); } // ...

En él, necesitamos tener acceso a la

 connection
objeto de crear un
 transactionManager
. Podríamos hacer lo que aconseja la documentación de TypeORM, y simplemente usar el
 getConnection
función como se muestra arriba:

 import { getConnection } from 'typeorm' ; // ... @Post ( 'remittance-with-typeorm-transaction' ) @ApiResponse ({ type : RemittanceResultDto, }) async makeRemittanceWithTypeOrmTransaction( @Body () remittanceDto: RemittanceDto) { return await getConnection().transaction( transactionManager => { return this .appService.makeRemittanceWithTypeOrmV1(transactionManager, remittanceDto.userIdFrom, remittanceDto.userIdTo, remittanceDto.sum, remittanceDto.withError); }); } // ...

Pero me parece que dicho código será más difícil de probar, y esto es simplemente incorrecto ( gran argumento ). Por lo tanto, tendremos que pasar el

 connection
dependencia en el constructor del controlador. Es muy afortunado que Nest te permita hacer esto simplemente describiendo el campo en el constructor con el tipo apropiado:

 @Controller () @ApiTags ( 'app' ) export class AppController { constructor ( private readonly appService: AppService, private readonly connection: Connection, // <-- it is - what we need ) { } // ... }

Por lo tanto, llegamos a la conclusión de que para poder usar transacciones en Nest cuando se usa TypeORM, es necesario pasar el

 connection
class en el controlador/constructor de servicios, por ahora solo recordamos esto.

Ahora echemos un vistazo a la

 makeRemittanceWithTypeOrmV1
método de nuestro
 appService
:

 async makeRemittanceWithTypeOrmV1(transactionEntityManager: EntityManager, fromId: number , toId: number , sum: number , withError = false ) { const fromUser = await transactionEntityManager.findOne(User, fromId); // <-- we need to use only provided transactionEntityManager, for make all requests in transaction const toUser = await transactionEntityManager.findOne(User, toId); // <-- and there if (fromUser === undefined ) { throw new Error (NOT_FOUND_USER_WITH_ID(fromId)); } if (toUser === undefined ) { throw new Error (NOT_FOUND_USER_WITH_ID(toId)); } if (fromUser.defaultPurseId === null ) { throw new Error (USER_DOES_NOT_HAVE_PURSE(fromId)); } if (toUser.defaultPurseId === null ) { throw new Error (USER_DOES_NOT_HAVE_PURSE(toId)); } const fromPurse = await transactionEntityManager.findOne(Purse, fromUser.defaultPurseId); // <-- there const toPurse = await transactionEntityManager.findOne(Purse, toUser.defaultPurseId); // <--there const modalSum = Math .abs(sum); if (fromPurse.balance < modalSum) { throw new Error (NOT_ENOUGH_MONEY(fromId)); } fromPurse.balance -= sum; toPurse.balance += sum; await this .appServiceV2.savePurse(fromPurse); // <-- oops, something was wrong if (withError) { throw new Error ( 'Unexpectable error was thrown while remittance' ); } await transactionEntityManager.save(toPurse); const remittance = new RemittanceResultDto(); remittance.fromId = fromId; remittance.toId = toId; remittance.fromBalance = fromPurse.balance; remittance.sum = sum; return remittance; }

Todo el proyecto es sintético, pero para mostrar lo desagradable de este enfoque, moví el

 savePurse
método utilizado para guardar la billetera en un separado
 appServiceV2
 service
, y utilizó este servicio con este método dentro del considerado
 makeRemittanceWithTypeOrmV1
método. Puedes ver el código de este método y servicio a continuación:

 @Injectable () export class AppServiceV2 { constructor ( @InjectRepository (Purse) private readonly purseRepository: Repository<Purse>, ) { } async savePurse(purse: Purse) { await this .purseRepository.save(purse); } // ... }

En realidad, en esta situación, obtenemos las siguientes consultas SQL:

 START TRANSACTION // ... SELECT "User" . "id" AS "User_id" , "User" . "name" AS "User_name" , "User" . "defaultPurseId" AS "User_defaultPurseId" FROM "user" "User" WHERE "User" . "id" IN ($ 1 ) START TRANSACTION // < -- this transaction from appServiceV2 😩 UPDATE "purse" SET "balance" = $ 2 WHERE "id" IN ($ 1 ) COMMIT SELECT "Purse" . "id" AS "Purse_id" , "Purse" . "balance" AS "Purse_balance" , "Purse" . "userId" AS "Purse_userId" FROM "purse" "Purse" WHERE "Purse" . "id" IN ($ 1 ) UPDATE "purse" SET "balance" = $ 2 WHERE "id" IN ($ 1 ) COMMIT

Si enviamos una solicitud para que ocurra un error, veremos claramente que la transacción interna de

 appServiceV2
no se revierte y, por lo tanto, nuestros usuarios están indignados nuevamente.

 START TRANSACTION // ... SELECT "Purse" . "id" AS "Purse_id" , "Purse" . "balance" AS "Purse_balance" , "Purse" . "userId" AS "Purse_userId" FROM "purse" "Purse" WHERE "Purse" . "id" IN ($ 1 ) START TRANSACTION UPDATE "purse" SET "balance" = $ 2 WHERE "id" IN ($ 1 ) COMMIT ROLLBACK

Aquí concluimos que para un enfoque estándar de enlace troncal, debe tener métodos especiales a los que deberá pasar

 transactionEntityManager
.

Si queremos deshacernos de la necesidad de inyectar explícitamente el

 transactionEntityManager
en los métodos correspondientes, entonces la documentación nos aconseja mirar a los decoradores.

Al aplicarlos, obtenemos este tipo de acción del controlador:

 // ... @Post ( 'remittance-with-typeorm-transaction-decorators' ) @ApiResponse ({ type : RemittanceResultDto, }) async makeRemittanceWithTypeOrmTransactionDecorators( @Body () remittanceDto: RemittanceDto) { return this .appService.makeRemittanceWithTypeOrmV2(remittanceDto.userIdFrom, remittanceDto.userIdTo, remittanceDto.sum, remittanceDto.withError); } // ...

Ahora se ha vuelto más simple - no hay necesidad de usar el

 connection
class, ni en el constructor, ni llamando al método global TypeORM. Perfectamente. Pero el método de nuestro servicio aún debería recibir una dependencia:
 transactionEntityManager
. Aquí es donde esos decoradores vienen al rescate:

 // ... @Transaction () // <-- this async makeRemittanceWithTypeOrmV2(fromId: number , toId: number , sum: number , withError: boolean , @TransactionManager () transactionEntityManager: EntityManager = null /* <-- and this */ ) { const fromUser = await transactionEntityManager.findOne(User, fromId); const toUser = await transactionEntityManager.findOne(User, toId); if (fromUser === undefined ) { throw new Error (NOT_FOUND_USER_WITH_ID(fromId)); } if (toUser === undefined ) { throw new Error (NOT_FOUND_USER_WITH_ID(toId)); } if (fromUser.defaultPurseId === null ) { throw new Error (USER_DOES_NOT_HAVE_PURSE(fromId)); } if (toUser.defaultPurseId === null ) { throw new Error (USER_DOES_NOT_HAVE_PURSE(toId)); } const fromPurse = await transactionEntityManager.findOne(Purse, fromUser.defaultPurseId); const toPurse = await transactionEntityManager.findOne(Purse, toUser.defaultPurseId); const modalSum = Math .abs(sum); if (fromPurse.balance < modalSum) { throw new Error (NOT_ENOUGH_MONEY(fromId)); } fromPurse.balance -= sum; toPurse.balance += sum; await this .appServiceV2.savePurseInTransaction(fromPurse, transactionEntityManager); // <-- we will check is it will working if (withError) { throw new Error ( 'Unexpectable error was thrown while remittance' ); } await transactionEntityManager.save(toPurse); const remittance = new RemittanceResultDto(); remittance.fromId = fromId; remittance.toId = toId; remittance.fromBalance = fromPurse.balance; remittance.sum = sum; return remittance; } // ...

Ya hemos descubierto el hecho de que simplemente usar un método de servicio de terceros interrumpe nuestras transacciones. Por lo tanto, utilizamos el nuevo método del servicio de terceros.

 transactionEntityManager
, que se ve así:

 // .. @Transaction () async savePurseInTransaction(purse: Purse, @TransactionManager () transactionManager: EntityManager = null ) { await transactionManager.save(Purse, purse); } // ...

Como puede ver en el código, en este método también usamos decoradores; de esta manera logramos la uniformidad en todos los métodos del proyecto ( sí, sí ), y también nos deshacemos de la necesidad de usar

 connection
en el constructor de controladores usando nuestro servicio
 appServiceV2
.

Con este enfoque, obtenemos las siguientes solicitudes:

 START TRANSACTION // ... SELECT "Purse" . "id" AS "Purse_id" , "Purse" . "balance" AS "Purse_balance" , "Purse" . "userId" AS "Purse_userId" FROM "purse" "Purse" WHERE "Purse" . "id" IN ($ 1 ) START TRANSACTION SELECT "Purse" . "id" AS "Purse_id" , "Purse" . "balance" AS "Purse_balance" , "Purse" . "userId" AS "Purse_userId" FROM "purse" "Purse" WHERE "Purse" . "id" IN ($ 1 ) UPDATE "purse" SET "balance" = $ 2 WHERE "id" IN ($ 1 ) COMMIT SELECT "Purse" . "id" AS "Purse_id" , "Purse" . "balance" AS "Purse_balance" , "Purse" . "userId" AS "Purse_userId" FROM "purse" "Purse" WHERE "Purse" . "id" IN ($ 1 ) UPDATE "purse" SET "balance" = $ 2 WHERE "id" IN ($ 1 ) COMMIT

Y, como consecuencia, la destrucción de la lógica de transacción y aplicación en caso de error:

 START TRANSACTION // ... SELECT "Purse" . "id" AS "Purse_id" , "Purse" . "balance" AS "Purse_balance" , "Purse" . "userId" AS "Purse_userId" FROM "purse" "Purse" WHERE "Purse" . "id" IN ($ 1 ) START TRANSACTION SELECT "Purse" . "id" AS "Purse_id" , "Purse" . "balance" AS "Purse_balance" , "Purse" . "userId" AS "Purse_userId" FROM "purse" "Purse" WHERE "Purse" . "id" IN ($ 1 ) UPDATE "purse" SET "balance" = $ 2 WHERE "id" IN ($ 1 ) COMMIT ROLLBACK

La única forma de trabajo, que describe la documentación, es evitar el uso de decoradores. Si usa decoradores en todos los métodos a la vez, aquellos que serán usados ​​por otros servicios inyectarán los suyos propios.

 transactionEntityManagers
, como sucedió con nuestro
 appServiceV2
servicio y su
 savePurseInTransaction
método. Intentemos reemplazar este método con otro:

 // app.service.ts @Transaction () async makeRemittanceWithTypeOrmV2(fromId: number , toId: number , sum: number , withError: boolean , @TransactionManager () transactionEntityManager: EntityManager = null ) { // ... await this .appServiceV2.savePurseInTransactionV2(fromPurse, transactionEntityManager); // ... } // app.service-v2.ts // .. async savePurseInTransactionV2(purse: Purse, transactionManager: EntityManager) { await transactionManager.save(Purse, purse); } // ..

Para mantener la coherencia de nuestros métodos y eliminar la jerarquía que ha aparecido, que se manifiesta en el hecho de que algunos métodos pueden llamar a otros, pero otros no podrán llamar al primero, cambiaremos el método del

 appService
clase. Así, habiendo recibido la primera opción de la documentación.

Una Manera Diferente

Bueno, parece que todavía tenemos que inyectar esto

 connection
en los constructores del controlador. Pero la forma propuesta de escribir código con transacciones todavía parece muy engorrosa e inconveniente.

¿Qué hacer?

Resolviendo este problema, hice un paquete que te permite usar transacciones de la manera más simple. Se llama nest-transact .

¿Qué está haciendo? Todo es simple aquí. Para nuestro ejemplo con usuarios y transferencias de dinero, veamos la misma lógica escrita con nest-transact.

El código de nuestro controlador no ha cambiado, y como nos hemos asegurado de que no podemos prescindir

 connection
en el constructor, lo especificaremos:

 @Controller () @ApiTags ( 'app' ) export class AppController { constructor ( private readonly appService: AppService, private readonly connection: Connection, // <-- use this ) { } // ... }

Acción del controlador:

 // ... @Post ( 'remittance-with-transaction' ) @ApiResponse ({ type : RemittanceResultDto, }) async makeRemittanceWithTransaction( @Body () remittanceDto: RemittanceDto) { return await this .connection.transaction( transactionManager => { return this .appService.withTransaction(transactionManager) /* <-- this is interesting new thing*/ .makeRemittance(remittanceDto.userIdFrom, remittanceDto.userIdTo, remittanceDto.sum, remittanceDto.withError); }); } // ...

Su diferencia con la acción, en el caso de usar el primer método de la documentación:

 @Post ( 'remittance-with-typeorm-transaction' ) @ApiResponse ({ type : RemittanceResultDto, }) async makeRemittanceWithTypeOrmTransaction( @Body () remittanceDto: RemittanceDto) { return await this .connection.transaction( transactionManager => { return this .appService.makeRemittanceWithTypeOrmV1(transactionManager, remittanceDto.userIdFrom, remittanceDto.userIdTo, remittanceDto.sum, remittanceDto.withError); }); }

Es que podemos utilizar los métodos habituales de servicios sin crear variaciones específicas para transacciones en las que es necesario pasar

 transactionManager
. Y también - que antes de usar nuestro método de negocio de servicios, llamamos al
 withTransaction
método en el mismo servicio, pasando nuestro
 transactionManager
lo.

Aquí puede hacer la pregunta: ¿de dónde vino este método?

Por eso:

 @Injectable () export class AppService extends TransactionFor<AppService> /* <-- step 1 */ { constructor ( @InjectRepository (User) private readonly userRepository: Repository<User>, @InjectRepository (Purse) private readonly purseRepository: Repository<Purse>, private readonly appServiceV2: AppServiceV2, moduleRef: ModuleRef, // <-- step 2 ) { super (moduleRef); } // ... }

Y aquí está el código de solicitud:

 START TRANSACTION // ... SELECT "Purse" . "id" AS "Purse_id" , "Purse" . "balance" AS "Purse_balance" , "Purse" . "userId" AS "Purse_userId" FROM "purse" "Purse" WHERE "Purse" . "id" IN ($ 1 ) UPDATE "purse" SET "balance" = $ 2 WHERE "id" IN ($ 1 ) SELECT "Purse" . "id" AS "Purse_id" , "Purse" . "balance" AS "Purse_balance" , "Purse" . "userId" AS "Purse_userId" FROM "purse" "Purse" WHERE "Purse" . "id" IN ($ 1 ) UPDATE "purse" SET "balance" = $ 2 WHERE "id" IN ($ 1 ) COMMIT

Y con el error:

 START TRANSACTION // ... SELECT "Purse" . "id" AS "Purse_id" , "Purse" . "balance" AS "Purse_balance" , "Purse" . "userId" AS "Purse_userId" FROM "purse" "Purse" WHERE "Purse" . "id" IN ($ 1 ) UPDATE "purse" SET "balance" = $ 2 WHERE "id" IN ($ 1 ) ROLLBACK

Pero ya lo viste al principio.

Para hacer que esta magia funcione, debe completar dos pasos:

  • Nuestro servicio debe heredar de la clase.
     TransactionFor <ServiceType>
  • Nuestro servicio debe tener una clase especial
     moduleRef: ModuleRef
    en la lista de dependencias del constructor

Es todo. Por cierto, dado que la inyección de dependencia por parte del marco en sí no ha ido a ninguna parte, no tiene que lanzar explícitamente

 moduleRef
. Solo para pruebas.

Quizás esté pensando: ¿Por qué debo heredar de esta clase? ¿Qué pasa si mi servicio tendrá que heredar de algún otro? Si pensó, le sugiero que calcule cuántos de sus servicios se heredan de otras clases y se usan en transacciones.

Ahora, ¿cómo funciona? el aparecido

 withTransaction
método: recrea su servicio para esta transacción, así como todas las dependencias de su servicio y las dependencias de las dependencias: todo, todo, todo. De ello se deduce que si de alguna manera almacena algún estado en sus servicios ( pero, ¿y si? ), entonces no estará allí al crear una transacción de esta manera. La instancia original de su servicio aún existe y cuando la llame, todo volverá a ser como antes.

Además del ejemplo anterior, también agregué un método codicioso: transferencia con comisión, que usa dos servicios a la vez en una acción del controlador:

 // ... @Post ( 'remittance-with-transaction-and-fee' ) @ApiResponse ({ type : RemittanceResultDto, }) async makeRemittanceWithTransactionAndFee( @Body () remittanceDto: RemittanceDto) { return this .connection.transaction( async manager => { const transactionAppService = this .appService.withTransaction(manager); // <-- this is interesting new thing const result = await transactionAppService.makeRemittance(remittanceDto.userIdFrom, remittanceDto.userIdTo, remittanceDto.sum, remittanceDto.withError); result.fromBalance -= 1 ; // <-- transfer fee const senderPurse = await transactionAppService.getPurse(remittanceDto.userIdFrom); senderPurse.balance -= 1 ; // <-- transfer fee, for example of using several services in one transaction in controller await this .appServiceV2.withTransaction(manager).savePurse(senderPurse); return result; }); } // ...

Este método realiza las siguientes solicitudes:

 START TRANSACTION // ... SELECT "Purse" . "id" AS "Purse_id" , "Purse" . "balance" AS "Purse_balance" , "Purse" . "userId" AS "Purse_userId" FROM "purse" "Purse" WHERE "Purse" . "id" IN ($ 1 ) UPDATE "purse" SET "balance" = $ 2 WHERE "id" IN ($ 1 ) SELECT "Purse" . "id" AS "Purse_id" , "Purse" . "balance" AS "Purse_balance" , "Purse" . "userId" AS "Purse_userId" FROM "purse" "Purse" WHERE "Purse" . "id" IN ($ 1 ) UPDATE "purse" SET "balance" = $ 2 WHERE "id" IN ($ 1 ) // this is new requests for fee: SELECT "Purse" . "id" AS "Purse_id" , "Purse" . "balance" AS "Purse_balance" , "Purse" . "userId" AS "Purse_userId" FROM "purse" "Purse" WHERE "Purse" . "userId" = $ 1 LIMIT 1 SELECT "Purse" . "id" AS "Purse_id" , "Purse" . "balance" AS "Purse_balance" , "Purse" . "userId" AS "Purse_userId" FROM "purse" "Purse" WHERE "Purse" . "id" IN ($ 1 ) UPDATE "purse" SET "balance" = $ 2 WHERE "id" IN ($ 1 ) COMMIT

De lo cual podemos ver que todas las solicitudes aún ocurren en una transacción y funcionará correctamente.

En resumen, me gustaría decir: al usar este paquete en varios proyectos reales, obtuve una forma mucho más conveniente de escribir transacciones, por supuesto, dentro de la pila Nest.js + TypeORM. Espero que lo encuentres útil también. Si te gusta este paquete y decides probarlo, un pequeño deseo: dale un asterisco en GitHub . No es difícil para ti, pero es útil para mí y para este paquete. También estaré encantado de escuchar críticas constructivas y posibles formas de mejorar esta solución.

Publicado anteriormente aquí .