Spring Transaction Management not working with Spring Boot + MyBatis?
Asked Answered
I

4

7

I am trying to get Spring Transaction Management working in my new Spring Boot + MyBatis application.

So far I have managed to get everything working with minimal issues - it's just getting the @Transactional annotation to function correctly. Currently, all statements are committed immediately regardless of whether the method is annotated or not.

Spring Boot does so much of the boilerplate configuration for you that it's difficult to find the missing link.

My build.gradle contains the following dependencies:

compile("org.springframework.boot:spring-boot-starter-amqp")
compile("org.mybatis.spring.boot:mybatis-spring-boot-starter:1.0.0")
compile("mysql:mysql-connector-java:5.1.38")

My application.properties contains the following datasource configuration:

spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/my_db
spring.datasource.username=my_user
spring.datasource.password=my_pass

A simple example of a method in a bean that is not acting as expected is as follows:

@Transactional
public void performTransactionTest() throws Exception {

    Person person = new Person();
    person.setPersonId(123);
    personMapper.insert(person);

    throw new Exception("This should force a rollback!");

}

The Exception gets thrown but the record has already been inserted.

There is basically no documentation currently in existence on transaction configuration for Spring Boot AND MyBatis together but as far as I understand, it should mostly wire itself up as would be done manually in a Spring + MyBatis application and where it doesn't - we are able to configure it further. With that said I have tried the following configurations in my applicationContext.xml with no luck:

<tx:annotation-driven proxy-target-class="true" transaction-manager="transactionManager" />

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource" />
</bean>

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="dataSource" />
</bean>

I can confirm that even without any of the above configurations a DataSourceTransactionManager is configured with the same DataSource that the MyBatis mappers' SqlSession uses.

Any help or ideas that could push me in the right direction would be greatly appreciated. If you require any further information I am happy to provide it!

Thanks in advance!

Xandel

Introgression answered 18/5, 2016 at 21:40 Comment(6)
Annotating private methods doesn't do any good. see https://mcmap.net/q/1623988/-how-to-set-up-transaction-with-mybatis-and-spring/217324Ruffo
Have edited the question - I can confirm that the issue still exists with a public method.Introgression
also checked exceptions don't cause a rollback by default.Ruffo
Can you post the insert method code too? do you use sqlSession to insert or simply using @Insert mapper annotations? If you are using sqlSession please post the relevant code on how you create the object. The same did work for me when i started working on mybatis. I used @Transactional on method level only.Will
I am using mybator generated mappers which creates mapper interfaces (no annotations) and a matching sql mapper file containing the insert / update sql queries. The SqlSession is wired automatically by Spring.Introgression
Just a comment on the answers I'm getting that are related to the rollback example - perhaps it was a bad example but that was just the small working example I provided to illustrate the problem. The actual issue was that records were committed immediately (rather than after the method completes) regardless of a rollback.Introgression
I
4

So I got it working by annotating the class definition with @Transactional instead of the method definition.

I am not sure if this is common practice. The Spring Boot Transaction Management documentation doesn't do it like that here but the Mybatis Spring sample does do it that way in their documentation here...

If anyone has further information that could explain this I will happily mark that answer as the correct one.

For now however, my problem is solved.

EDIT

Returning to this problem month's later I have finally gotten to the bottom of it. There were 2 main issues here.

  1. As Kazuki correctly mentioned, you need to explicitly declare that rollbacks need to happen for checked exceptions using the @Transactional(rollbackFor = Exception.class) annotation.

  2. "Transaction boundaries are only created when properly annotated methods are called through a Spring proxy. This means that you need to call your annotated method directly through an @Autowired bean or the transaction will never start." (reference to this source below)

In my sample code I was calling this.performTransactionTest() from the same class. In this way the transaction will be ignored. If I instead call it via a wired reference to my class such as myAutoWiredBean.performTransactionTest() everything works as expected. This also explains why it appeared only the class level annotation was working, but that's because any method called would have been referenced by a wired bean.

Two articles which were MAJOR assistance to me in understanding the finer details of Spring's transaction management are here. A big thanks to the authors Nitin Prabhu and Tim Mattison.

https://dzone.com/articles/spring-transaction-management

http://blog.timmattison.com/archives/2012/04/19/tips-for-debugging-springs-transactional-annotation/

I hope this helps somebody!

Introgression answered 18/5, 2016 at 22:1 Comment(0)
G
2

A default behavior of Spring Transaction Management is to commit when a checked exception occurred. If you want to rollback a transaction, you can throw an unchecked exception(RuntimeException). Also @Transactional(rollbackFor = Exception.class) gives the same result.

Please try this.

For details see https://docs.spring.io/spring/docs/current/spring-framework-reference/data-access.html#transaction-declarative-rolling-back

Thanks.

Giddy answered 10/4, 2017 at 16:37 Comment(0)
A
0

The root cause is missing transaction manager for cases we are explicitly creating data source rather and not relying on spring boot to create automatically based on parameters from application.yml. Also it is not needed to annotate the class instead annotate the method like below and initialize the transaction manager bean during startup.

@Transactional(propagation = Propagation.REQUIRED, transactionManager = "transactionManager", rollbackFor = CustomExcp.class)
public int updt(Emp vo) throws CustomExcp {
...
}

@Bean
    public DataSourceTransactionManager transactionManager() {
        DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
        transactionManager.setDataSource(dataSource);
        log.info("DataSource Transaction Manager");
        return transactionManager;
    }
Azeotrope answered 10/5, 2018 at 8:29 Comment(0)
E
0

You probably have two data sources using MapperScan that might be messing up a mybatis config. You need to add SqlSessionFactory and SqlSessionTemplate as mentioned here http://mybatis.org/spring/getting-started.html.

import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

import javax.sql.DataSource;

@Configuration
@Slf4j
@MapperScan(value = "com.abc.xyx.aaa", sqlSessionTemplateRef = "PrimarySessionTemplate")
public class MyBatisPrimaryConfig {

    @Bean(name = "PrimarySessionFactory")
    @Primary
    public SqlSessionFactory sessionFactory(@Qualifier("PrimaryDataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        return sqlSessionFactoryBean.getObject();
    }

    @Bean(name = "PrimarySessionTemplate")
    @Primary
    public SqlSessionTemplate primarySessionTemplate(@Qualifier("PrimarySessionFactory") SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }
}
Esker answered 21/2, 2022 at 10:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.