Search code examples
javaspring-bootspring-jdbc

Spring JDBC JdbcTemplate inserts ain't committed


I have a problem - I cannot insert data in the table with Spring JDBC.

Don't know is it important, just in case - I have an app that uses Spring/SpringBoot with Spring JPA and other things. The database is Postgresql and everything works fine there.

The problem is, that in the same app I need to connect to another database, which is Firebird. I am trying to insert values to some table in this Firebird database using Spring JDBC and looks like the changes ain't committed.

I've created a simple table for tests:

CREATE TABLE table_master
(
    id       INTEGER NOT NULL,
    name     VARCHAR(200),
    CONSTRAINT t_master_pk PRIMARY KEY (id)
);

And I tried both approaches taken from the Spring documentation, like this:

@SpringBootTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@Slf4j
class JdbcTest {
    private DataSource fbDataSource;

    @Autowired
    private PlatformTransactionManager transactionManager;

    @BeforeEach
    public void setUp() {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl("jdbc:firebirdsql://localhost:3055//opt/fb/db/DB.GDB");
        config.setUsername("user");
        config.setPassword("passwd");
        config.setAutoCommit(false);
        fbDataSource = new HikariDataSource(config);
    }

    private static final String insertQuery = "INSERT INTO table_master(id, name) VALUES(?, ?)";
    private static final String cntQuery = "SELECT COUNT(*) FROM table_master";

    @Test
    public void testFbLoad() throws SQLException {
        JdbcTemplate fbJdbcTemplate = new JdbcTemplate(fbDataSource);
        DefaultTransactionDefinition def = new DefaultTransactionDefinition();
        def.setPropagationBehavior(DefaultTransactionDefinition.PROPAGATION_REQUIRES_NEW);
        def.setIsolationLevel(DefaultTransactionDefinition.ISOLATION_READ_COMMITTED);
        TransactionStatus status = transactionManager.getTransaction(def);

        for (int i = 0; i < 100; i++) {
            if (fbJdbcTemplate.update(insertQuery, i, "Row #" + i) == 0) {
                throw new RuntimeException("Cannot insert row #" + i);
            }
        }
        transactionManager.commit(status);

        Integer cnt = fbJdbcTemplate.queryForObject(cntQuery, Integer.class);
        log.info("Rows in table_master: {}", cnt);
    }

and this:

 @Test
    public void testFbLoad() throws SQLException {
        JdbcTemplate fbJdbcTemplate = new JdbcTemplate(fbDataSource);
        TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
        transactionTemplate.execute(status ->
                {
                    try {
                        for (int i = 0; i < 100; i++) {
                            if (fbJdbcTemplate.update(insertQuery, i, "Row #" + i) == 0) {
                                throw new RuntimeException("Cannot insert row #" + i);
                            }
                        }
                    } catch (Exception e) {
                        log.error("Rolling back: {}", e.getMessage(), e);
                        status.setRollbackOnly();
                    }
                    return null;
                }
        );


        Integer cnt = fbJdbcTemplate.queryForObject(cntQuery, Integer.class);
        log.info("Rows in table_master: {}", cnt);
    }

And in both cases I observe that JdbcTemplate#update calls are executed but on completion I see no data in the table and "Rows in table_master: 0" in the log with no errors.

I have no idea what am I doing wrong. Please help, how should I use Spring JDBC for such a tasks?

Update:

Well, I tried @Transactional and it also gives me a strange result:

    @Test
    @Transactional
    public void testFbLoad() throws SQLException {
        JdbcTemplate fbJdbcTemplate = new JdbcTemplate(fbDataSource);
        TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
        transactionTemplate.execute(status ->
                {
                    try {
                        for (int i = 0; i < 100; i++) {
                            if (fbJdbcTemplate.update(insertQuery, i, "Row #" + i) == 0) {
                                throw new RuntimeException("Cannot insert row #" + i);
                            }
                        }
                        status.setRollbackOnly();
                    } catch (Exception e) {
                        log.error("Rolling back: {}", e.getMessage(), e);
                        status.setRollbackOnly();
                    }
                    return null;
                }
        );


        Integer cnt = fbJdbcTemplate.queryForObject(cntQuery, Integer.class);
        log.info("Rows in table_master: {}", cnt);
    }

In this case all inserts made are rolled back, but I observe "Rows in table_master: 100"

Please help. Thank you!

Update 2:

looks like I found the reason. Here's the log:

2024-11-30T04:00:50.693+02:00 DEBUG 15770 --- [    Test worker] o.s.jdbc.core.JdbcTemplate               : Executing prepared SQL update
2024-11-30T04:00:50.693+02:00 DEBUG 15770 --- [    Test worker] o.s.jdbc.core.JdbcTemplate               : Executing prepared SQL statement [INSERT INTO table_master(id, name) VALUES(?, ?)]
2024-11-30T04:00:50.693+02:00 DEBUG 15770 --- [    Test worker] o.s.jdbc.datasource.DataSourceUtils      : Fetching JDBC Connection from DataSource
2024-11-30T04:00:50.701+02:00 DEBUG 15770 --- [    Test worker] o.f.gds.ng.wire.AbstractWireOperations   : readStatusVector arg:isc_arg_gds int: 0
2024-11-30T04:00:50.711+02:00 DEBUG 15770 --- [    Test worker] o.f.gds.ng.wire.AbstractWireOperations   : readStatusVector arg:isc_arg_gds int: 0
2024-11-30T04:00:50.711+02:00 DEBUG 15770 --- [    Test worker] o.f.gds.ng.wire.AbstractWireOperations   : readStatusVector arg:isc_arg_gds int: 0
2024-11-30T04:00:50.724+02:00 DEBUG 15770 --- [    Test worker] o.f.gds.ng.wire.AbstractWireOperations   : readStatusVector arg:isc_arg_gds int: 0
2024-11-30T04:00:50.726+02:00 DEBUG 15770 --- [    Test worker] o.f.gds.ng.wire.AbstractWireOperations   : readStatusVector arg:isc_arg_gds int: 0
2024-11-30T04:00:50.728+02:00 DEBUG 15770 --- [    Test worker] o.f.jaybird.xca.FBManagedConnection      : End called: Xid[1219176952]
2024-11-30T04:00:50.731+02:00 DEBUG 15770 --- [    Test worker] o.f.gds.ng.wire.AbstractWireOperations   : readStatusVector arg:isc_arg_gds int: 0
2024-11-30T04:00:50.731+02:00 DEBUG 15770 --- [    Test worker] o.f.gds.ng.wire.AbstractWireOperations   : readStatusVector arg:isc_arg_gds int: 0
2024-11-30T04:00:50.731+02:00 DEBUG 15770 --- [    Test worker] com.zaxxer.hikari.pool.ProxyConnection   : HikariPool-1 - Executed rollback on connection org.firebirdsql.jdbc.FBConnection@5f5fe604 due to dirty commit state on close().
2024-11-30T04:00:50.731+02:00 DEBUG 15770 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Initiating transaction commit
2024-11-30T04:00:50.732+02:00 DEBUG 15770 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Committing JPA transaction on EntityManager [SessionImpl(1821322830<open>)]
2024-11-30T04:00:50.732+02:00 DEBUG 15770 --- [    Test worker] o.h.e.t.internal.TransactionImpl         : committing
2024-11-30T04:00:50.733+02:00 DEBUG 15770 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Closing JPA EntityManager [SessionImpl(1821322830<open>)] after transaction

So, JdbcTemplate closes connection after update and Hikari rolls it back then. The question is still the same - what am I doing wrong and how to fix it? Thank you!

Update3:

Just in case I've filed a issue report here, but maybe it's my fault and there's a solution. Please help.


Solution

  • The problem is your test as it doesn't really make sense.

    1. You create a DataSource outside the scope of Spring
    2. You are injecting a PlatformTransactionManager that is tied to a different DataSource.

    Due to this there is no dedicated PlatformTransactionManager for your own embedded DataSource. If you would rewrite your test to the following

    @TestMethodOrder(MethodOrderer.OrderAnnotation.class)
    @Slf4j
    class JdbcTest {
    
        private DataSource fbDataSource;
        private PlatformTransactionManager transactionManager;
    
        @BeforeEach
        public void setUp() {
            HikariConfig config = new HikariConfig();
            config.setJdbcUrl("jdbc:firebirdsql://localhost:3055//opt/fb/db/DB.GDB");
            config.setUsername("user");
            config.setPassword("passwd");
            config.setAutoCommit(false);
            fbDataSource = new HikariDataSource(config);
    
            this.transactionManager = new DataSourceTransactionManager(fbDataSource);
        }
    
        private static final String insertQuery = "INSERT INTO table_master(id, name) VALUES(?, ?)";
        private static final String cntQuery = "SELECT COUNT(*) FROM table_master";
    
        @Test
        public void testFbLoad() throws SQLException {
            JdbcTemplate fbJdbcTemplate = new JdbcTemplate(fbDataSource);
            DefaultTransactionDefinition def = new DefaultTransactionDefinition();
            def.setPropagationBehavior(DefaultTransactionDefinition.PROPAGATION_REQUIRES_NEW);
            def.setIsolationLevel(DefaultTransactionDefinition.ISOLATION_READ_COMMITTED);
            TransactionStatus status = transactionManager.getTransaction(def);
    
            for (int i = 0; i < 100; i++) {
                if (fbJdbcTemplate.update(insertQuery, i, "Row #" + i) == 0) {
                    throw new RuntimeException("Cannot insert row #" + i);
                }
            }
            transactionManager.commit(status);
    
            Integer cnt = fbJdbcTemplate.queryForObject(cntQuery, Integer.class);
            log.info("Rows in table_master: {}", cnt);
        }
    }
    

    This will work as you now have a PlatformTransactionManager for the specific transactional resource, in this case your DataSource.

    If you define an additional DataSource and are not using JTA then you also need an additional transaction manager.

    @Configuration
    public class MyFirebaseConfig {
    
      @Bean
      public DataSource firebaseDataSource() { ... }
    
      @Bean
      public DataSourceTransactionManager firebaseTransactionManager(DataSource firebaseDataSource) {
        return new DataSourceTransactionManager(firebaseDataSource);
      } 
    
      // Other beans like JdbcTemplate etc.
    }
    

    Now if you have a service that requires this you can specify the name of the transaction manager to use in the @Transactional.

    @Service
    public class SomeService {
    
      private final JdbcTemplate jdbc;
    
      public SomeService(@Qualifier("firebaseJdbcTemplate") JdbcTemplate jdbc) {
        this.jdbc=jdbc;
      }
    
      @Transactional("firebaseTransactionManager")
      public void foobar() { ... }
    }
    

    To inject the specific DataSource or JdbcTemplate you can use an @Qualifier.