理解Spring框架中的数据库连接共享方法(数据库事务)
本次,我将解释有关Spring的数据库连接共享方法(即在同一个事务中执行多个SQL的方法)的机制。
只是解释机制可能会对不熟悉Spring的人来说有一定难度!?因此……我想边使用Spring提供的数据库访问功能(JdbcTemplate),边解释Spring是如何生成和共享数据库连接的。
行为验证版本
-
- Spring Boot 1.5.1.RELEASE
- Spring Framework 4.3.6.RELEASE
创建一个用于验证的Spring Boot项目。
首先,让我们创建一个用于验证的Spring Boot项目(在Dependencies中指定jdbc和h2)。在这个例子中,我们使用命令行创建项目,不过您也可以使用SPRING INITIALIZR的Web UI或您所使用的IDE的功能来生成(当然也可以)。
$ 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的内存数据库进行验证。
CREATE TABLE account (
id CHAR(10) PRIMARY KEY,
name VARCHAR(255)
);
另外,由于很难验证Spring Boot内置的DataSource(带有连接池的DataSource)是否正确执行了事务控制,所以我们会定义一个没有连接池的DataSource作为验证用途。
注意事项:
开发一般应用程序时,请务必使用连接池!!(不需要像下面这样的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进行数据库访问
让我们在SpringTxDemoApplication类中实现CommandLineRunner接口,并在run方法中使用JdbcTemplate来实现对数据库的访问。
注意:
将Spring提供的DB访问功能的日志(调试日志)输出,可以更容易地确认其内部运作方式。
src/main/resources/application.properties
logging.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条记录并且获取了插入的记录,所以……
在控制台上…
{ID=1, NAME=San Francisco, STATE=CA, COUNTRY=US}
我想您期望输出为「と」,但是…..
当实际执行时,获取插入的记录时会发生以下错误。
./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时处理连接的方式
为什么在最近插入记录的时候找不到记录呢?
下图展示了在没有Spring事务管理的情况下调用JdbcTemplate方法时,如何获取连接。
JdbcTemplate
のメソッドを呼び出す。③JdbcTemplate
は、自身に割り当てられているDataSource
からConnection
を取得する。その際、直接DataSource
からConnection
を取得するのではなく、DataSourceUtil#getConnection(DataSource)
を介して取得する仕組みになっています。後で詳しく説明しますが、トランザクション管理下でのConnection
の共有を実現しているのがDataSourceUtil
のメソッドになります。④DataSourceUtil
から取得したConnection
のメソッドを使用してSQLの実行を依頼する。⑤Connection
(Connection
から取得したStatement
)は、データベースにSQLを実行する。根据气泡说明,每次调用JdbcTemplate的方法都会使用不同的连接,因此如果在INSERT时使用的连接未进行提交,后续的SELECT时使用的连接将无法找到INSERT的记录。(顺便提一下,这是在连接的隔离级别为“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时处理连接时,在事务管理下的处理方式。
如果想在同一事务中执行多个更新操作,应该怎么做呢?正如Spring用户所知道的那样,只需在希望在事务管理下执行的类或方法上添加@Transactional注解即可。
请注意:
如果使用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
..
查看日志发现…生成连接(新建JDBC驱动连接至[jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE])的日志输出仅仅只有一次,而DataSourceTransactionManager的日志(包括事务开始的日志等)开始被输出。从日志来看,似乎存在多个操作共享同一个连接。
那么,我们来解析一下为什么会共享连接。
下面的图表展示了在Spring的事务管理下调用JdbcTemplate方法时,如何生成和使用连接。
虽然有点复杂,但只要逐个查看,相信你一定能够理解的!!(可能我的解释有些不清楚……)
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を実行する。简单地说,就是在事务开始时从数据源获取一个连接,然后通过使用DataSourceUtil(TransactionSynchronizationManager)类的方法来共享该连接,在后续的操作中,能够在同一事务中执行多个操作的机制。(第3点和第6点是关键)
尽管图表中没有记录,但当Service类的方法调用结束时,TransactionInterceptor会调用DataSourceTransactionManager的commit方法或rollback方法,从而调用被分配给事务的Connection的commit方法或rollback方法来实现。
除了JDBC外的事务是什么? JDBC de shì ?)
除了JDBC之外,Spring和其子项目提供了一系列用于不同基础设施的PlatformTransactionManager,包括JMS(Java消息服务)、JTA(Java事务API)、JPA(Java持久化API)等。虽然各种机制用于在同一事务中执行多个操作略有不同,但是像JDBC一样,它们通常采用通过TransactionSynchronizationManager共享必要的事务控制对象的方式。(至少JMS和JPA采用了相同的方式)
顺便提一下,我经常在工作中使用的MyBatis(MyBatis-Spring)的SqlSession也通过TransactionSynchronizationManager来控制在同一事务内共享相同的SqlSession。
最后
由於我沒有進行整理,所以作為最後…
我久違地寫下了”理解系列”。這次我介紹了Spring的核心功能之一,即交易管理機制的一部分。由於Spring有各種功能,所以如果心情好的話(只要我自己能夠理解),我會考慮再次發表。
因为「动了所以可以!而不是……为什么?怎么做到的?想要解开正在运动的人的谜底」
相关条目 (Guanxi entari)
以下是与交易相关的条目。
-
- 3rdパーティ製のDBアクセスライブラリをSpringのトランザクション管理下に参加させる方法
-
- Spring Boot 1.5からPlatformTransactionManager用のCustomizerが追加される
-
- Spring Bootで@Transactionalを使わずにトランザクション制御を行う方法
-
- SpringのDataSourceTransactionManagerを使うとエラー時にCommitされる可能性あり!?
- MyBatis-Spring 1.3からSpringのトランザクションタイムアウト値が連携される!!
参考网站
我参考了以下网站。
-
- http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#tx-decl-explained
- http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#jdbc-DataSourceTransactionManager