Spring Bootとmybatisでトランザクションタイムアウトが効かない



## トランザクションコミット前のチェック
* DataSourceTransactionManagerは、トランザクションのコミット前にトランザクションタイムアウトのチェックをしない。
* JtaTransactionManagerは、トランザクションのコミット前にチェックを実施し、指定した時間が経過していた場合には、TransactionTimedOutExceptionが発生する。(実装はAtomikosで確認)

## SQL実行前のチェック
* JdbcTemplateは、SQL実行前にトランザクションタイムアウトチェックを実施し、指定した時間が経過していた場合にはTransactionTimedOutExceptionが発生する。また、トランザクションタイムアウトまでの残り時間をStatement#setQueryTimeoutに指定する。
* mybatisは、トランザクションタイムアウトのチェックは実施しない。


上記をまとめると以下のようになる。

| TxManager                   | SQL         |  コミット前チェック | SQL実行前チェック | 
|-----------------------------|-------------|------------------|-----------------|
| JtaTransactionManager       |JdbcTemplate | ○                | ○               |
| JtaTransactionManager       |mybatis      | ○                | ×               |   
| DataSourceTransactionManager|JdbcTemplate | ×                | ○               |
| DataSourceTransactionManager|mybatis      | ×                | ×               |

つまりDataSourceTransactionManagerとmybatisの組み合わせでは、トランザクションタイムアウトの設定が無視されることになる。

## JTAを利用せずにタイムアウトチェックを実施するには

以下のようにDataSourceTransactionManager#doCommitを拡張すれば、コミット前に必ずトランザクションのチェックが実行される。

```
@Configuration
public class TransactionConfiguration {

  @Bean
  @Autowired
  public DataSourceTransactionManager transactionManager(
      DataSource dataSource) {
    DataSourceTransactionManager transactionManager = 
    							new TimeoutAwareTransactionManager();
    transactionManager.setDefaultTimeout(10000); //タイムアウト値設定    
    transactionManager.setDataSource(dataSource);
    transactionManager.setRollbackOnCommitFailure(true);
    return transactionManager;
  }

  public static class TimeoutAwareTransactionManager 
  					extends DataSourceTransactionManager {

    private static final long serialVersionUID = 1L;

    @Override
    protected void doCommit(DefaultTransactionStatus status) {
      JdbcTransactionObjectSupport txObject =
          (JdbcTransactionObjectSupport) status.getTransaction();

      ConnectionHolder holder = txObject.getConnectionHolder();

      if (holder.hasTimeout()) {
        // トランザクションタイムアウトしていたらTransactionTimedOutExceptionがスローされる。
        holder.getTimeToLiveInMillis();
      }
      super.doCommit(status);
    }

  }

}
```

### 暗黙コミットについて
余談だがAbstractPlatformTransactionManager#rollbackOnCommitFailureはdefaultがfalseになっているため、
明示的にtrueにしないと、JDBC Driverの実装によっては、Connection#rollbackを実行しない限り暗黙コミットが走ってしまう。
(少なくともOracle JDBC Driverを利用した場合はConnection#setAutoCommit(false)が実行されているとclose時に暗黙コミットされる）
また、Connectionプールを用いても、DataSourceTransactionManager#doCleanupAfterCompletionがConnection#setAutoCommit(true)を実行しておりこのタイミングで暗黙コミットがかかる。

### 任意のタイミングでチェックする
任意の@Repositoryや@ServiceなどBeanの呼び出しのタイミングでチェックを実施するには以下のようなAdviceを作って適用してあげればよい。


```
@Aspect
@Component
@Slf4j
public class TransactionTimeoutAdvice {

  @Autowired
  private DataSource dataSource;

  @Before("適用先のpointcut")
  public void checkTimeout() {
    ConnectionHolder holder =
        (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
    // タイムアウトしているとTransactionTimedOutExceptionが発生
    if (holder != null && holder.hasTimeout()) {
      long ttl = holder.getTimeToLiveInMillis();
      log.debug("transaction will be timed out after " + ttl + " msec.");
    }
  }

}
```