LoginSignup
29
26

More than 5 years have passed since last update.

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

Posted at

先ほど「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のトランザクション管理下に参加させたい場合は、オリジナルのDataSourceTransactionAwareDataSourceProxyにラップして使用します。

@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のトランザクション管理下で実行することができます。

spring-jdbc-aware-tx.png

項番 説明
ControllerからSpringのトランザクション管理下のServiceクラスのメソッドを呼び出す。トランザクション管理下のServiceクラスのメソッドを呼び出すと、実際にはProxyオブジェクトのメソッドが呼び出され、トランザクション制御を行うクラス(TransactionInterceptor)が呼び出される仕組みになっています。
TransactionInterceptorは、DataSourceTransactionManagerのメソッドを呼び出してトランザクションの開始を依頼する。
DataSourceTransactionManagerは、自身に割り当てられたDataSourceからConnection(autoCommitfalseとなる)を取得し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アクセスライブラリですが、DataSourceTransactionAwareDataSourceProxyでラップする必要はありません。
これは、MyBatis-SpringがMyBatis本体の中で使うConnectionDataSourceUtil#getConnection(DataSource)メソッドを介して取得する仕組みになっているためです。
仮にTransactionAwareDataSourceProxyでラップしても、MyBatis-Springの中でTransactionAwareDataSourceProxyの中に保持するオリジナルのDataSourceを取得して利用する仕組みなっています。

まとめ

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

参考サイト

29
26
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
29
26