0

I am trying to set up my web app that uses Spring and MyBatis.

Here are the important snippets of code.

Maven dependencies in pom.xml:

    ...
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-core</artifactId>
        <version>4.3.5.RELEASE</version>
    </dependency>

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-web</artifactId>
        <version>4.3.5.RELEASE</version>
    </dependency>

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
        <version>4.3.5.RELEASE</version>
    </dependency>

    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>postgresql</artifactId>
        <version>9.4.1212.jre7</version>
    </dependency>

    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.4.2</version>
    </dependency>

    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis-spring</artifactId>
        <version>1.3.1</version>
    </dependency>
    ...

And configuration of Spring beans:

@Configuration
@EnableTransactionManagement
public class DatabaseConfiguration {

    @Value("classpath:db/mybatis/mybatis-configuration.xml")
    private Resource myBatisConfiguration;

    @Bean
    public DataSource dataSource() {
        final DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setDriverClassName("org.postgresql.Driver");
        dataSource.setUrl("jdbc:postgresql://127.0.0.1:5432/ehdb");
        dataSource.setUsername(/*my username*/);
        dataSource.setPassword(/*my password*/);
        return dataSource;
    }

    @Bean
    public PlatformTransactionManager transactionManager() {
        final DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(dataSource());
        transactionManager.setValidateExistingTransaction(true);
        return transactionManager;
    }

    @Bean
    public SqlSessionFactoryBean sqlSessionFactoryBean() {
        final SqlSessionFactoryBean sqlSessionFactory = new SqlSessionFactoryBean();
        sqlSessionFactory.setDataSource(dataSource());
        sqlSessionFactory.setConfigLocation(myBatisConfiguration);
        return sqlSessionFactory;
    }

    @Bean
    public SqlSession sqlSession() throws Exception {
        return new SqlSessionTemplate(sqlSessionFactoryBean().getObject());
    }
}

And here is a service that should call some MyBatis statement in a transactional method:

@Service
public class HelloManagerImpl implements HelloManager {
    private final HelloDao helloDao;

    public HelloManagerImpl(@Autowired final HelloDao helloDao) {
        this.helloDao = helloDao;
    }

    @Override
    @Transactional
    public String doSomething() {
        helloDao.insertRow(); // a row is inserted into DB table via MyBatis; bean sqlSession is autowired in HelloDao
        throw new RuntimeException(); // transaction will be rolled back here
    }
}

If I call the method doSomething, it works as expected. No new row appears in the database table as the transaction is rolled back due to the thrown RuntimeException.

If I comment out the throw statement and repeat the experiment, a new row appears in the database table. This is the expected behavior again.

And now, if I additionally comment out the @Transactional annotation and call doSomething(), the method succeeds and a new row is inserted into the table. It seems that MyBatis creates a transaction for the INSERT statement automatically if no transaction exists.

I would prefer failing in the last case. If I forget to write the @Transactional annotation, it is probably a mistake. It would be fine, if an exception is thrown in such case forcing me to fix my code rather than creating some transaction silently.

Is there a way to achive this please?

Thanks for help.

Cimlman
  • 3,404
  • 5
  • 25
  • 35

2 Answers2

1

I would suggest to set all transactions read-only to achieve your last case and only annotate methods that should write in database. For example, your service would look like this :

@Service
@Transactional(readOnly = true)
public class HelloManagerImpl implements HelloManager {
  private final HelloDao helloDao;

  public HelloManagerImpl(@Autowired final HelloDao helloDao) {
    this.helloDao = helloDao;
  }

  @Override
  @Transactional(readOnly = false)
  public String doSomething() {
    helloDao.insertRow(); // a row is inserted into DB table via MyBatis; bean sqlSession is autowired in HelloDao
    throw new RuntimeException(); // transaction will be rolled back here
  }
}
victor gallet
  • 1,819
  • 17
  • 25
  • +1This is not a MyBatis configuration I was looking for originally but it is a reasonable workaround. I am also considering `@Transactional(propagation = Propagation.MANDATORY)` on DAO classes or methods. – Cimlman Jan 27 '17 at 09:57
0

The behaviour of a transaction is managed by Spring itself, and spring roolbacks a transaction in case of RuntimeException.

If you don't provide @Transactional annotation, spring don't consider it as a transaction and will do nothing in case of RuntimeException. So, Make a good practice to put @Transactional annotation above the service methods.

Avinash
  • 4,115
  • 2
  • 22
  • 41
  • Yes, I understand that Spring cannot rollback transaction without the @Transactional annotation. MyBatis (or my own code customizing MyBatis somehow) should throw an exception. If I call MyBatis (e.g. `sqlSession.insert("statementId")`), an exception should be thrown if no transaction is in progress. I am working on another project where Hibernate is used instead of MyBatis and an exception is thrown in an analogous case. – Cimlman Jan 20 '17 at 16:46
  • You are throwing RuntimeException after MyBatis task is completed, So, there is no effect RuntimeException – Avinash Jan 20 '17 at 16:49
  • I know. :) The implementation of `helloDao.insertRow()` just calls `sqlSession.insert("myStatementId");`. The method `insert` should throw an exception. The exception should be throw by MyBatis before any SQL statement is sent to database. I am looking for something like `MyBatisConfiguration.setThrowExceptionWheneverAnyMethodOnSqlSessionIsCalledWithoutPreceedingTransactionalAnnotation(true);`. – Cimlman Jan 20 '17 at 17:45