Aliheym / typeorm-transactional

A Transactional Method Decorator for TypeORM that uses Async Local Storage or cls-hooked to handle and propagate transactions between different repositories and service methods.
MIT License
201 stars 27 forks source link

Transaction not rolling back as expected #46

Open sayinmehmet47 opened 8 months ago

sayinmehmet47 commented 8 months ago

I have a method decorated with @Transactional() where I'm trying to delete a non-existing record to force a transaction rollback. However, even though I see a log message indicating that the transaction has been rolled back, the changes made earlier in the transaction are not being rolled back in the database.

image image

Expected behavior I expect that when an error is thrown in a transaction, all changes made in that transaction are rolled back.

To Reproduce Here's a simplified version of my code:

@Transactional()
async updateTask({ id }: Task): Promise<TaskEntity> {
  // ... some code to update a task ...

  const taskSaved = await this.taskRepository.save(updatedTask);

  runOnTransactionRollback(() => {
    this.logger.log('Transaction rolled back');
  });

  // delete a task that does not exist to test transaction rollback
  await this.taskRepository.delete({ id: 'non-existing-id' });

  return taskSaved;
}

In this code, I'm updating a task and then trying to delete a non-existing task. When the delete operation doesn't find a task to delete, it should throw an error and the transaction should roll back, undoing the previous save operation. However, the save operation is not being rolled back.

Aliheym commented 8 months ago

Could you please clarify how do you create taskRepository?

sayinmehmet47 commented 8 months ago

@Aliheym The taskRepository is injected into the service through the constructor, as per standard NestJS practices. Here's an example of how it's done:

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { TaskEntity } from './task.entity';

@Injectable()
export class TaskService {
  constructor(
    @InjectRepository(TaskEntity)
    private taskRepository: Repository<TaskEntity>,
  ) {}
}
Aliheym commented 8 months ago

@sayinmehmet47 I made a test case with the same code as yours and it worked fine.

What versions of typeorm and typeorm-transactional are you using?

Also, if you have a chance to create a minimal example with this behaviour, it would help a lot.

sayinmehmet47 commented 8 months ago

Thank you for your patience, @Aliheym. I believe the confusion arose from the difference in the way the DataSource is initialized and added to the transactional context in the general example versus the NestJS specific example in the documentation.

In the general example, the DataSource is created and then immediately initialized and added to the transactional context:

const dataSource = new DataSource({
  type: 'postgres',
  host: 'localhost',
  port: 5435,
  username: 'postgres',
  password: 'postgres'
});

initializeTransactionalContext({ storageDriver: StorageDriver.AUTO });
addTransactionalDataSource(dataSource);

However, in the NestJS example, the DataSource is created and added to the transactional context within the dataSourceFactory function in the TypeOrmModule.forRootAsync method

TypeOrmModule.forRootAsync({
  useFactory() {
    return {
      type: 'postgres',
      host: 'localhost',
      port: 5435,
      username: 'postgres',
      password: 'postgres',
      synchronize: true,
      logging: false,
    };
  },
  async dataSourceFactory(options) {
    if (!options) {
      throw new Error('Invalid options passed');
    }

    return addTransactionalDataSource(new DataSource(options));
  },
})

This difference led me to initially try to initialize and add the DataSource to the transactional context directly after creating it, as shown in the general example. However, this approach did not work in my NestJS application. It was only after I followed the NestJS specific example and moved the initialization and addition of the DataSource to the transactional context into the dataSourceFactory function that my application worked as expected.

I hope this clarifies the source of my confusion. I believe it would be helpful if the documentation could highlight this difference more clearly to guide other developers who might encounter the same issue.

Aliheym commented 8 months ago

I hope this clarifies the source of my confusion. I believe it would be helpful if the documentation could highlight this difference more clearly to guide other developers who might encounter the same issue.

Thanks for your detailed explanation. Your point is clear and makes sense to me. I will improve the documentation.

tomcerdeira commented 8 months ago

I am having a similar problem in a plain Node.js project.

On the server.ts file, I initialize the TransactionalConext like this:

const start = async() => {
    await appInstance.setDb()
    await appInstance.setRoutes() 
    await appInstance.printVersion()
    listenForMessagesInSqsQueue;
    initializeTransactionalContext({ storageDriver: StorageDriver.AUTO });
    addTransactionalDataSource(AppDataSource);
    https.createServer(HTTPS_OPTIONS, appInstance.getApp()).listen(PORT);
}

Where "AppDataSource" is imported from a class "ConnectionsManager" and represents the DataSource.

This is the method I created with the annotation to test it:

    @Transactional()
    async createOpportunityMissionWithoutUser(urn: string, jobId: number):Promise<string>{
        const mission = new OpportunityMission();
        mission.userStringIdentifier = urn;
        let savedMission = await this.getRepo().save(mission);

        runOnTransactionRollback(() => {
            this.loggerChild.info('Transaction rolled back');
          });

        throw Error();
     }

The log 'Transaction rolled back' is being printed, but the entity is still being persisted in the DB.

Seeing the DB Logs:

image

2 transactions are being made, one that is committed and one that is rolled back 🧐