3rdパーティ製のDBアクセスライブラリをSpringのトランザクション管理下に参加させる方法

先ほど「[SpringのDBコネクションの共有方法(DBトランザクション)を理解する](http://qiita.com/kazuki43zoo/items/48061b82933e9b3d4ca8)」を投稿しましたが、3rdパーティ製のDBアクセスライブラリ(Domaとか)やオレオレJDBCフレームワークをSpringのトランザクション管理下で実行する方法も紹介しておきましょう。

# 動作検証バージョン

* Spring Boot 1.5.1.RELEASE
* Spring Framework 4.3.6.RELEASE

# オレオレDBアクセス機能を使ったSQLの実行

まず、`DataSource`から取得した`Connection`を使ってSQLを実行するオレオレDBアクセス機能を実装します。


```java
package com.example;

import java.sql.*;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Properties;

import javax.sql.DataSource;

import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.SimpleDriverDataSource;
import org.springframework.transaction.annotation.Transactional;

@SpringBootApplication
public class SpringTxDemoApplication implements CommandLineRunner {

	public static void main(String[] args) {
		SpringApplication.run(SpringTxDemoApplication.class, args);
	}

	private final SqlRunner sqlRunner;

	public SpringTxDemoApplication(SqlRunner sqlRunner) {
		this.sqlRunner = sqlRunner;
	}

	@Transactional
	@Override
	public void run(String... args) throws Exception {

		sqlRunner.insert("INSERT INTO city (name, state, country) VALUES ('San Francisco', 'CA', 'US')");

		Map<String, Object> city = sqlRunner.find("SELECT id, name, state, country FROM city WHERE id = 1");

		System.out.println(city);

	}

	// ★★★ オレオレDBアクセス機能 ★★★
	static class SqlRunner {

		private final DataSource dataSource;

		SqlRunner(DataSource dataSource) {
			this.dataSource = dataSource;
		}

		int insert(String sql) throws SQLException {
			try (Connection connection = dataSource.getConnection(); Statement statement = connection.createStatement()) {
				return statement.executeUpdate(sql);
			}
		}

		Map<String, Object> find(String sql) throws SQLException {
			Map<String, Object> record = null;
			try (Connection connection = dataSource.getConnection();
					Statement statement = connection.createStatement();
					ResultSet resultSet = statement.executeQuery(sql)) {
				ResultSetMetaData metaData = resultSet.getMetaData();
				if (resultSet.next()) {
					record = new LinkedHashMap<>();
					for (int i = 0; i < metaData.getColumnCount(); i++) {
						record.put(metaData.getColumnName(i + 1), resultSet.getObject(i + 1));
					}
				}
			}
			return record;
		}

	}

	@Configuration
	static class DataSourceConfiguration {
		@Bean
		public DataSource dataSource(DataSourceProperties properties) throws ClassNotFoundException {
			SimpleDriverDataSource dataSource = new SimpleDriverDataSource();
			dataSource.setDriverClass((Class<? extends Driver>) Class.forName(properties.determineDriverClassName()));
			dataSource.setUrl(properties.determineUrl());
			dataSource.setUsername(properties.determineUsername());
			dataSource.setPassword(properties.determinePassword());
			Properties connectionProperties = new Properties();
			connectionProperties.setProperty("autoCommit", "false");
			dataSource.setConnectionProperties(connectionProperties);
			return dataSource;
		}

		// ★★★ オレオレDBアクセス機能のBean定義 ★★★
		@Bean
		public SqlRunner sqlRunner(DataSource dataSource) {
			return new SqlRunner(dataSource);
		}
	}

}
```

オレオレDBアクセス機能を使ってSQLを実行してみます。

```
$ ./mvnw spring-boot:run
...
2017-02-12 23:24:29.820 DEBUG 46942 --- [           main] o.s.j.d.DataSourceTransactionManager     : Creating new transaction with name [com.example.SpringTxDemoApplication$$EnhancerBySpringCGLIB$$28c8c8e4.run]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT; ''
2017-02-12 23:24:29.821 DEBUG 46942 --- [           main] o.s.j.datasource.SimpleDriverDataSource  : Creating new JDBC Driver Connection to [jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE]
2017-02-12 23:24:29.821 DEBUG 46942 --- [           main] o.s.j.d.DataSourceTransactionManager     : Acquired Connection [conn1: url=jdbc:h2:mem:testdb user=SA] for JDBC transaction
2017-02-12 23:24:29.842 DEBUG 46942 --- [           main] o.s.j.datasource.SimpleDriverDataSource  : Creating new JDBC Driver Connection to [jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE]
2017-02-12 23:24:29.847 DEBUG 46942 --- [           main] o.s.j.datasource.SimpleDriverDataSource  : Creating new JDBC Driver Connection to [jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE]
null
2017-02-12 23:24:29.861 DEBUG 46942 --- [           main] o.s.j.d.DataSourceTransactionManager     : Initiating transaction commit
2017-02-12 23:24:29.861 DEBUG 46942 --- [           main] o.s.j.d.DataSourceTransactionManager     : Committing JDBC transaction on Connection [conn1: url=jdbc:h2:mem:testdb user=SA]
2017-02-12 23:24:29.861 DEBUG 46942 --- [           main] o.s.j.d.DataSourceTransactionManager     : Releasing JDBC Connection [conn1: url=jdbc:h2:mem:testdb user=SA] after transaction
2017-02-12 23:24:29.862 DEBUG 46942 --- [           main] o.s.jdbc.datasource.DataSourceUtils      : Returning JDBC Connection to DataSource
...
```

Springのトランザクション管理は有効になっていますが、INSERT後のSELECTでレコードが取得できません。
ログをみてみると・・・「`Creating new JDBC Driver Connection to [jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE]`」が３回出力されており、トランザクション開始時に取得した`Connection`が共有できていないようです。

# Springのトランザクション管理下への参加

3rdパーティ製のDBアクセスライブラリ(Domaとか)やオレオレJDBCフレームワークをSpringのトランザクション管理下に参加させたい場合は、オリジナルの`DataSource`を`TransactionAwareDataSourceProxy`にラップして使用します。

```java
@Bean
public SqlRunner sqlRunner(DataSource dataSource) {
	return new SqlRunner(new TransactionAwareDataSourceProxy(dataSource)); // ★★★ TransactionAwareDataSourceProxyにラップする ★★★
}
```

`TransactionAwareDataSourceProxy`にラップして実行すると、INSERT後のSELECTでレコードが取得できます。ログを確認すると、`Connection`の生成が１回になっており、`Connection`が共有されていることが確認できます。

```
$ ./mvnw spring-boot:run
...
2017-02-13 00:50:24.228 DEBUG 47173 --- [           main] o.s.j.d.DataSourceTransactionManager     : Creating new transaction with name [com.example.SpringTxDemoApplication$$EnhancerBySpringCGLIB$$8ef74f1f.run]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT; ''
2017-02-13 00:50:24.229 DEBUG 47173 --- [           main] o.s.j.datasource.SimpleDriverDataSource  : Creating new JDBC Driver Connection to [jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE]
2017-02-13 00:50:24.229 DEBUG 47173 --- [           main] o.s.j.d.DataSourceTransactionManager     : Acquired Connection [conn1: url=jdbc:h2:mem:testdb user=SA] for JDBC transaction
{ID=1, NAME=San Francisco, STATE=CA, COUNTRY=US}
2017-02-13 00:50:24.257 DEBUG 47173 --- [           main] o.s.j.d.DataSourceTransactionManager     : Initiating transaction commit
2017-02-13 00:50:24.257 DEBUG 47173 --- [           main] o.s.j.d.DataSourceTransactionManager     : Committing JDBC transaction on Connection [conn1: url=jdbc:h2:mem:testdb user=SA]
2017-02-13 00:50:24.258 DEBUG 47173 --- [           main] o.s.j.d.DataSourceTransactionManager     : Releasing JDBC Connection [conn1: url=jdbc:h2:mem:testdb user=SA] after transaction
2017-02-13 00:50:24.258 DEBUG 47173 --- [           main] o.s.jdbc.datasource.DataSourceUtils      : Returning JDBC Connection to DataSource
...
```



`TransactionAwareDataSourceProxy`を使用すると、以下の図で示すようにSpringのトランザクションに割り当てられた`Connection`に処理を委譲するため、3rdパーティ製のDBアクセスライブラリ内で発行するSQLをSpringのトランザクション管理下で実行することができます。

![spring-jdbc-aware-tx.png](https://qiita-image-store.s3.amazonaws.com/0/117313/45dabd48-accf-a5b9-73bf-45e8b6cf1499.png)

| 項番 | 説明 |
| :---: | --- |
| ① | ControllerからSpringのトランザクション管理下のServiceクラスのメソッドを呼び出す。トランザクション管理下のServiceクラスのメソッドを呼び出すと、実際にはProxyオブジェクトのメソッドが呼び出され、トランザクション制御を行うクラス(`TransactionInterceptor`)が呼び出される仕組みになっています。|
| ② | `TransactionInterceptor`は、`DataSourceTransactionManager`のメソッドを呼び出してトランザクションの開始を依頼する。|
| ③ | `DataSourceTransactionManager`は、自身に割り当てられた`DataSource`から`Connection`(`autoCommit`は`false`となる)を取得し`ConnectionHolder`(トランザクション内で共有する`Connection`を保持する領域)を生成する。生成した`ConnectionHolder`は、`TransactionSynchronizationManager`クラス上のスレッドローカルな`Map`型の変数に格納される。|
| ④ | `TransactionInterceptor`は、トランザクション開始後にServiceのメソッドを呼び出す。|
| ⑤ | Serviceから3rdパーティ製のDBアクセスライブラリのメソッドを呼び出す。 |
| ⑥ | 3rdパーティ製のDBアクセスライブラリは、`DataSource`(`TransactionAwareDataSourceProxy`)から`Connection`を取得する。|
| ⑦ | `DataSource`から取得した`Connection`のメソッドを使用してSQLの実行を依頼する。`TransactionAwareDataSourceProxy`から取得した`Connection`のメソッドを呼び出すと、実際には`Connection`のProxyオブジェクトのメソッドが呼び出され、Springのトランザクションに割り当てられた`Connection`に処理を委譲するクラス(`TransactionAwareInvocationHandler`)が呼び出される仕組みになっています。|
| ⑧ | `TransactionAwareInvocationHandler`は、`DataSourceUtil#doGetConnection(DataSource)`を介してトランザクションに割り当てられている`Connection`を取得し、取得した`Connection`のメソッドを使用してSQLの実行を依頼する。 |


# MyBatis(MyBatis-Spring)使用時は`TransactionAwareDataSourceProxy`は不要

私が仕事でよくつかうMyBatis(MyBatis-Spring)は3rdパーティ製のDBアクセスライブラリですが、`DataSource`を`TransactionAwareDataSourceProxy`でラップする必要はありません。
これは、MyBatis-SpringがMyBatis本体の中で使う`Connection`を`DataSourceUtil#getConnection(DataSource)`メソッドを介して取得する仕組みになっているためです。
仮に`TransactionAwareDataSourceProxy`でラップしても、MyBatis-Springの中で`TransactionAwareDataSourceProxy`の中に保持するオリジナルの`DataSource`を取得して利用する仕組みなっています。

# まとめ

SpringまたはSpringとの連携コンポーネントの提供がない3rdパーティ製のDBアクセスライブラリを使う場合でも、Springのトランザクション管理下でSQLを実行できるよ！！という紹介でした。私は仕事ではSpringの`JdbcTemplate`やMyBatisを使うことが多いので、ほとんど`TransactionAwareDataSourceProxy`を使ったことはありませんが、お気に入りのDBアクセスライブラリが連携コンポーネントを提供していない場合は、`TransactionAwareDataSourceProxy`を使うとよいでしょう。

# 参考サイト

* http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#tx-resource-synchronization-tadsp