先ほど「SpringのDBコネクションの共有方法(DBトランザクション)を理解する」を投稿しましたが、3rdパーティ製のDBアクセスライブラリ(Domaとか)やオレオレJDBCフレームワークをSpringのトランザクション管理下で実行する方法も紹介しておきましょう。
動作検証バージョン
- Spring Boot 1.5.1.RELEASE
- Spring Framework 4.3.6.RELEASE
オレオレDBアクセス機能を使ったSQLの実行
まず、DataSource
から取得したConnection
を使ってSQLを実行するオレオレDBアクセス機能を実装します。
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]
」が3回出力されており、トランザクション開始時に取得したConnection
が共有できていないようです。
Springのトランザクション管理下への参加
3rdパーティ製のDBアクセスライブラリ(Domaとか)やオレオレJDBCフレームワークをSpringのトランザクション管理下に参加させたい場合は、オリジナルのDataSource
をTransactionAwareDataSourceProxy
にラップして使用します。
@Bean
public SqlRunner sqlRunner(DataSource dataSource) {
return new SqlRunner(new TransactionAwareDataSourceProxy(dataSource)); // ★★★ TransactionAwareDataSourceProxyにラップする ★★★
}
TransactionAwareDataSourceProxy
にラップして実行すると、INSERT後のSELECTでレコードが取得できます。ログを確認すると、Connection
の生成が1回になっており、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のトランザクション管理下で実行することができます。
項番 | 説明 |
---|---|
① | 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
を使うとよいでしょう。