2

I need some help trying to debug why the transaction management of my spring boot app is not working.

The basic idea is that I have 2 tables I would like to write something in. When anything goes wrong in one of the 2 tables, the transaction should be rolled back and nothing should be written to the database.

Here is a simplified version of the code:

@Transactional
public void archiveTask(String taskId) {

    OffloadedRun run = new OffloadedRun();
    run.setStartDateTime(LocalDateTime.now());
    calculationRunRepository.save(run);

    List<SingleContractCalculationResults> activeResults = contractCalculationResultAccessService.get(taskId);

    for (SingleContractCalculationResults result : example) {
        for (Map.Entry<String, ContractResults> entry : result.getResultsPerScenario().entrySet()) {
            String scenario = entry.getKey();
            ContractResults results = entry.getValue();

            OffloadedCalculationResult offloadedCalculationResult = new OffloadedCalculationResult();

//                offloadedCalculationResult.setOffloadedRun(run);
            offloadedCalculationResult.setContractId(result.getContractId());
            calculationResultRepository.save(offloadedCalculationResult);
        }
    }
}

The classes that I execute the save methods on are Spring Data JPA repositories that are defined like this:

public interface CalculationRunRepository extends JpaRepository<OffloadedRun, String> {
}

the line I commented out is a mandatory column. I do this to enforce a ConstraintViolationException to test what happens on an exception when saving something in the second table.

What happens is that the first entity is saved successfully, which should not have happened. I'm trying to figure out why this is.

My spring boot application is configured with @EnableTransactionManagement to enable the @Transactional annotations in my own services (like this one).

I changed the logging level for org.springframework.transaction.interceptor to TRACE to see what's going on:

o.s.t.i.TransactionInterceptor           : Getting transaction for [be.sodemo.calculator.offloading.TaskArchiverImpl.archiveTask]
o.s.t.i.TransactionInterceptor           : Getting transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]
o.s.t.i.TransactionInterceptor           : Completing transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]
o.s.t.i.TransactionInterceptor           : Getting transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]
o.s.t.i.TransactionInterceptor           : Completing transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]
o.s.t.i.TransactionInterceptor           : Getting transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]
o.s.t.i.TransactionInterceptor           : Completing transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]
o.s.t.i.TransactionInterceptor           : Getting transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]
o.s.t.i.TransactionInterceptor           : Completing transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]
o.s.t.i.TransactionInterceptor           : Getting transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]
o.s.t.i.TransactionInterceptor           : Completing transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]
o.s.t.i.TransactionInterceptor           : Completing transaction for [be.sodemo.calculator.offloading.TaskArchiverImpl.archiveTask]

o.h.e.j.s.SqlExceptionHelper             : SQL Error: 1048, SQLState: 23000
o.h.e.j.s.SqlExceptionHelper             : Column 'run_id' cannot be null
o.h.e.j.b.i.AbstractBatchImpl            : HHH000010: On release of batch it still contained JDBC statements
o.a.c.c.C.[.[.[.[dispatcherServlet]      : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.dao.DataIntegrityViolationException: could not execute statement; SQL [n/a]; constraint [null]; nested exception is org.hibernate.exception.ConstraintViolationException: could not execute statement] with root cause

com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: Column 'run_id' cannot be null
    at sun.reflect.GeneratedConstructorAccessor2599.newInstance(Unknown Source) ~[?:?]
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java) ~[?:1.8.0_102]
    at java.lang.reflect.Constructor.newInstance(Constructor.java:423) ~[?:1.8.0_102]
    at com.mysql.jdbc.Util.handleNewInstance(Util.java:425) ~[mysql-connector-java-5.1.40.jar:5.1.40]

After this

I'm not sure what the logging would look like if transaction management works properly but it looks like it's completing every transaction.

Does anyone have an idea what I can try next to see what's going wrong?

Edit: Until now I have been using a MySQL database. I noticed that when I tested this exact same code on a H2 database, it seems like the rollback works as intended. The only difference I see with that is that it throws another vendor-specific exception.

I've tried explicitly setting the rollbackFor attribute on the @Transactional annotation like so:

@Transactional(rollbackFor = {Exception.class, MySQLIntegrityConstraintViolationException.class})

But even that didn't cause a rollback.

Edit:

These are my spring boot settings related to JPA/Hibernate:

spring:
  jpa:
    hibernate:
      ddl-auto: none
      dialect: org.hibernate.dialect.MySQL5Dialect
    database: mysql
    properties:
      hibernate:
        order_inserts: true
        jdbc:
          batch_size: 50
  datasource:
    url: jdbc:mysql://localhost/local-test-db
    driver-class-name: com.mysql.jdbc.Driver
Geoffrey De Vylder
  • 3,963
  • 7
  • 36
  • 56
  • 2
    Could you please check the import for the `@Transactional` annotation? Make sure it's not imported from the javax packages but from the spring ones. – Arnold Galovics Apr 11 '17 at 10:38
  • It's the spring version of the annotation: org.springframework.transaction.annotation.Transactional – Geoffrey De Vylder Apr 11 '17 at 10:40
  • Could you please include the method `calculationRunRepository.save(run)`? It might be that you have different transaction propagation here. – Arnold Galovics Apr 11 '17 at 10:45
  • I use spring data for my repositories. I added a small example :) – Geoffrey De Vylder Apr 11 '17 at 10:48
  • Show your hibernate configuration...Also when using MySQL make sure you are actually using table types that support transactions... If you use a non InnoDB dialect and use hibernate to create the schema transactions are useless as MyISAM doesn't support transactions. – M. Deinum Apr 11 '17 at 14:24
  • My tables are all using the InnoDB engine. I'll add my hibernate configuration in the question. – Geoffrey De Vylder Apr 12 '17 at 08:14
  • Why did you add `@EnableTransactionManagement` Spring Boot already does that for you... Did you define your own `TransactionManager` as well? If so post the configuration... Also there is no `spring.jpa.hibernate.dialect` property, you should use `spring.jpa.hibernate.database-platform` instead and remove `spring.jpa.hibernate.database`. – M. Deinum Apr 12 '17 at 08:49
  • Thanks for pointing that out, I find mixed answers regarding the @EnableTransactionManagement being needed or not so I was not sure. I removed / changed the incorrect properties. The issue is still present. – Geoffrey De Vylder Apr 12 '17 at 17:44

3 Answers3

1

Please verify whether you are using single dataSource or multiple dataSources in your application.

If you are using single dataSource then @Transactional will pick that single dataSource by default else it will pick any one from multiple dataSources and this type of untraceable issue occurs.

Please have a look at @Transaction annotation with different data sources

I have resolved the issue using

/* This code is present in @Configuration class */
@Bean(name = "postgresDataSource")
DataSource postgresDataSource(){
    // DataSource configuration code
}

@Bean(name = "postgresTransactionManager")
public PlatformTransactionManager transactionManager(@Qualifier("postgresDataSource") DataSource dataSource) {
    return new DataSourceTransactionManager(dataSource);
}

/* This code is present in @Service class */
@Override
@Transactional("postgresTransactionManager")
public void save(Map queueMsg) {
    // Transaction specific code 
}

Ziaullhaq Savanur
  • 1,848
  • 2
  • 17
  • 20
0

I am not sure why you are using String for your id type in repository <OffloadedRun, String>. Your OffloadedRun domain has id with String type? It should match with type of your id field in OffloadedRun domain. Make sure for the case. To confirm, Could you please post your OffloadedRun domain code also?

Infinite
  • 146
  • 6
0

You have to use org.springframework.orm.hibernate4.HibernateTransactionManager.

You might be using org.springframework.orm.jpa.JpaTransactionManager.

Ankit Gupta
  • 2,529
  • 2
  • 15
  • 26