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:
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:
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 ).
¿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.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.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:
TransactionFor <ServiceType>
moduleRef: ModuleRef
en la lista de dependencias del constructorEs 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í .