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

Transaction not rolling back as expected #46

Open
sayinmehmet47 opened this issue Dec 19, 2023 · 6 comments
Open

Transaction not rolling back as expected #46

sayinmehmet47 opened this issue Dec 19, 2023 · 6 comments
Assignees
Labels
documentation Improvements or additions to documentation enhancement New feature or request

Comments

@sayinmehmet47
Copy link

sayinmehmet47 commented Dec 19, 2023

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
Copy link
Owner

Aliheym commented Dec 19, 2023

Could you please clarify how do you create taskRepository?

@sayinmehmet47
Copy link
Author

@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
Copy link
Owner

Aliheym commented Dec 21, 2023

@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
Copy link
Author

sayinmehmet47 commented Dec 21, 2023

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 Aliheym self-assigned this Dec 22, 2023
@Aliheym Aliheym added documentation Improvements or additions to documentation enhancement New feature or request labels Dec 22, 2023
@Aliheym
Copy link
Owner

Aliheym commented Dec 22, 2023

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
Copy link

tomcerdeira commented Dec 28, 2023

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 🧐

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Improvements or additions to documentation enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants