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 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". transacción de base de datos Las transacciones en un entorno de base de datos tienen dos propósitos principales: 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. 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 en el que hay dos entidades: proyecto 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 ( ). Pero, ¿cómo los aplicas? esperemos que todos estén de acuerdo con esto 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ó , al que volveremos un poco más adelante. TypeORM 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: makeRemittance(fromId: , toId: , sum: , withError = , transaction = ): <RemittanceResultDto> { fromUser = .userRepository.findOne(fromId, { transaction }); toUser = .userRepository.findOne(toId, { transaction }); (fromUser === ) { (NOT_FOUND_USER_WITH_ID(fromId)); } (toUser === ) { (NOT_FOUND_USER_WITH_ID(toId)); } (fromUser.defaultPurseId === ) { (USER_DOES_NOT_HAVE_PURSE(fromId)); } (toUser.defaultPurseId === ) { (USER_DOES_NOT_HAVE_PURSE(toId)); } fromPurse = .purseRepository.findOne(fromUser.defaultPurseId, { transaction }); toPurse = .purseRepository.findOne(toUser.defaultPurseId, { transaction }); modalSum = .abs(sum); (fromPurse.balance < modalSum) { (NOT_ENOUGH_MONEY(fromId)); } fromPurse.balance -= sum; toPurse.balance += sum; .purseRepository.save(fromPurse, { transaction }); (withError) { ( ); } .purseRepository.save(toPurse, { transaction }); remittance = RemittanceResultDto(); remittance.fromId = fromId; remittance.toId = toId; remittance.fromBalance = fromPurse.balance; remittance.sum = sum; remittance; } // ... async number number number false true Promise const await this const await this if undefined throw new Error if undefined throw new Error if null throw new Error if null throw new Error const await this const await this const Math if throw new Error await this if throw new Error 'Unexpectable error was thrown while remittance' await this const new return // ... ¡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 ( ) realmente ama un marco maravilloso cuando tiene que escribir un backend. este soy yo El nombre de este marco es 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. Nest.js. 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 oficial para escribir transacciones: ejemplo { getConnection } ; getConnection().transaction( transactionalEntityManager => { transactionalEntityManager.save(users); transactionalEntityManager.save(photos); }); import from 'typeorm' await async await await // ... Segunda forma de crear transacciones a partir de la documentación: () save(user: User, () transactionManager: EntityManager) { transactionManager.save(User, user); } @Transaction @TransactionManager return En general, el punto de este enfoque es el siguiente: necesita obtener un - 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. transactionEntityManager: EntityManager 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 en ellos. transactionEntityManager Al mismo tiempo, debe tenerse en cuenta que si decide utilizar el enfoque a través de decoradores, cuando transfiera el de un servicio al segundo, y el método del segundo servicio también estará decorado; en el segundo método, recibirá el que no se pasa como una dependencia, y la que crea el decorador, lo que significa dos transacciones diferentes, lo que significa usuarios desafortunados. transactionEntityManager transactionEntityManager Empezar con ejemplos A continuación se muestra el código para una acción de controlador que maneja las solicitudes de los usuarios: ( ) ({ : RemittanceResultDto, }) makeRemittanceWithTypeOrmTransaction( () remittanceDto: RemittanceDto) { .connection.transaction( { .appService.makeRemittanceWithTypeOrmV1(transactionManager, remittanceDto.userIdFrom, remittanceDto.userIdTo, remittanceDto.sum, remittanceDto.withError); }); } // ... @Post 'remittance-with-typeorm-transaction' @ApiResponse type async @Body return await this => transactionManager return this // ... En él, necesitamos tener acceso a la objeto de crear un . Podríamos hacer lo que aconseja la documentación de TypeORM, y simplemente usar el función como se muestra arriba: connection transactionManager getConnection { getConnection } ; ( ) ({ : RemittanceResultDto, }) makeRemittanceWithTypeOrmTransaction( () remittanceDto: RemittanceDto) { getConnection().transaction( { .appService.makeRemittanceWithTypeOrmV1(transactionManager, remittanceDto.userIdFrom, remittanceDto.userIdTo, remittanceDto.sum, remittanceDto.withError); }); } import from 'typeorm' // ... @Post 'remittance-with-typeorm-transaction' @ApiResponse type async @Body return await => transactionManager return this // ... Pero me parece que dicho código será más difícil de probar, y esto es simplemente incorrecto ( ). Por lo tanto, tendremos que pasar el 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: gran argumento connection () ( ) AppController { ( ) { } } @Controller @ApiTags 'app' export class constructor readonly appService: AppService, readonly connection: Connection, private private // <-- 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 class en el controlador/constructor de servicios, por ahora solo recordamos esto. connection Ahora echemos un vistazo a la método de nuestro : makeRemittanceWithTypeOrmV1 appService makeRemittanceWithTypeOrmV1(transactionEntityManager: EntityManager, fromId: , toId: , sum: , withError = ) { fromUser = transactionEntityManager.findOne(User, fromId); toUser = transactionEntityManager.findOne(User, toId); (fromUser === ) { (NOT_FOUND_USER_WITH_ID(fromId)); } (toUser === ) { (NOT_FOUND_USER_WITH_ID(toId)); } (fromUser.defaultPurseId === ) { (USER_DOES_NOT_HAVE_PURSE(fromId)); } (toUser.defaultPurseId === ) { (USER_DOES_NOT_HAVE_PURSE(toId)); } fromPurse = transactionEntityManager.findOne(Purse, fromUser.defaultPurseId); toPurse = transactionEntityManager.findOne(Purse, toUser.defaultPurseId); modalSum = .abs(sum); (fromPurse.balance < modalSum) { (NOT_ENOUGH_MONEY(fromId)); } fromPurse.balance -= sum; toPurse.balance += sum; .appServiceV2.savePurse(fromPurse); (withError) { ( ); } transactionEntityManager.save(toPurse); remittance = RemittanceResultDto(); remittance.fromId = fromId; remittance.toId = toId; remittance.fromBalance = fromPurse.balance; remittance.sum = sum; remittance; } async number number number false const await // <-- we need to use only provided transactionEntityManager, for make all requests in transaction const await // <-- and there if undefined throw new Error if undefined throw new Error if null throw new Error if null throw new Error const await // <-- there const await // <--there const Math if throw new Error await this // <-- oops, something was wrong if throw new Error 'Unexpectable error was thrown while remittance' await const new return Todo el proyecto es sintético, pero para mostrar lo desagradable de este enfoque, moví el método utilizado para guardar la billetera en un separado , y utilizó este servicio con este método dentro del considerado método. Puedes ver el código de este método y servicio a continuación: savePurse appServiceV2 service makeRemittanceWithTypeOrmV1 () AppServiceV2 { ( ) { } savePurse(purse: Purse) { .purseRepository.save(purse); } } @Injectable export class constructor (Purse) readonly purseRepository: Repository<Purse>, @InjectRepository private async await this // ... 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 no se revierte y, por lo tanto, nuestros usuarios están indignados nuevamente. appServiceV2 // ... . , . , . . ($ ) = $ ($ ) 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 en los métodos correspondientes, entonces la documentación nos aconseja mirar a los decoradores. transactionEntityManager Al aplicarlos, obtenemos este tipo de acción del controlador: ( ) ({ : RemittanceResultDto, }) makeRemittanceWithTypeOrmTransactionDecorators( () remittanceDto: RemittanceDto) { .appService.makeRemittanceWithTypeOrmV2(remittanceDto.userIdFrom, remittanceDto.userIdTo, remittanceDto.sum, remittanceDto.withError); } // ... @Post 'remittance-with-typeorm-transaction-decorators' @ApiResponse type async @Body return this // ... Ahora se ha vuelto más simple - no hay necesidad de usar el 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: . Aquí es donde esos decoradores vienen al rescate: connection transactionEntityManager () makeRemittanceWithTypeOrmV2(fromId: , toId: , sum: , withError: , () transactionEntityManager: EntityManager = ) { fromUser = transactionEntityManager.findOne(User, fromId); toUser = transactionEntityManager.findOne(User, toId); (fromUser === ) { (NOT_FOUND_USER_WITH_ID(fromId)); } (toUser === ) { (NOT_FOUND_USER_WITH_ID(toId)); } (fromUser.defaultPurseId === ) { (USER_DOES_NOT_HAVE_PURSE(fromId)); } (toUser.defaultPurseId === ) { (USER_DOES_NOT_HAVE_PURSE(toId)); } fromPurse = transactionEntityManager.findOne(Purse, fromUser.defaultPurseId); toPurse = transactionEntityManager.findOne(Purse, toUser.defaultPurseId); modalSum = .abs(sum); (fromPurse.balance < modalSum) { (NOT_ENOUGH_MONEY(fromId)); } fromPurse.balance -= sum; toPurse.balance += sum; .appServiceV2.savePurseInTransaction(fromPurse, transactionEntityManager); (withError) { ( ); } transactionEntityManager.save(toPurse); remittance = RemittanceResultDto(); remittance.fromId = fromId; remittance.toId = toId; remittance.fromBalance = fromPurse.balance; remittance.sum = sum; remittance; } // ... @Transaction // <-- this async number number number boolean @TransactionManager null /* <-- and this */ const await const await if undefined throw new Error if undefined throw new Error if null throw new Error if null throw new Error const await const await const Math if throw new Error await this // <-- we will check is it will working if throw new Error 'Unexpectable error was thrown while remittance' await const new return // ... 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. , que se ve así: transactionEntityManager () savePurseInTransaction(purse: Purse, () transactionManager: EntityManager = ) { transactionManager.save(Purse, purse); } // .. @Transaction async @TransactionManager null await // ... 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 ( ), y también nos deshacemos de la necesidad de usar en el constructor de controladores usando nuestro servicio . sí, sí connection 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. , como sucedió con nuestro servicio y su método. Intentemos reemplazar este método con otro: transactionEntityManagers appServiceV2 savePurseInTransaction () makeRemittanceWithTypeOrmV2(fromId: , toId: , sum: , withError: , () transactionEntityManager: EntityManager = ) { .appServiceV2.savePurseInTransactionV2(fromPurse, transactionEntityManager); } savePurseInTransactionV2(purse: Purse, transactionManager: EntityManager) { transactionManager.save(Purse, purse); } // app.service.ts @Transaction async number number number boolean @TransactionManager null // ... await this // ... // app.service-v2.ts // .. async await // .. 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 clase. Así, habiendo recibido la primera opción de la documentación. appService Una Manera Diferente Bueno, parece que todavía tenemos que inyectar esto en los constructores del controlador. Pero la forma propuesta de escribir código con transacciones todavía parece muy engorrosa e inconveniente. connection ¿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 en el constructor, lo especificaremos: connection () ( ) AppController { ( ) { } } @Controller @ApiTags 'app' export class constructor readonly appService: AppService, readonly connection: Connection, private private // <-- use this // ... Acción del controlador: ( ) ({ : RemittanceResultDto, }) makeRemittanceWithTransaction( () remittanceDto: RemittanceDto) { .connection.transaction( { .appService.withTransaction(transactionManager) .makeRemittance(remittanceDto.userIdFrom, remittanceDto.userIdTo, remittanceDto.sum, remittanceDto.withError); }); } // ... @Post 'remittance-with-transaction' @ApiResponse type async @Body return await this => transactionManager return this /* <-- this is interesting new thing*/ // ... Su diferencia con la acción, en el caso de usar el primer método de la documentación: ( ) ({ : RemittanceResultDto, }) makeRemittanceWithTypeOrmTransaction( () remittanceDto: RemittanceDto) { .connection.transaction( { .appService.makeRemittanceWithTypeOrmV1(transactionManager, remittanceDto.userIdFrom, remittanceDto.userIdTo, remittanceDto.sum, remittanceDto.withError); }); } @Post 'remittance-with-typeorm-transaction' @ApiResponse type async @Body return await this => transactionManager return this Es que podemos utilizar los métodos habituales de servicios sin crear variaciones específicas para transacciones en las que es necesario pasar . Y también - que antes de usar nuestro método de negocio de servicios, llamamos al método en el mismo servicio, pasando nuestro lo. transactionManager withTransaction transactionManager Aquí puede hacer la pregunta: ¿de dónde vino este método? Por eso: () AppService TransactionFor<AppService> { ( ) { (moduleRef); } } @Injectable export class extends /* <-- step 1 */ constructor (User) readonly userRepository: Repository<User>, (Purse) readonly purseRepository: Repository<Purse>, readonly appServiceV2: AppServiceV2, moduleRef: ModuleRef, @InjectRepository private @InjectRepository private private // <-- step 2 super // ... 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 en la lista de dependencias del constructor moduleRef: ModuleRef 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: Si pensó, le sugiero que calcule cuántos de sus servicios se heredan de otras clases y se usan en transacciones. ¿Por qué debo heredar de esta clase? ¿Qué pasa si mi servicio tendrá que heredar de algún otro? Ahora, ¿cómo funciona? el aparecido 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 ( ), 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. withTransaction pero, ¿y si? 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: ( ) ({ : RemittanceResultDto, }) makeRemittanceWithTransactionAndFee( () remittanceDto: RemittanceDto) { .connection.transaction( manager => { transactionAppService = .appService.withTransaction(manager); result = transactionAppService.makeRemittance(remittanceDto.userIdFrom, remittanceDto.userIdTo, remittanceDto.sum, remittanceDto.withError); result.fromBalance -= ; senderPurse = transactionAppService.getPurse(remittanceDto.userIdFrom); senderPurse.balance -= ; .appServiceV2.withTransaction(manager).savePurse(senderPurse); result; }); } // ... @Post 'remittance-with-transaction-and-fee' @ApiResponse type async @Body return this async const this // <-- this is interesting new thing const await 1 // <-- transfer fee const await 1 // <-- transfer fee, for example of using several services in one transaction in controller await this return // ... Este método realiza las siguientes solicitudes: // ... . , . , . . ($ ) = $ ($ ) . , . , . . ($ ) = $ ($ ) // this requests fee: . , . , . . = $ . , . , . . ($ ) = $ ($ ) 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 is new for 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 . 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. GitHub Publicado anteriormente aquí .