今回は、SpringのDBコネクションの共有方法(=複数のSQLを同一トランザクション内で実行する方法)の仕組みについて説明します。
仕組みだけ説明をするとSpringに詳しくない人に対してハードルが高くなる!?気がするので・・・Spring提供のDBアクセス機能(JdbcTemplate
)を実際に使いながら、SpringがDBコネクションをどうやって生成・共有するのか説明していきたいと思います。
動作検証バージョン
- Spring Boot 1.5.1.RELEASE
- Spring Framework 4.3.6.RELEASE
検証用のSpring Bootプロジェクトの作成
まず、検証用のSpring Bootプロジェクト作成しましょう(Dependencieにはjdbcとh2を指定)。ここではコマンドラインでプロジェクトを作成する例になっていますが、SPRING INITIALIZRのWeb UIやお使いのIDEの機能で生成しても(もちろん)OKです!!
$ curl -s https://start.spring.io/starter.tgz\
-d name=spring-tx-demo\
-d artifactId=spring-tx-demo\
-d dependencies=jdbc,h2\
-d baseDir=spring-tx-demo\
| tar -xzvf -
プロジェクトを生成すると、以下のような構成のMavenプロジェクトが生成されます。
$ cd spring-tx-demo
$ tree
.
├── mvnw
├── mvnw.cmd
├── pom.xml
└── src
├── main
│ ├── java
│ │ └── com
│ │ └── example
│ │ └── SpringTxDemoApplication.java
│ └── resources
│ └── application.properties
└── test
└── java
└── com
└── example
└── SpringTxDemoApplicationTests.java
必要に応じてお使いのIDE上にインポートしてください!!
データベース(データソース)のセットアップ
今回はH2のインメモリDBを使用して検証します。
CREATE TABLE account (
id CHAR(10) PRIMARY KEY,
name VARCHAR(255)
);
また、Spring Bootが内蔵しているDataSource
(コネクションプール付きのDataSource
)を使用するとトランザクション制御が正しく行われているか検証するのが難しいので、検証用にコネクションプールなしのDataSource
をBean定義します。
Note:
通常のアプリケションを開発する際は、コネクションプールを使ってくださいね!!(↓のようなBean定義は不要です)
package com.example;
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 javax.sql.DataSource;
import java.sql.Driver;
import java.util.Properties;
@SpringBootApplication
public class SpringTxDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringTxDemoApplication.class, args);
}
@Configuration
static class DataSourceConfiguration {
@Bean
public DataSource dataSource(DataSourceProperties properties) throws ClassNotFoundException {
// ★★★ Springが提供しているコネクションをプールしない実装クラスを利用 ★★★
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"); // ★★★ 自動コミットをOFFにする ★★★
dataSource.setConnectionProperties(connectionProperties);
return dataSource;
}
}
}
また、自動コミットフラグは必ずfalse
(自動コミットしない)に設定してください。自動コミットフラグをtrue
(自動コミットする)にしてしまうと、実際だれがコミットしたのか判断できないので・・・。
JdbcTemplate
を使用したDBアクセス
SpringTxDemoApplication
クラスにCommandLineRunner
インタフェースを実装し、run
メソッドの中でJdbcTemplate
を使用したDBアクセスを実装しましょう。
Note:
Springが提供するDBアクセス機能のログ(デバッグログ)を出力するようにしておくと、内部でどのような動きをしているか確認しやすくなります。
src/main/resources/application.properteislogging.level.org.springframework.jdbc=debug
package com.example;
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.core.JdbcTemplate;
import org.springframework.jdbc.datasource.SimpleDriverDataSource;
import javax.sql.DataSource;
import java.sql.Driver;
import java.util.Map;
import java.util.Properties;
@SpringBootApplication
public class SpringTxDemoApplication implements CommandLineRunner {
public static void main(String[] args) {
SpringApplication.run(SpringTxDemoApplication.class, args);
}
private final JdbcTemplate jdbcTemplate;
public SpringTxDemoApplication(JdbcTemplate jdbcTemplate){
this.jdbcTemplate = jdbcTemplate;
}
// ★★★ JdbcTemplateを使用したDBアクセス処理を実装する ★★★
@Override
public void run(String... args) throws Exception {
jdbcTemplate.update("INSERT INTO city (name, state, country) VALUES ('San Francisco', 'CA', 'US')");
Map<String, Object> city = jdbcTemplate.queryForMap("SELECT id, name, state, country FROM city WHERE id = 1");
System.out.println(city);
}
@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;
}
}
}
では、このクラスをSpring Bootアプリとして実行すると、どういう結果になるでしょうか?
1件レコードをINSERTして、INSERTしたレコードを取得しているので・・・
コンソールには・・・
{ID=1, NAME=San Francisco, STATE=CA, COUNTRY=US}
と出力されることを期待すると思いますが・・・・
実際に実行してみると、INSERTしたレコードを取得する際に以下のエラーが発生します
./mvnw spring-boot:run
...
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v1.5.1.RELEASE)
...
2017-02-12 17:26:08.627 ERROR 46138 --- [ main] o.s.boot.SpringApplication : Application startup failed
java.lang.IllegalStateException: Failed to execute CommandLineRunner
at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:779) [spring-boot-1.5.1.RELEASE.jar:1.5.1.RELEASE]
at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:760) [spring-boot-1.5.1.RELEASE.jar:1.5.1.RELEASE]
at org.springframework.boot.SpringApplication.afterRefresh(SpringApplication.java:747) [spring-boot-1.5.1.RELEASE.jar:1.5.1.RELEASE]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:315) [spring-boot-1.5.1.RELEASE.jar:1.5.1.RELEASE]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1162) [spring-boot-1.5.1.RELEASE.jar:1.5.1.RELEASE]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1151) [spring-boot-1.5.1.RELEASE.jar:1.5.1.RELEASE]
at com.example.SpringTxDemoApplication.main(SpringTxDemoApplication.java:21) [classes/:na]
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_121]
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_121]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_121]
at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_121]
at org.springframework.boot.maven.AbstractRunMojo$LaunchRunner.run(AbstractRunMojo.java:527) [spring-boot-maven-plugin-1.5.1.RELEASE.jar:1.5.1.RELEASE]
at java.lang.Thread.run(Thread.java:745) [na:1.8.0_121]
Caused by: org.springframework.dao.EmptyResultDataAccessException: Incorrect result size: expected 1, actual 0
at org.springframework.dao.support.DataAccessUtils.requiredSingleResult(DataAccessUtils.java:71) ~[spring-tx-4.3.6.RELEASE.jar:4.3.6.RELEASE]
at org.springframework.jdbc.core.JdbcTemplate.queryForObject(JdbcTemplate.java:495) ~[spring-jdbc-4.3.6.RELEASE.jar:4.3.6.RELEASE]
at org.springframework.jdbc.core.JdbcTemplate.queryForMap(JdbcTemplate.java:489) ~[spring-jdbc-4.3.6.RELEASE.jar:4.3.6.RELEASE]
at com.example.SpringTxDemoApplication.run(SpringTxDemoApplication.java:35) [classes/:na]
at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:776) [spring-boot-1.5.1.RELEASE.jar:1.5.1.RELEASE]
... 12 common frames omitted
...
原因例外をみると、「Caused by: org.springframework.dao.EmptyResultDataAccessException: Incorrect result size: expected 1, actual 0
」となっているので、レコードがみつからなかったことが原因のようです。
JdbcTemplate
利用時のConnectionの扱い
では、なぜ直前にINSERTしたはずのレコードがみつからなかったのでしょうか?
以下の図は、Springのトランザクション管理外でJdbcTemplate
のメソッドを呼び出した場合に、どのようにConnection
を取得しているか示したものです。
項番 | 説明 |
---|---|
① | ControllerからSpringのトランザクション管理外のServiceクラスのメソッドを呼び出す。 |
② | ServiceからJdbcTemplate のメソッドを呼び出す。 |
③ |
JdbcTemplate は、自身に割り当てられているDataSource からConnection を取得する。その際、直接DataSource からConnection を取得するのではなく、DataSourceUtil#getConnection(DataSource) を介して取得する仕組みになっています。後で詳しく説明しますが、トランザクション管理下でのConnection の共有を実現しているのがDataSourceUtil のメソッドになります。 |
④ |
DataSourceUtil から取得したConnection のメソッドを使用してSQLの実行を依頼する。 |
⑤ |
Connection (Connection から取得したStatement )は、データベースにSQLを実行する。 |
図中の吹き出しで説明しているように、JdbcTemplate
のメソッド呼び出しごとに別のConnection
が使われるため、INSERT時に使用したConnection
がコミットされないと、後続のSELECT時に使うConnection
からはINSERTしたレコードをみつけることはできません。(ちなみに・・・これはConnection
の分離レベル=Isolation LevelがREAD COMMITTEDの時の動きなので、分離レベルによってはレコードをみつけることができる点を補足しておきます)
ためしに・・・自動コミットフラグをtrue
にして再度実行してみてください。
@Bean
public DataSource dataSource(DataSourceProperties properties) throws ClassNotFoundException {
SimpleDriverDataSource dataSource = new SimpleDriverDataSource();
// ...
connectionProperties.setProperty("autoCommit", "true"); // ★★★ trueに変更 ★★★
dataSource.setConnectionProperties(connectionProperties);
return dataSource;
}
$ ./mvnw spring-boot:run
...
2017-02-12 18:15:25.097 DEBUG 46292 --- [ main] o.s.jdbc.core.JdbcTemplate : Executing SQL update [INSERT INTO city (name, state, country) VALUES ('San Francisco', 'CA', 'US')]
2017-02-12 18:15:25.098 DEBUG 46292 --- [ main] o.s.jdbc.datasource.DataSourceUtils : Fetching JDBC Connection from DataSource
2017-02-12 18:15:25.098 DEBUG 46292 --- [ 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 18:15:25.100 DEBUG 46292 --- [ main] o.s.jdbc.core.JdbcTemplate : SQL update affected 1 rows
2017-02-12 18:15:25.103 DEBUG 46292 --- [ main] o.s.jdbc.datasource.DataSourceUtils : Returning JDBC Connection to DataSource
2017-02-12 18:15:25.103 DEBUG 46292 --- [ main] o.s.jdbc.core.JdbcTemplate : Executing SQL query [SELECT id, name, state, country FROM city WHERE id = 1]
2017-02-12 18:15:25.103 DEBUG 46292 --- [ main] o.s.jdbc.datasource.DataSourceUtils : Fetching JDBC Connection from DataSource
2017-02-12 18:15:25.103 DEBUG 46292 --- [ 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 18:15:25.123 DEBUG 46292 --- [ main] o.s.jdbc.datasource.DataSourceUtils : Returning JDBC Connection to DataSource
{ID=1, NAME=San Francisco, STATE=CA, COUNTRY=US}
2017-02-12 18:15:25.126 INFO 46292 --- [ main] com.example.SpringTxDemoApplication : Started SpringTxDemoApplication in 1.636 seconds (JVM running for 5.305)
...
上記のようなログが出力され、SELECTに成功します。これは、自動コミットフラグをtrue
にすることで、INSERT成功時にConnection
がコミットされたためです。
じゃ〜自動コミットフラグをtrue
にすれば一件落着かというと・・・当然ながらそんなことはありません
説明する必要もないと思いますが・・・以下のように複数の更新操作を行う場合は、すべての更新操作が成功した場合にコミット、一部の更新操作が失敗した場合はロールバックする(=いわゆるトランザクション制御を行う)必要がでてきます。こういったケースでは、SQLの実行毎にコミットが行われてしまう「自動コミットフラグ=true
」を使うわけにはいかないのです。
@Override
public void run(String... args) throws Exception {
jdbcTemplate.update("INSERT INTO city (name, state, country) VALUES ('San Francisco', 'CA', 'US')");
jdbcTemplate.update("INSERT INTO city (name, state, country) VALUES ('豊島区', '東京', 'JPN')");
// ...
}
トランザクション管理下でのJdbcTemplate
利用時のConnectionの扱い
複数の更新操作を同一トランザクション内で行うためにはどうすればいいのか?というと・・・Springユーザならご存知のとおり、トランザクション管理下で実行したいクラスやメソッドに@Transactional
を付与するだけです。
Note:
Spring Bootを使う場合は特別な設定は不要ですが・・・非Spring Bootの場合は、
@EnableTransactionManagement
の付与PlatformTransactionManager
(DBトランザクションの場合はDataSourceTransactionManager
)のBean定義が必要になります。(本エントリーでは非Spring Bootの説明は割愛させて頂きます)
では、実際に検証アプリに@Transactional
を付与して実行してみましょう。(なお、自動コミットフラグをfalse
に戻すことを忘れずに!!)
@Transactional // ★★★ メソッドに付与 ★★★
@Override
public void run(String... args) throws Exception {
jdbcTemplate.update("INSERT INTO city (name, state, country) VALUES ('San Francisco', 'CA', 'US')");
Map<String, Object> city = jdbcTemplate.queryForMap("SELECT id, name, state, country FROM city WHERE id = 1");
System.out.println(city);
}
@Transactional
を付与した後にSpring Bootアプリを実行すると、SELECTも成功するようになりました
$ ./mvnw spring-boot:run
...
2017-02-12 18:42:46.689 DEBUG 46340 --- [ main] o.s.j.d.DataSourceTransactionManager : Creating new transaction with name [com.example.SpringTxDemoApplication$$EnhancerBySpringCGLIB$$7f2a8641.run]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT; ''
2017-02-12 18:42:46.690 DEBUG 46340 --- [ 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 18:42:46.690 DEBUG 46340 --- [ main] o.s.j.d.DataSourceTransactionManager : Acquired Connection [conn1: url=jdbc:h2:mem:testdb user=SA] for JDBC transaction
2017-02-12 18:42:46.697 DEBUG 46340 --- [ main] o.s.jdbc.core.JdbcTemplate : Executing SQL update [INSERT INTO city (name, state, country) VALUES ('San Francisco', 'CA', 'US')]
2017-02-12 18:42:46.700 DEBUG 46340 --- [ main] o.s.jdbc.core.JdbcTemplate : SQL update affected 1 rows
2017-02-12 18:42:46.703 DEBUG 46340 --- [ main] o.s.jdbc.core.JdbcTemplate : Executing SQL query [SELECT id, name, state, country FROM city WHERE id = 1]
{ID=1, NAME=San Francisco, STATE=CA, COUNTRY=US}
2017-02-12 18:42:46.721 DEBUG 46340 --- [ main] o.s.j.d.DataSourceTransactionManager : Initiating transaction commit
2017-02-12 18:42:46.722 DEBUG 46340 --- [ main] o.s.j.d.DataSourceTransactionManager : Committing JDBC transaction on Connection [conn1: url=jdbc:h2:mem:testdb user=SA]
2017-02-12 18:42:46.722 DEBUG 46340 --- [ main] o.s.j.d.DataSourceTransactionManager : Releasing JDBC Connection [conn1: url=jdbc:h2:mem:testdb user=SA] after transaction
2017-02-12 18:42:46.722 DEBUG 46340 --- [ main] o.s.jdbc.datasource.DataSourceUtils : Returning JDBC Connection to DataSource
..
ログをみてみると・・・コネクションの生成(Creating new JDBC Driver Connection to [jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE]
)のログ出力が1回だけになり、DataSourceTransactionManager
のログ(トランザクション開始ログなど)が出力されるようになりました。ログを見る限りだと、複数の操作でコネクションが共有されているようです。
では、なぜコネクションが共有されるようになったのか?を紐解いていきましょう。
以下の図は、Springのトランザクション管理下でJdbcTemplate
のメソッドを呼び出した場合に、どのようにConnection
を生成し利用しているかを示したものです。
ちょっと複雑ですが、ひとつひとつ見ていけばきっと理解できると思います!!(わたしの説明がわかりずらい・・・とかはあるかも)
項番 | 説明 |
---|---|
① | ControllerからSpringのトランザクション管理下のServiceクラスのメソッドを呼び出す。トランザクション管理下のServiceクラスのメソッドを呼び出すと、実際にはProxyオブジェクトのメソッドが呼び出され、トランザクション制御を行うクラス(TransactionInterceptor)が呼び出される仕組みになっています。 |
② |
TransactionInterceptor は、DataSourceTransactionManager のメソッドを呼び出してトランザクションの開始を依頼する。 |
③ |
DataSourceTransactionManager は、自身に割り当てられたDataSource からConnection (autoCommit はfalse となる)を取得しConnectionHolder (トランザクション内で共有するConnection を保持する領域)を生成する。生成したConnectionHolder は、TransactionSynchronizationManager クラス上のスレッドローカルなMap 型の変数に格納されます(Map に格納する際のキーとしてDataSource インスタンスが利用されます)。 |
④ |
TransactionInterceptor は、トランザクション開始後にServiceのメソッドを呼び出す。 |
⑤ | ServiceからJdbcTemplate のメソッドを呼び出す。 |
⑥ |
JdbcTemplate は、DataSourceUtil#getConnection(DataSource) を介してトランザクションに割り当てられているConnection を取得し、取得したConnection のメソッドを使用してSQLの実行を依頼する。DataSourceUtil は、TransactionSynchronizationManager クラス上のスレッドローカルなMap 型の変数からConnectionHolder を取得する際のキーとして引数で受け取ったDataSource インスタンスを指定します。そのため、JdbcTemplate とDataSourceTransactionManager に指定するDataSource は、同一インスタンスのものを指定する必要があります。 |
⑦ |
Connection (Connection から取得したStatement )は、データベースにSQLを実行する。 |
つまり・・・トランザクションの開始時にDataSource
からConnection
をひとつ取得し、以降の処理ではDataSourceUtil
(TransactionSynchronizationManager
)クラスのメソッドを介してConnection
を共有することで、複数の操作を同一トランザクション内で実行する仕組みを提供しているのです。(③と⑥がポイント)
図には記載していませんが、Serviceクラスのメソッド呼び出しが終了すると、TransactionInterceptor
からDataSourceTransactionManager
のcommit
メソッド又はrollback
メソッドが呼び出され、トランザクションに割り当てられているConnection
のcommit
メソッド又はrollback
メソッドが呼び出される仕組みになっています。
JDBC以外のトランザクションは?
JDBC以外にも、JMS(Java Message Service)、JTA(Java Transaction API)、JPA(Java Persistence API)など様々なインフラストラクチャ用のPlatformTransactionManager
がSpringやSpringのサブプロジェクトから提供されています。複数の操作を同一トランザクション内で実行するための仕組みはそれぞれ異なりますが、JDBCと同様にトランザクション制御に必要なオブジェクトをTransactionSynchronizationManager
を介して共有する方式を採用しているケースが多いようです。(少なくてもJMSとJPAは同じ方式を採用しています)
Note:
ちなみに・・・わたしが仕事でよく使うMyBatis(MyBatis-Spring)の
SqlSession
も、TransactionSynchronizationManager
を介して同一トランザクション内で同じSqlSession
が共有されるように制御されています。
さいごに
「まとめ」ることがなかったので、「さいごに」として・・・
「理解するシリーズ」を久々にかいてみました。今回はSpringのコア機能のひとつであるトランザクション管理の仕組みの一部を紹介しました。Springには様々な機能があるので、気が向いたら(書けるくらい自分が理解したら)また投稿したいと思います。
By 「動いたからOK!じゃなくて・・・なぜ?どうやって?動いているのか紐解きたい人」
関連エントリー
以下は、トランザクション関連のエントリーです。
- 3rdパーティ製のDBアクセスライブラリをSpringのトランザクション管理下に参加させる方法
- Spring Boot 1.5からPlatformTransactionManager用のCustomizerが追加される
- Spring Bootで@Transactionalを使わずにトランザクション制御を行う方法
- SpringのDataSourceTransactionManagerを使うとエラー時にCommitされる可能性あり!?
- MyBatis-Spring 1.3からSpringのトランザクションタイムアウト値が連携される!!
参考サイト
以下のサイトを参考にしました。