理解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方法时,如何获取连接。

spring-jdbc-non-tx.png
項番説明①ControllerからSpringのトランザクション管理外のServiceクラスのメソッドを呼び出す。②Serviceから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方法时,如何生成和使用连接。
虽然有点复杂,但只要逐个查看,相信你一定能够理解的!!(可能我的解释有些不清楚……)

spring-jdbc-tx.png
項番説明①ControllerからSpringのトランザクション管理下のServiceクラスのメソッドを呼び出す。トランザクション管理下のServiceクラスのメソッドを呼び出すと、実際にはProxyオブジェクトのメソッドが呼び出され、トランザクション制御を行うクラス(TransactionInterceptor)が呼び出される仕組みになっています。②TransactionInterceptorは、DataSourceTransactionManagerのメソッドを呼び出してトランザクションの開始を依頼する。③DataSourceTransactionManagerは、自身に割り当てられたDataSourceからConnection(autoCommitfalseとなる)を取得しConnectionHolder(トランザクション内で共有するConnectionを保持する領域)を生成する。生成したConnectionHolderは、TransactionSynchronizationManagerクラス上のスレッドローカルなMap型の変数に格納されます(Mapに格納する際のキーとしてDataSourceインスタンスが利用されます)。④TransactionInterceptorは、トランザクション開始後にServiceのメソッドを呼び出す。⑤ServiceからJdbcTemplateのメソッドを呼び出す。⑥JdbcTemplateは、DataSourceUtil#getConnection(DataSource)を介してトランザクションに割り当てられているConnectionを取得し、取得したConnectionのメソッドを使用してSQLの実行を依頼する。DataSourceUtilは、TransactionSynchronizationManagerクラス上のスレッドローカルなMap型の変数からConnectionHolderを取得する際のキーとして引数で受け取ったDataSourceインスタンスを指定します。そのため、JdbcTemplateDataSourceTransactionManagerに指定する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
广告
将在 10 秒后关闭
bannerAds