CyclingSir
CyclingSir

Reputation: 162

JPA - Spanning a transaction over multiple JpaRepository method calls

I'm using SpringBoot 2.x with SpringData-JPA accessing the database via a CrudRepository.

Basically, I would like to call the CrudRepository's methods to update or persist the data. In one use case, I would like to delete older entries from the database (for the brevity of this example assume: delete all entries from the table) before I insert a new element. In case persisting the new element fails for any reason, the delete operation shall be rolled back.

However, the main problem seems to be that new transactions are opened for every method called from the CrudRepository. Even though, a transaction was opened by the method from the calling service. I couldn't get the repository methods to use the existing transaction.

Getting transaction for [org.example.jpatrans.ChairUpdaterService.updateChairs]
Getting transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.deleteWithinGivenTransaction]
Completing transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.deleteWithinGivenTransaction]

I've tried using different Propagation. (REQUIRED, SUPPORTED, MANDATORY) on different methods (service/repository) to no avail. Changing the methods @Transactional annoation to @Transactional(propagation = Propagation.NESTED) sounded that this would just do that, but didn't help.

JpaDialect does not support savepoints - check your JPA provider's capabilities

Can I achieve the expected behaviour, not using an EntityManager directly?
I also would like to avoid to having to be using native queries as well. Is there anything I have overlooked?

For demonstration purposes, I've created a very condensed example. The complete example can be found at https://gitlab.com/cyc1ingsir/stackoverlow_jpa_transactions

Here are the main (even more simplified) details:
First I've got a very simple entity defined:

@Entity
@Table(name = "chair")
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Chair {

  // Not auto generating the id is on purpose
  // for later testing with non unique keys
  @Id
  private int id;

  @Column(name = "legs", nullable = false)
  private Integer legs;
}   

The connection to the database is made via the CrudRepository:

@Repository
public interface ChairRepository extends CrudRepository<Chair, Integer> {
}

This is being called from another bean (main methods here are updateChairs and doUpdate):

@Slf4j
@Service
@AllArgsConstructor
@Transactional
public class ChairUpdater {

    ChairRepository repository;

    /*
     * Initialize the data store with some
     * sample data
     */
    public void initializeChairs() {

        repository.deleteAll();
        Chair chair4 = new Chair(1, 4);
        Chair chair3 = new Chair(2, 3);

        repository.save(chair4);
        repository.save(chair3);

    }

    public void addChair(int id, Integer legCount) {
        repository.save(new Chair(id, legCount));
    }

    /*
     * Expected behaviour:
     * when saving a given chair fails ->
     * deleting all other is rolled back
     */
    @Transactional        
    public void updateChairs(int id, Integer legCount) {

        Chair chair = new Chair(id, legCount);
        repository.deleteAll();
        repository.save(chair);
    }    
}

The goal, I want to achieve is demonstrated by these two test cases:

@Slf4j
@RunWith(SpringRunner.class)
@DataJpaTest
@Import(ChairUpdater.class)
public class ChairUpdaterTest {

    private static final int COUNT_AFTER_ROLLBACK = 3;
    @Autowired
    private ChairUpdater updater;

    @Autowired
    private ChairRepository repository;

    @Before
    public void setup() {
        updater.initializeChairs();
    }

    @Test
    public void positiveTest() throws UpdatingException {
        updater.updateChairs(3, 10);
    }

    @Test
    public void testRollingBack() {

        // Trying to update with an invalid element
        // to force rollback
        try {
            updater.updateChairs(3, null);
        } catch (Exception e) {
            LOGGER.info("Rolled back?", e);
        }

        // Adding a valid element after the rollback
        // should succeed
        updater.addChair(4, 10);
        assertEquals(COUNT_AFTER_ROLLBACK, repository.findAll().spliterator().getExactSizeIfKnown());
    }
}

Update:

It seems to work, if the repository is not extended from either CrudRepository or JpaRepository but from a plain Repository, definening all needed methods explicitly. For me, that seems to be a workaround rather than beeing a propper solution.
The question it boils down to seems to be: Is it possible to prevent SimpleJpaRepository from opening new transactions for every (predefined) method used from the repository interface? Or, if that is not possible, how to "force" the transaction manager to reuse the transaction, opened in the service to make a complete rollback possible?

Upvotes: 4

Views: 4695

Answers (2)

Maurice
Maurice

Reputation: 7371

Yes this is possible. First alter the @Transactional annotation so that it includes rollBackFor = Exception.class.

/*
 * Expected behaviour:
 * when saving a given chair fails ->
 * deleting all other is rolled back
 */
@Transactional(rollbackFor = Exception.class)        
public void updateChairs(int id, Integer legCount) {

    Chair chair = new Chair(id, legCount);
    repository.deleteAll();
    repository.save(chair);
}  

This will cause the transaction to roll back for any exception and not just RuntimeException or Error.

Next you must add enableDefaultTransactions = false to @EnableJpaRepositories and put the annotation on one of your configuration classes if you hadn't already done so.

@Configuration
@EnableJpaRepositories(enableDefaultTransactions = false)
public class MyConfig{

}

This will cause all inherited jpa methods to stop creating a transaction by default whenever they're called. If you want custom jpa methods that you've defined yourself to also use the transaction of the calling service method, then you must make sure that you didn't annotate any of these custom methods with @Transactional. Because that would prompt them to start their own transactions as well.

Once you've done this all of the repository methods should be executed using the service method transaction only. You can test this by creating and using a custom update method that is annotated with @Modifying. For more on testing please see my answer in this SO thread. Spring opens a new transaction for each JpaRepository method that is called within an @Transactional annotated method

Upvotes: 1

Jacob
Jacob

Reputation: 11

Hi I found this documentation that looks will help you:

https://www.logicbig.com/tutorials/spring-framework/spring-data/transactions.html

Next an example take from the previous web site:

@Configuration
**@ComponentScan
@EnableTransactionManagement**
public class AppConfig {
 ....
}

Then we can use transactions like this:

@Service
public class MyExampleBean{

**@Transactional**

public void saveChanges() {
    **repo.save(..);
    repo.deleteById(..);**
    .....
}

}

Upvotes: 1

Related Questions