# 数据访问

引用文档的这一部分涉及数据访问以及数据访问层和业务或服务层之间的交互。

Spring 的全面事务管理支持进行了一些详细的介绍,随后是对 Spring 框架与之集成的各种数据访问框架和技术的全面介绍。

# 1. 事务管理

全面的事务支持是使用 Spring 框架的最有说服力的理由之一。 Spring 框架为事务管理提供了一个一致的抽象,它提供了以下好处:

  • 跨不同事务 API 的一致的编程模型,例如 爪哇 事务 API、JDBC、 Hibernate 和 爪哇 持久性 API( JPA)。

  • 支持声明式事务管理

  • 用于纲领性的事务管理的比复杂事务 API(如 JTA)更简单的 API。

  • 与 Spring 的数据访问抽象进行了出色的集成。

以下章节描述了 Spring 框架的事务特性和技术:

本章还讨论了最佳实践,应用服务器集成常见问题的解决方案

# 1.1. Spring 框架的事务支持模型的优点

传统上,爪哇 EE 开发人员在事务管理方面有两种选择:全局事务或本地事务,这两种选择都有很大的局限性。在接下来的两节中,将介绍全局和本地事务管理,然后讨论 Spring 框架的事务管理支持如何解决全局和本地事务模型的局限性。

# 1.1.1.全球交易

全局事务允许你使用多个事务资源,通常是关系数据库和消息队列。应用程序服务器通过 JTA 管理全局事务,JTA 是一个繁琐的 API(部分原因是其异常模型)。此外,JTAUserTransaction通常需要来自 JNDI,这意味着你还需要使用 JNDI 才能使用 JTA。全局事务的使用限制了应用程序代码的任何潜在重用,因为 JTA 通常仅在应用程序服务器环境中可用。

以前,使用全局事务的首选方式是通过 EJB CMT(容器管理事务)。CMT 是声明式事务管理的一种形式(区别于程序化事务管理)。EJB CMT 消除了对与事务相关的 JNDI 查找的需求,尽管 EJB 本身的使用需要使用 JNDI。它消除了编写 爪哇 代码来控制事务的大部分(但不是全部)需求。最大的缺点是,CMT 与 JTA 和应用程序服务器环境绑定在一起。而且,只有当你选择在 EJB 中实现业务逻辑(或者至少在事务性 EJB facade 的后面)时,它才可用。总的来说,EJB 的负面影响是如此之大,以至于这不是一个有吸引力的提议,尤其是在面对声明式事务管理的令人信服的替代方案时。

# 1.1.2.本地交易

本地事务是特定于资源的,例如与 JDBC 连接关联的事务。本地事务可能更容易使用,但有一个明显的缺点:它们不能跨多个事务资源工作。例如,通过使用 JDBC 连接来管理事务的代码不能在全局 JTA 事务中运行。由于应用程序服务器不参与事务管理,因此它无法帮助确保跨多个资源的正确性。(值得注意的是,大多数应用程序使用单个事务资源。)另一个缺点是本地事务对编程模型具有侵入性。

# 1.1.3. Spring 框架的一致性编程模型

Spring 解决了全局和本地事务的缺点。它允许应用程序开发人员在任何环境中使用一致的编程模型。你只需编写一次代码,就可以从不同环境中的不同事务管理策略中受益。 Spring 框架提供了声明式和程序化的事务管理。大多数用户更喜欢声明式事务管理,我们在大多数情况下推荐这种方法。

对于程序化事务管理,开发人员使用 Spring 框架事务抽象,它可以运行在任何底层事务基础设施上。对于优选的声明式模型,开发人员通常很少或根本不编写与事务管理相关的代码,因此,不依赖于 Spring Framework Transaction API 或任何其他事务 API。

你需要用于事务管理的应用程序服务器吗?

Spring 框架的事务管理支持改变了 Enterprise爪哇 应用程序何时需要应用服务器的传统规则。

特别是,你不需要一个应用程序服务器,它只用于通过 EJB 进行声明性事务。实际上,即使你的应用程序服务器具有强大的 JTA 功能,你也可能认为 Spring 框架的声明式事务提供了比 EJB CMT 更强大的功能和更高效的编程模型。

通常,只有当你的应用程序需要处理跨多个资源的事务时,你才需要应用程序服务器的 JTA 功能,而这对许多应用程序来说并不是必需的。许多高端应用程序使用单一的、高度可扩展的数据库(如 Oracle RAC)。独立事务管理器(例如Atomikos 交易 (opens new window)JOTM (opens new window))是其他选项。当然,你可能需要其他应用程序服务器功能,例如 爪哇 消息服务和 爪哇 EE 连接器架构。

Spring 框架为你提供了何时将应用程序扩展到全负载应用程序服务器的选择。使用 EJB CMT 或 JTA 的唯一替代方法是使用本地事务(例如 JDBC 连接上的事务)编写代码,如果你需要在全局的、容器管理的事务中运行这些代码,那么你将面临大量的返工,这种情况已经一去不复返了。在 Spring 框架中,只有配置文件中的 Bean 定义中的一些需要更改(而不是代码)。

# 1.2.理解 Spring 框架事务抽象

Spring 事务抽象的关键是事务策略的概念。事务策略由TransactionManager定义,特别是用于命令式事务管理的org.springframework.transaction.PlatformTransactionManager接口和用于反应式事务管理的org.springframework.transaction.ReactiveTransactionManager接口。下面的清单显示了PlatformTransactionManagerAPI 的定义:

public interface PlatformTransactionManager extends TransactionManager {

    TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;

    void commit(TransactionStatus status) throws TransactionException;

    void rollback(TransactionStatus status) throws TransactionException;
}

这主要是一个服务提供程序接口,尽管你可以从应用程序代码中使用它程式设计。因为PlatformTransactionManager是一个接口,所以可以根据需要轻松地对其进行模拟或截断。它不绑定到查找策略,例如 JNDI。实现像 Spring 框架 IoC 容器中的任何其他对象(或 Bean)一样定义。仅这一优点就使 Spring 框架事务成为一个有价值的抽象,即使在使用 JTA 的情况下也是如此。与直接使用 JTA 相比,你可以更容易地测试事务性代码。

同样,根据 Spring 的原理,可以由PlatformTransactionManager接口的任何方法抛出的TransactionException不受检查(也就是说,它扩展了java.lang.RuntimeException类)。事务基础设施故障几乎总是致命的。在应用程序代码实际上可以从事务失败中恢复的极少数情况下,应用程序开发人员仍然可以选择捕获和处理TransactionException。最重要的一点是,开发商并不是被迫这么做的。

getTransaction(..)方法返回一个TransactionStatus对象,取决于TransactionDefinition参数。如果当前调用堆栈中存在匹配的事务,则返回的TransactionStatus可能表示一个新事务,或者可以表示一个现有事务。后一种情况的含义是,与 爪哇 EE 事务上下文一样,TransactionStatus与执行线程相关联。

在 Spring 框架 5.2 中, Spring 还提供了用于使用反应性类型或 Kotlin 协程的反应性应用程序的事务管理抽象。下面的清单显示了由org.springframework.transaction.ReactiveTransactionManager定义的事务策略:

public interface ReactiveTransactionManager extends TransactionManager {

    Mono<ReactiveTransaction> getReactiveTransaction(TransactionDefinition definition) throws TransactionException;

    Mono<Void> commit(ReactiveTransaction status) throws TransactionException;

    Mono<Void> rollback(ReactiveTransaction status) throws TransactionException;
}

反应式事务管理器主要是一个服务提供程序接口,尽管你可以从应用程序代码中使用它程式设计。因为ReactiveTransactionManager是一个接口,所以可以根据需要轻松地对其进行模拟或删除。

TransactionDefinition接口指定:

  • 传播:通常,事务范围内的所有代码都在该事务中运行。但是,如果事务方法在事务上下文已经存在的情况下运行,则可以指定该行为。例如,代码可以在现有的事务中继续运行(常见的情况),或者可以暂停现有的事务并创建新的事务。 Spring 提供了从 EJB CMT 中熟悉的所有事务传播选项。要了解 Spring 中事务传播的语义,请参见事务传播

  • 隔离:此事务与其他事务的工作隔离的程度。例如,这个事务可以看到来自其他事务的未提交的写吗?

  • 超时:此事务在超时和被底层事务基础设施自动回滚之前运行了多长时间。

  • 只读状态:当你的代码读取但不修改数据时,你可以使用只读事务。只读事务在某些情况下可以是一种有用的优化,例如当你使用 Hibernate 时。

这些设置反映了标准的事务概念。如果有必要,请参考讨论事务隔离级别和其他核心事务概念的资源。理解这些概念对于使用 Spring 框架或任何事务管理解决方案是至关重要的。

TransactionStatus接口为事务代码控制事务执行和查询事务状态提供了一种简单的方法。这些概念应该是熟悉的,因为它们是所有事务 API 所共有的。下面的清单显示了TransactionStatus接口:

public interface TransactionStatus extends TransactionExecution, SavepointManager, Flushable {

    @Override
    boolean isNewTransaction();

    boolean hasSavepoint();

    @Override
    void setRollbackOnly();

    @Override
    boolean isRollbackOnly();

    void flush();

    @Override
    boolean isCompleted();
}

无论你是否 OPT 用于 Spring 中的声明式或程序化事务管理,定义正确的TransactionManager实现是绝对必要的。你通常通过依赖注入来定义这个实现。

TransactionManager实现通常需要了解它们工作的环境:JDBC、JTA、 Hibernate 等等。下面的示例展示了如何定义本地PlatformTransactionManager实现(在本例中,使用普通的 JDBC)。

你可以通过创建类似于以下内容的 Bean 来定义 JDBCDataSource:

<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <property name="driverClassName" value="${jdbc.driverClassName}" />
    <property name="url" value="${jdbc.url}" />
    <property name="username" value="${jdbc.username}" />
    <property name="password" value="${jdbc.password}" />
</bean>

相关的PlatformTransactionManager Bean 定义则具有对DataSource定义的引用。它应该类似于以下示例:

<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
</bean>

如果在 爪哇 EE 容器中使用 JTA,则使用通过 JNDI 获得的容器DataSource,并结合 Spring 的JtaTransactionManager。下面的示例显示了 JTA 和 JNDI 查找版本的外观:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:jee="http://www.springframework.org/schema/jee"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/jee
        https://www.springframework.org/schema/jee/spring-jee.xsd">

    <jee:jndi-lookup id="dataSource" jndi-name="jdbc/jpetstore"/>

    <bean id="txManager" class="org.springframework.transaction.jta.JtaTransactionManager" />

    <!-- other <bean/> definitions here -->

</beans>

JtaTransactionManager不需要了解DataSource(或任何其他特定资源),因为它使用容器的全局事务管理基础架构。

前面的dataSource Bean 的定义使用<jndi-lookup/>标记
来自jee名称空间。有关更多信息,请参见JEE 模式
如果你使用 JTA,那么你的事务管理器定义看起来应该是相同的,无论你使用的是什么数据访问技术,无论是 JDBC、 Hibernate JPA,还是任何其他受支持的技术。这是因为 JTA 事务是全局事务,
可以获取任何事务资源。

在所有 Spring 事务设置中,应用程序代码不需要更改。你可以仅通过更改配置来更改事务的管理方式,即使这种更改意味着从本地事务转移到全局事务,或者反之亦然。

# 1.2.1. Hibernate 交易设置

还可以轻松地使用 Hibernate 本地事务,如以下示例所示。在这种情况下,你需要定义一个 Hibernate LocalSessionFactoryBean,你的应用程序代码可以使用它来获得 Hibernate Session实例。

DataSource Bean 的定义类似于前面显示的本地 JDBC 示例,因此在下面的示例中没有显示。

如果DataSource(由任何非 JTA 事务管理器使用)通过
JNDI 查找并由 爪哇 EE 容器管理,则它应该是非事务性的,因为
Spring 框架(而不是 爪哇 EE 容器)管理事务。

在这种情况下,txManager Bean 是HibernateTransactionManager类型的。就像DataSourceTransactionManager需要引用DataSource一样,HibernateTransactionManager也需要引用SessionFactory。下面的示例声明sessionFactorytxManagerbean:

<bean id="sessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean">
    <property name="dataSource" ref="dataSource"/>
    <property name="mappingResources">
        <list>
            <value>org/springframework/samples/petclinic/hibernate/petclinic.hbm.xml</value>
        </list>
    </property>
    <property name="hibernateProperties">
        <value>
            hibernate.dialect=${hibernate.dialect}
        </value>
    </property>
</bean>

<bean id="txManager" class="org.springframework.orm.hibernate5.HibernateTransactionManager">
    <property name="sessionFactory" ref="sessionFactory"/>
</bean>

如果使用 Hibernate 和 爪哇 EE 容器管理的 JTA 事务,则应该使用与上一个 JTA 示例中的 JDBC 相同的JtaTransactionManager,如下例所示。另外,建议 Hibernate 通过 JTA 的事务协调器以及可能的连接释放模式配置来了解 JTA:

<bean id="sessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean">
    <property name="dataSource" ref="dataSource"/>
    <property name="mappingResources">
        <list>
            <value>org/springframework/samples/petclinic/hibernate/petclinic.hbm.xml</value>
        </list>
    </property>
    <property name="hibernateProperties">
        <value>
            hibernate.dialect=${hibernate.dialect}
            hibernate.transaction.coordinator_class=jta
            hibernate.connection.handling_mode=DELAYED_ACQUISITION_AND_RELEASE_AFTER_STATEMENT
        </value>
    </property>
</bean>

<bean id="txManager" class="org.springframework.transaction.jta.JtaTransactionManager"/>

或者,你可以将JtaTransactionManager传递到你的LocalSessionFactoryBean中,以执行相同的默认值:

<bean id="sessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean">
    <property name="dataSource" ref="dataSource"/>
    <property name="mappingResources">
        <list>
            <value>org/springframework/samples/petclinic/hibernate/petclinic.hbm.xml</value>
        </list>
    </property>
    <property name="hibernateProperties">
        <value>
            hibernate.dialect=${hibernate.dialect}
        </value>
    </property>
    <property name="jtaTransactionManager" ref="txManager"/>
</bean>

<bean id="txManager" class="org.springframework.transaction.jta.JtaTransactionManager"/>

# 1.3.将资源与事务同步

如何创建不同的事务管理器以及它们如何链接到需要同步到事务的相关资源(例如DataSourceTransactionManager到一个 JDBCDataSourceHibernateTransactionManager到一个 Hibernate SessionFactory,等等)现在应该已经很清楚了。本节描述了应用程序代码如何(通过使用持久性 API(如 JDBC、 Hibernate 或 JPA)直接或间接地)确保正确地创建、重用和清理这些资源。本节还讨论了如何(可选地)通过相关的TransactionManager触发事务同步。

# 1.3.1.高层同步方法

首选的方法是使用 Spring 最高级别的基于模板的持久性集成 API,或者使用带有事务感知工厂 bean 或代理的本地 ORM API 来管理本地资源工厂。这些事务感知解决方案在内部处理资源的创建和重用、清理、资源的可选事务同步以及异常映射。因此,用户数据访问代码不必处理这些任务,而可以完全关注于非样板持久化逻辑。通常,你使用本机 ORM API,或者通过使用JdbcTemplate对 JDBC 访问采用模板方法。这些解决方案将在参考文档的后续部分中详细介绍。

# 1.3.2.低水平同步方法

诸如DataSourceUtils(对于 JDBC)、EntityManagerFactoryUtils(对于 JPA)、SessionFactoryUtils(对于 Hibernate)等类存在于较低的级别。当你希望应用程序代码直接处理本机持久性 API 的资源类型时,可以使用这些类来确保获得适当的 Spring 框架管理的实例,(可选地)同步事务,以及将过程中发生的异常正确地映射到一致的 API。

例如,在 JDBC 的情况下,你可以使用 Spring 的org.springframework.jdbc.datasource.DataSourceUtils类,而不是在getConnection()上调用getConnection()方法的传统 JDBC 方法,如下所示:

Connection conn = DataSourceUtils.getConnection(dataSource);

如果现有事务已经有一个与之同步(链接)的连接,则返回该实例。否则,方法调用将触发新连接的创建,该连接(可选地)与任何现有事务同步,并可用于该相同事务的后续重用。如前所述,任何SQLException都包装在 Spring 框架CannotGetJdbcConnectionException中,这是 Spring 框架的未检查DataAccessException类型的层次结构之一。这种方法为你提供了比从SQLException更容易获得的信息,并确保了跨数据库甚至跨不同持久性技术的可移植性。

该方法也可以在没有 Spring 事务管理的情况下工作(事务同步是可选的),因此无论是否使用 Spring 用于事务管理,都可以使用它。

当然,一旦使用了 Spring 的 JDBC 支持、 JPA 支持或 Hibernate 支持,通常不会使用DataSourceUtils或其他 helper 类,因为与直接使用相关的 API 相比,通过 Spring 抽象进行工作要快乐得多。例如,如果你使用 Spring JdbcTemplatejdbc.object包来简化对 JDBC 的使用,那么正确的连接检索将在幕后发生,并且你不需要编写任何特殊代码。

# 1.3.3.TransactionAwareDataSourceProxy

在最底层存在TransactionAwareDataSourceProxy类。这是目标DataSource的代理,该代理封装了目标DataSource以添加对 Spring 管理事务的感知。在这方面,它类似于事务 JNDIDataSource,由 爪哇 EE 服务器提供。

你几乎不需要或不想使用这个类,除非必须调用现有代码并传递标准的 JDBCDataSource接口实现。在这种情况下,该代码可能是可用的,但它参与了 Spring 管理的事务。你可以使用前面提到的更高级别的抽象来编写新代码。

# 1.4.声明式事务管理

Spring 大多数框架用户选择声明式事务管理。此选项对应用程序代码的影响最小,因此最符合
非侵入式轻量级容器的理想。

Spring 框架的声明性事务管理通过 Spring 面向方面的编程( AOP)而成为可能。然而,由于事务性方面的代码来自 Spring 框架分布并且可以以样板方式使用, AOP 概念通常不必被理解以有效地使用该代码。

Spring 框架的声明式事务管理类似于 EJB CMT,因为你可以指定事务行为(或缺少事务行为),直至单个方法级别。如果有必要,可以在事务上下文中进行setRollbackOnly()调用。这两种类型的事务管理的区别在于:

  • 与绑定到 JTA 的 EJB CMT 不同, Spring 框架的声明式事务管理可以在任何环境中工作。它可以通过使用 JDBC、 JPA 或 Hibernate 调整配置文件来处理 JTA 事务或本地事务。

  • 你可以将 Spring 框架声明式事务管理应用于任何类,而不仅仅是 EJB 等特殊类。

  • Spring 框架提供了声明性回滚规则,这是一个没有 EJB 等价物的特性。提供了对回滚规则的编程支持和声明式支持。

  • Spring 框架允许你通过使用 AOP 自定义事务行为。例如,你可以在事务回滚的情况下插入自定义行为。你还可以添加任意的建议以及事务性建议。使用 EJB CMT,你不能影响容器的事务管理,除非使用setRollbackOnly()

  • Spring 框架不支持跨远程调用传播事务上下文,就像高端应用服务器所做的那样。如果你需要此功能,我们建议你使用 EJB。然而,在使用这样的特性之前要仔细考虑,因为通常情况下,人们不希望事务跨越远程调用。

回滚规则的概念很重要。它们允许你指定哪些异常(和 throwable)应该导致自动回滚。你可以在配置中,而不是在 爪哇 代码中,以声明式的方式指定此项。因此,尽管你仍然可以在TransactionStatus对象上调用setRollbackOnly()来回滚当前事务,但大多数情况下,你可以指定一条规则,即MyApplicationException必须始终导致回滚。此选项的显著优点是业务对象不依赖于事务基础设施。例如,它们通常不需要导入 Spring 事务 API 或其他 Spring API。

虽然 EJB 容器的默认行为会在系统异常(通常是运行时异常)上自动回滚事务,但 EJB CMT 不会在应用程序异常(即除java.rmi.RemoteException以外的检查异常)上自动回滚事务。虽然 Spring 声明性事务管理的默认行为遵循 EJB 约定(回滚仅在未检查的异常情况下是自动的),但定制这种行为通常是有用的。

# 1.4.1.理解 Spring 框架的声明式事务实现

仅仅告诉你使用@Transactional注释对类进行注释,将@EnableTransactionManagement添加到配置中,并期望你了解它是如何工作的,这是不够的。为了提供更深入的理解,本节将在与事务相关的问题的上下文中解释 Spring 框架的声明式事务基础结构的内部工作方式。

关于 Spring 框架的声明性事务支持,需要理解的最重要的概念是启用了via AOP proxies,并且事务建议是由元数据(目前是基于 XML 或注释)驱动的。 AOP 与事务性元数据的组合产生了 AOP 代理,该代理使用TransactionInterceptor并结合适当的TransactionManager实现来驱动围绕方法调用的事务。

Spring AOP 在the AOP section中涵盖。

Spring Framework 的TransactionInterceptor为命令式和反应式编程模型提供事务管理。拦截器通过检查方法返回类型来检测所需的事务管理风格。返回诸如Publisher或 Kotlin Flow之类的反应性类型的方法(或其中的一个子类型)符合进行反应性事务管理的条件。包括void在内的所有其他返回类型都使用强制事务管理的代码路径。

事务管理风格会影响所需的事务管理器。命令式事务需要PlatformTransactionManager,而反应式事务使用ReactiveTransactionManager实现。

@Transactional通常与PlatformTransactionManager管理的线程绑定事务一起工作,将事务暴露给
当前执行线程中的所有数据访问操作。注意:这将不是传播到方法内新启动的线程


ReactiveTransactionManager管理的反应式事务使用反应器上下文
而不是线程本地属性。因此,所有参与的数据访问
操作都需要在相同的反应性管道中的相同的反应器上下文中执行。

下图显示了在事务代理上调用方法的概念视图:

tx

# 1.4.2.声明式事务实现示例

考虑下面的接口及其附带的实现。这个示例使用FooBar类作为占位符,这样你就可以专注于事务使用,而不必关注特定的域模型。对于本例的目的,DefaultFooService类在每个实现的方法的主体中抛出UnsupportedOperationException实例的事实是好的。这种行为允许你看到正在创建的事务,然后响应UnsupportedOperationException实例进行回滚。下面的清单显示了FooService接口:

爪哇

// the service interface that we want to make transactional

package x.y.service;

public interface FooService {

    Foo getFoo(String fooName);

    Foo getFoo(String fooName, String barName);

    void insertFoo(Foo foo);

    void updateFoo(Foo foo);

}

Kotlin

// the service interface that we want to make transactional

package x.y.service

interface FooService {

    fun getFoo(fooName: String): Foo

    fun getFoo(fooName: String, barName: String): Foo

    fun insertFoo(foo: Foo)

    fun updateFoo(foo: Foo)
}

下面的示例展示了前面接口的一个实现:

爪哇

package x.y.service;

public class DefaultFooService implements FooService {

    @Override
    public Foo getFoo(String fooName) {
        // ...
    }

    @Override
    public Foo getFoo(String fooName, String barName) {
        // ...
    }

    @Override
    public void insertFoo(Foo foo) {
        // ...
    }

    @Override
    public void updateFoo(Foo foo) {
        // ...
    }
}

Kotlin

package x.y.service

class DefaultFooService : FooService {

    override fun getFoo(fooName: String): Foo {
        // ...
    }

    override fun getFoo(fooName: String, barName: String): Foo {
        // ...
    }

    override fun insertFoo(foo: Foo) {
        // ...
    }

    override fun updateFoo(foo: Foo) {
        // ...
    }
}

假设FooService接口的前两个方法getFoo(String)getFoo(String, String)必须在具有只读语义的事务的上下文中运行,而其他方法insertFoo(Foo)updateFoo(Foo)必须在具有读写语义的事务的上下文中运行。下面的几段将详细介绍以下配置:

<!-- from the file 'context.xml' -->
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/tx
        https://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- this is the service object that we want to make transactional -->
    <bean id="fooService" class="x.y.service.DefaultFooService"/>

    <!-- the transactional advice (what 'happens'; see the <aop:advisor/> bean below) -->
    <tx:advice id="txAdvice" transaction-manager="txManager">
        <!-- the transactional semantics... -->
        <tx:attributes>
            <!-- all methods starting with 'get' are read-only -->
            <tx:method name="get*" read-only="true"/>
            <!-- other methods use the default transaction settings (see below) -->
            <tx:method name="*"/>
        </tx:attributes>
    </tx:advice>

    <!-- ensure that the above transactional advice runs for any execution
        of an operation defined by the FooService interface -->
    <aop:config>
        <aop:pointcut id="fooServiceOperation" expression="execution(* x.y.service.FooService.*(..))"/>
        <aop:advisor advice-ref="txAdvice" pointcut-ref="fooServiceOperation"/>
    </aop:config>

    <!-- don't forget the DataSource -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="oracle.jdbc.driver.OracleDriver"/>
        <property name="url" value="jdbc:oracle:thin:@rj-t42:1521:elvis"/>
        <property name="username" value="scott"/>
        <property name="password" value="tiger"/>
    </bean>

    <!-- similarly, don't forget the TransactionManager -->
    <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!-- other <bean/> definitions here -->

</beans>

检查前面的配置。它假定你希望创建一个事务性的服务对象fooService Bean。要应用的事务语义封装在<tx:advice/>定义中。<tx:advice/>定义为“所有以get开头的方法都将在只读事务的上下文中运行,所有其他方法都将使用默认事务语义运行”。<tx:advice/>标记的transaction-manager属性被设置为将用于驱动事务的TransactionManager Bean 的名称(在本例中,txManager Bean)。

如果要
中的TransactionManager线的 Bean 名为transactionManager,则可以省略事务通知transaction-manager中的transactionManager属性。如果要连接的TransactionManager Bean 中的
有任何其他名称,则必须显式地使用transaction-manager属性,如前面的示例所示。

<aop:config/>定义确保由txAdvice Bean 定义的事务通知在程序的适当位置运行。首先,定义一个切入点,该切入点与FooService接口(fooServiceOperation)中定义的任何操作的执行相匹配。然后使用 advisor 将切入点与txAdvice关联起来。结果表明,在执行fooServiceOperation时,运行由txAdvice定义的通知。

<aop:pointcut/>元素中定义的表达式是 AspectJ 切入点表达式。有关 Spring 中的切入点表达式的更多详细信息,请参见the AOP section

一个常见的需求是使整个服务层具有事务性。实现此目的的最佳方法是更改切入点表达式,以匹配服务层中的任何操作。下面的示例展示了如何做到这一点:

<aop:config>
    <aop:pointcut id="fooServiceMethods" expression="execution(* x.y.service.*.*(..))"/>
    <aop:advisor advice-ref="txAdvice" pointcut-ref="fooServiceMethods"/>
</aop:config>
在前面的示例中,假设你的所有服务接口都是在x.y.service包中定义的
。有关更多详细信息,请参见the AOP section

既然我们已经分析了配置,那么你可能会问自己:“所有这些配置实际上做了什么?”

前面显示的配置用于围绕根据fooService Bean 定义创建的对象创建事务代理。该代理配置了事务通知,以便当在代理上调用适当的方法时,根据与该方法关联的事务配置,启动、挂起、标记为只读等等。考虑以下测试驱动前面显示的配置的程序:

爪哇

public final class Boot {

    public static void main(final String[] args) throws Exception {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("context.xml");
        FooService fooService = ctx.getBean(FooService.class);
        fooService.insertFoo(new Foo());
    }
}

Kotlin

import org.springframework.beans.factory.getBean

fun main() {
    val ctx = ClassPathXmlApplicationContext("context.xml")
    val fooService = ctx.getBean<FooService>("fooService")
    fooService.insertFoo(Foo())
}

运行前一个程序的输出应该类似于以下内容(为清楚起见,对DefaultFooService类的insertFoo(..)方法抛出的UnsupportedOperationException的 log4j 输出和堆栈跟踪进行了截断):

<!-- the Spring container is starting up... -->
[AspectJInvocationContextExposingAdvisorAutoProxyCreator] - Creating implicit proxy for bean 'fooService' with 0 common interceptors and 1 specific interceptors

<!-- the DefaultFooService is actually proxied -->
[JdkDynamicAopProxy] - Creating JDK dynamic proxy for [x.y.service.DefaultFooService]

<!-- ... the insertFoo(..) method is now being invoked on the proxy -->
[TransactionInterceptor] - Getting transaction for x.y.service.FooService.insertFoo

<!-- the transactional advice kicks in here... -->
[DataSourceTransactionManager] - Creating new transaction with name [x.y.service.FooService.insertFoo]
[DataSourceTransactionManager] - Acquired Connection [[email protected]] for JDBC transaction

<!-- the insertFoo(..) method from DefaultFooService throws an exception... -->
[RuleBasedTransactionAttribute] - Applying rules to determine whether transaction should rollback on java.lang.UnsupportedOperationException
[TransactionInterceptor] - Invoking rollback for transaction on x.y.service.FooService.insertFoo due to throwable [java.lang.UnsupportedOperationException]

<!-- and the transaction is rolled back (by default, RuntimeException instances cause rollback) -->
[DataSourceTransactionManager] - Rolling back JDBC transaction on Connection [[email protected]]
[DataSourceTransactionManager] - Releasing JDBC Connection after transaction
[DataSourceUtils] - Returning JDBC Connection to DataSource

Exception in thread "main" java.lang.UnsupportedOperationException at x.y.service.DefaultFooService.insertFoo(DefaultFooService.java:14)
<!-- AOP infrastructure stack trace elements removed for clarity -->
at $Proxy0.insertFoo(Unknown Source)
at Boot.main(Boot.java:11)

要使用反应式事务管理,代码必须使用反应式类型。

Spring 框架使用ReactiveAdapterRegistry来确定方法
的返回类型是否是反应性的。

下面的清单显示了以前使用的FooService的修改版本,但这次代码使用了反应类型:

爪哇

// the reactive service interface that we want to make transactional

package x.y.service;

public interface FooService {

    Flux<Foo> getFoo(String fooName);

    Publisher<Foo> getFoo(String fooName, String barName);

    Mono<Void> insertFoo(Foo foo);

    Mono<Void> updateFoo(Foo foo);

}

Kotlin

// the reactive service interface that we want to make transactional

package x.y.service

interface FooService {

    fun getFoo(fooName: String): Flow<Foo>

    fun getFoo(fooName: String, barName: String): Publisher<Foo>

    fun insertFoo(foo: Foo) : Mono<Void>

    fun updateFoo(foo: Foo) : Mono<Void>
}

下面的示例展示了前面接口的一个实现:

爪哇

package x.y.service;

public class DefaultFooService implements FooService {

    @Override
    public Flux<Foo> getFoo(String fooName) {
        // ...
    }

    @Override
    public Publisher<Foo> getFoo(String fooName, String barName) {
        // ...
    }

    @Override
    public Mono<Void> insertFoo(Foo foo) {
        // ...
    }

    @Override
    public Mono<Void> updateFoo(Foo foo) {
        // ...
    }
}

Kotlin

package x.y.service

class DefaultFooService : FooService {

    override fun getFoo(fooName: String): Flow<Foo> {
        // ...
    }

    override fun getFoo(fooName: String, barName: String): Publisher<Foo> {
        // ...
    }

    override fun insertFoo(foo: Foo): Mono<Void> {
        // ...
    }

    override fun updateFoo(foo: Foo): Mono<Void> {
        // ...
    }
}

对于事务边界和事务属性定义,命令式事务管理和反应式事务管理共享相同的语义。命令式事务和反应式事务之间的主要区别在于后者的延迟性。TransactionInterceptor使用事务操作符装饰返回的反应类型,以开始并清理事务。因此,调用事务性反应方法将实际事务管理推迟到激活反应类型处理的订阅类型。

反应式事务管理的另一个方面涉及数据转义,这是编程模型的自然结果。

命令式事务的方法返回值在方法成功终止时从事务性方法返回,以便部分计算的结果不会逃脱方法闭包。

Active Transaction 方法返回一个 Active 包装器类型,该类型表示一个计算序列以及开始和完成计算的承诺。

Publisher可以在事务正在进行但不一定完成时发出数据。因此,依赖于成功完成整个事务的方法需要确保调用代码中的完成和缓冲结果。

# 1.4.3.回滚声明性事务

上一节概述了如何在应用程序中以声明性的方式为类(通常是服务层类)指定事务设置的基础知识。本节介绍如何以简单的声明式方式控制事务的回滚。

向 Spring 框架的事务基础结构指示要回滚事务工作的推荐方法是,从当前在事务上下文中执行的代码中抛出Exception。 Spring 框架的事务基础设施代码捕获任何未处理的Exception,因为它在调用堆栈中冒泡,并决定是否将事务标记为回滚。

在其默认配置中, Spring 框架的事务基础设施代码仅在运行时未检查异常的情况下标记用于回滚的事务。也就是说,当抛出的异常是RuntimeException的实例或子类时。(默认情况下,Error实例也会导致回滚)。从事务方法抛出的已检查异常不会在默认配置中导致回滚。

你可以准确地配置哪些Exception类型标记了一个用于回滚的事务,包括选中的异常。下面的 XML 片段演示了如何为选中的、特定于应用程序的Exception类型配置回滚:

<tx:advice id="txAdvice" transaction-manager="txManager">
    <tx:attributes>
    <tx:method name="get*" read-only="true" rollback-for="NoProductInStockException"/>
    <tx:method name="*"/>
    </tx:attributes>
</tx:advice>

如果你不希望在抛出异常时回滚事务,也可以指定“没有回滚规则”。下面的示例告诉 Spring 框架的事务基础结构,即使面对未处理的InstrumentNotFoundException,也要提交伴随的事务:

<tx:advice id="txAdvice">
    <tx:attributes>
    <tx:method name="updateStock" no-rollback-for="InstrumentNotFoundException"/>
    <tx:method name="*"/>
    </tx:attributes>
</tx:advice>

Spring 框架的事务基础设施捕捉到异常并参考配置的回滚规则以确定是否将事务标记为回滚时,最强的匹配规则胜出。因此,在以下配置的情况下,除InstrumentNotFoundException以外的任何异常都会导致附带事务的回滚:

<tx:advice id="txAdvice">
    <tx:attributes>
    <tx:method name="*" rollback-for="Throwable" no-rollback-for="InstrumentNotFoundException"/>
    </tx:attributes>
</tx:advice>

你还可以通过编程的方式指示所需的回滚。尽管很简单,但这个过程是非常具有侵入性的,并且将你的代码与 Spring 框架的事务基础结构紧密地结合在一起。下面的示例展示了如何以编程方式指示所需的回滚:

爪哇

public void resolvePosition() {
    try {
        // some business logic...
    } catch (NoProductInStockException ex) {
        // trigger rollback programmatically
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    }
}

Kotlin

fun resolvePosition() {
    try {
        // some business logic...
    } catch (ex: NoProductInStockException) {
        // trigger rollback programmatically
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    }
}

如果可能的话,我们强烈建议你使用声明式方法进行回滚。如果你绝对需要编程回滚,它是可用的,但是它的使用与实现一个干净的基于 POJO 的体系结构是背道而驰的。

# 1.4.4.为不同的 bean 配置不同的事务语义

考虑这样的场景:你有许多服务层对象,并且希望对每个对象应用完全不同的事务配置。你可以通过使用不同的pointcutadvice-ref属性值来定义不同的<aop:advisor/>元素。

作为比较,首先假设你的所有服务层类都是在根x.y.service包中定义的。要使在该包(或子包)中定义的类实例以及名称以Service结尾的所有 bean 具有默认的事务配置,你可以编写以下内容:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/tx
        https://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">

    <aop:config>

        <aop:pointcut id="serviceOperation"
                expression="execution(* x.y.service..*Service.*(..))"/>

        <aop:advisor pointcut-ref="serviceOperation" advice-ref="txAdvice"/>

    </aop:config>

    <!-- these two beans will be transactional... -->
    <bean id="fooService" class="x.y.service.DefaultFooService"/>
    <bean id="barService" class="x.y.service.extras.SimpleBarService"/>

    <!-- ... and these two beans won't -->
    <bean id="anotherService" class="org.xyz.SomeService"/> <!-- (not in the right package) -->
    <bean id="barManager" class="x.y.service.SimpleBarManager"/> <!-- (doesn't end in 'Service') -->

    <tx:advice id="txAdvice">
        <tx:attributes>
            <tx:method name="get*" read-only="true"/>
            <tx:method name="*"/>
        </tx:attributes>
    </tx:advice>

    <!-- other transaction infrastructure beans such as a TransactionManager omitted... -->

</beans>

下面的示例展示了如何配置具有完全不同的事务设置的两个不同的 bean:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/tx
        https://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">

    <aop:config>

        <aop:pointcut id="defaultServiceOperation"
                expression="execution(* x.y.service.*Service.*(..))"/>

        <aop:pointcut id="noTxServiceOperation"
                expression="execution(* x.y.service.ddl.DefaultDdlManager.*(..))"/>

        <aop:advisor pointcut-ref="defaultServiceOperation" advice-ref="defaultTxAdvice"/>

        <aop:advisor pointcut-ref="noTxServiceOperation" advice-ref="noTxAdvice"/>

    </aop:config>

    <!-- this bean will be transactional (see the 'defaultServiceOperation' pointcut) -->
    <bean id="fooService" class="x.y.service.DefaultFooService"/>

    <!-- this bean will also be transactional, but with totally different transactional settings -->
    <bean id="anotherFooService" class="x.y.service.ddl.DefaultDdlManager"/>

    <tx:advice id="defaultTxAdvice">
        <tx:attributes>
            <tx:method name="get*" read-only="true"/>
            <tx:method name="*"/>
        </tx:attributes>
    </tx:advice>

    <tx:advice id="noTxAdvice">
        <tx:attributes>
            <tx:method name="*" propagation="NEVER"/>
        </tx:attributes>
    </tx:advice>

    <!-- other transaction infrastructure beans such as a TransactionManager omitted... -->

</beans>

# 1.4.5.<tx:advice/>设置

本节总结了你可以通过使用<tx:advice/>标记来指定的各种事务设置。默认的<tx:advice/>设置是:

  • 传播设定REQUIRED.

  • 隔离级别为DEFAULT.

  • 事务是可读写的。

  • 事务超时默认为基础事务系统的默认超时,如果不支持超时,则无超时。

  • 任何RuntimeException都会触发回滚,而任何选中的Exception都不会。

你可以更改这些默认设置。下表总结了嵌套在<tx:advice/><tx:attributes/>标记中的<tx:method/>标记的各种属性:

Attribute Required? Default 说明
name Yes 将事务属性与之关联的方法名。可以使用
通配符(*)字符将相同的事务属性
设置与许多方法关联(例如,get*handle*on*Event,以及
forth)。
propagation No REQUIRED 事务传播行为。
isolation No DEFAULT 事务隔离级别。仅适用于REQUIREDREQUIRES_NEW的传播设置。
timeout No -1 事务超时(秒)。仅适用于传播REQUIREDREQUIRES_NEW
read-only No false 读写与只读事务。仅适用于REQUIREDREQUIRES_NEW
rollback-for No 触发回滚的Exception实例的逗号分隔列表。例如,com.foo.MyBusinessException,ServletException
no-rollback-for No 不触发回滚的Exception实例的逗号分隔列表。例如,com.foo.MyBusinessException,ServletException

# 1.4.6.使用@Transactional

除了事务配置的基于 XML 的声明式方法之外,还可以使用基于注释的方法。直接在 爪哇 源代码中声明事务语义会使声明更接近受影响的代码。不存在太大的过度耦合的危险,因为本来打算用于交易的代码几乎总是以这种方式部署的。

标准的javax.transaction.Transactional注释也被支持为 Spring 自己的注释的
插入替换。有关更多详细信息,请参阅 JTA1.2 文档

使用@Transactional注释所提供的易用性最好用一个示例来说明,下面的文本对此进行了解释。考虑以下类定义:

爪哇

// the service class that we want to make transactional
@Transactional
public class DefaultFooService implements FooService {

    @Override
    public Foo getFoo(String fooName) {
        // ...
    }

    @Override
    public Foo getFoo(String fooName, String barName) {
        // ...
    }

    @Override
    public void insertFoo(Foo foo) {
        // ...
    }

    @Override
    public void updateFoo(Foo foo) {
        // ...
    }
}

Kotlin

// the service class that we want to make transactional
@Transactional
class DefaultFooService : FooService {

    override fun getFoo(fooName: String): Foo {
        // ...
    }

    override fun getFoo(fooName: String, barName: String): Foo {
        // ...
    }

    override fun insertFoo(foo: Foo) {
        // ...
    }

    override fun updateFoo(foo: Foo) {
        // ...
    }
}

在类级别上使用,如上面所述,该注释表示声明类的所有方法(以及其子类)的默认值。或者,每个方法都可以单独注释。有关方法 Spring 考虑事务性的更多详细信息,请参见[Method Visibility and@Transactional](#Transaction-Declarative-Annotations-Method-Visibility)。请注意,类级注释不适用于类层次结构中的祖先类;在这种情况下,继承的方法需要在本地重新声明才能参与子类级注释。

当像上面这样的 POJO 类在 Spring 上下文中被定义为 Bean 时,可以通过@EnableTransactionManagement类中的@Configuration注释使 Bean 实例具有事务性。有关详细信息,请参见javadoc (opens new window)

在 XML 配置中,<tx:annotation-driven/>标记提供了类似的便利:

<!-- from the file 'context.xml' -->
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/tx
        https://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- this is the service object that we want to make transactional -->
    <bean id="fooService" class="x.y.service.DefaultFooService"/>

    <!-- enable the configuration of transactional behavior based on annotations -->
    <!-- a TransactionManager is still required -->
    <tx:annotation-driven transaction-manager="txManager"/> (1)

    <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <!-- (this dependency is defined somewhere else) -->
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!-- other <bean/> definitions here -->

</beans>
1 使 Bean 实例具有事务性的行。
如果要连接的TransactionManager的 Bean 名称为transactionManager,则可以在<tx:annotation-driven/>标记中省略transaction-manager属性。如果要依赖注入的TransactionManager Bean 中的
有任何其他名称,则必须使用transaction-manager属性,如在
前面的示例中所示。

反应式事务方法使用反应式返回类型,而不是命令式编程安排,如下所示:

爪哇

// the reactive service class that we want to make transactional
@Transactional
public class DefaultFooService implements FooService {

    @Override
    public Publisher<Foo> getFoo(String fooName) {
        // ...
    }

    @Override
    public Mono<Foo> getFoo(String fooName, String barName) {
        // ...
    }

    @Override
    public Mono<Void> insertFoo(Foo foo) {
        // ...
    }

    @Override
    public Mono<Void> updateFoo(Foo foo) {
        // ...
    }
}

Kotlin

// the reactive service class that we want to make transactional
@Transactional
class DefaultFooService : FooService {

    override fun getFoo(fooName: String): Flow<Foo> {
        // ...
    }

    override fun getFoo(fooName: String, barName: String): Mono<Foo> {
        // ...
    }

    override fun insertFoo(foo: Foo): Mono<Void> {
        // ...
    }

    override fun updateFoo(foo: Foo): Mono<Void> {
        // ...
    }
}

请注意,对于返回的Publisher有关于无功流抵消信号的特殊考虑因素。有关更多详细信息,请参见“使用 TransactionOperator”下的取消信号小节。

方法可见性和@Transactional

当你使用具有 Spring 的标准配置的事务代理时,你应该只对具有
可见性的方法应用@Transactional注释。如果你使用
注释protectedprivate或使用@Transactional注释的包-可见方法,则不会引发错误,但是注释的方法不显示已配置的
事务设置。如果你需要对非公共方法进行注释,请考虑下面一段中关于基于类的代理的提示,或者考虑使用 AspectJ Compile-Time 或Load-Time Weaving(稍后将进行说明)。在类中使用时,protected
包-visible 方法也可以通过
注册一个自定义的transactionAttributeSource Bean 来使基于类的代理具有事务性,就像下面的示例一样。
注意,在基于接口的代理中,事务方法必须始终是public并在代理接口中定义。

<br/>/**<br/> * Register a custom AnnotationTransactionAttributeSource with the<br/> * publicMethodsOnly flag set to false to enable support for<br/> * protected and package-private @Transactional methods in<br/> * class-based proxies.<br/> *<br/> * @see ProxyTransactionManagementConfiguration#transactionAttributeSource()<br/> */<br/>@Bean<br/>TransactionAttributeSource transactionAttributeSource() {<br/> return new AnnotationTransactionAttributeSource(false);<br/>}<br/>

Spring TestContext Framework通过
默认支持非私有@Transactional测试方法。参见事务管理中的测试
章节中的示例。

你可以将@Transactional注释应用于接口定义、接口上的方法、类定义或类上的方法。然而,仅仅存在@Transactional注释并不足以激活事务行为。@Transactional注释仅仅是元数据,它可以被一些运行时基础设施(@Transactional)使用,并且可以使用元数据来配置具有事务行为的适当 bean。在前面的示例中,<tx:annotation-driven/>元素在事务行为上进行切换。

Spring 团队建议你只使用@Transactional注释来注释具体的类(以及
具体类的方法),而不是注释接口,
你当然可以将@Transactional注释放在接口(或接口
方法)上,但是,只有当你使用基于接口的
代理时,这才能正常工作。爪哇 注释不是从接口继承而来的事实意味着,如果你使用基于类的代理(proxy-target-class="true")或基于编织的
方面(mode="aspectj"),代理
和编织基础设施将无法识别事务设置,并且该对象不会包装在事务代理中。
在代理模式(这是默认模式)中,只有通过
代理进入的外部方法调用才会被拦截。这意味着自我调用(实际上,在
中的一个方法调用目标对象的另一个方法)在运行时不会导致实际的
事务,即使调用的方法被标记为@Transactional。此外,
代理必须完全初始化以提供预期的行为,因此你不应该在初始化代码中依赖
这个特性——例如,在@PostConstruct方法中。

如果你希望使用事务来包装自调用,请考虑使用 AspectJ 模式(参见下表中的mode属性)。在这种情况下,首先不存在代理。相反,目标类被编织(也就是说,它的字节码被修改)以支持在任何类型的方法上的@Transactional运行时行为。

XML Attribute Annotation Attribute Default 说明
transaction-manager N/A (see TransactionManagementConfigurer (opens new window) javadoc) transactionManager 要使用的事务管理器的名称。仅当事务
管理器的名称不是transactionManager时才需要,如前面的示例所示。
mode mode proxy 默认模式(proxy)处理要通过使用 Spring 的 AOP
框架来处理注释的 bean(遵循代理语义,如前所述,应用于仅通过代理进入的方法调用
)。替代模式(aspectj)使用 Spring 的 AspectJ 事务方面来编织
受影响的类,修改目标类
字节码以应用于任何类型的方法调用。AspectJ Weaving 在 Classpath 中需要spring-aspects.jar以及启用加载时编织(或编译时
编织)。(有关如何设置加载时编织的详细信息,请参见Spring configuration
proxy-target-class proxyTargetClass false 仅适用于proxy模式。控制为使用@Transactional注释的类创建的事务代理类型
。如果proxy-target-class属性设置为true,则创建基于类的代理。
如果proxy-target-classfalse,或者如果省略了该属性,则创建标准的基于接口的代理。(有关不同代理类型的详细检查,请参见代理机制
order order Ordered.LOWEST_PRECEDENCE 定义应用于使用@Transactional注释的 bean 的事务通知的顺序。(有关与 AOP 通知的排序有关的规则的更多信息,请参见。)没有指定的排序意味着 AOP 子系统确定通知的顺序。
处理@Transactional注释的默认通知模式是proxy
,这仅允许通过代理拦截调用。在
相同的类内的本地调用不能以这种方式被拦截。对于更高级的拦截模式,
可以考虑结合编译时或加载时编织切换到aspectj模式。
proxy-target-class属性控制为使用@Transactional注释的类创建的事务代理类型
。如果proxy-target-class设置为true,则创建基于类的代理。如果proxy-target-classfalse,或者省略了该属性,则创建标准的基于接口的 JDK
代理。(有关不同代理类型的讨论,请参见代理机制
@EnableTransactionManagement<tx:annotation-driven/>仅在定义它们的相同应用程序上下文中的 bean 上查找@Transactional
这意味着,如果将注释驱动的配置放在WebApplicationContext中,用于DispatcherServlet,它仅在你的控制器
中检查@Transactionalbean,而不是在你的服务中。有关更多信息,请参见MVC

在计算方法的事务设置时,派生最多的位置优先。在下面的示例中,DefaultFooService类在类级别上使用只读事务的设置进行注释,但是在同一个类中的@Transactional方法上的@Transactional注释优先于在类级别上定义的事务设置。

爪哇

@Transactional(readOnly = true)
public class DefaultFooService implements FooService {

    public Foo getFoo(String fooName) {
        // ...
    }

    // these settings have precedence for this method
    @Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW)
    public void updateFoo(Foo foo) {
        // ...
    }
}

Kotlin

@Transactional(readOnly = true)
class DefaultFooService : FooService {

    override fun getFoo(fooName: String): Foo {
        // ...
    }

    // these settings have precedence for this method
    @Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW)
    override fun updateFoo(foo: Foo) {
        // ...
    }
}
# @Transactional设置

@Transactional注释是元数据,它指定接口、类或方法必须具有事务语义(例如,“在调用该方法时启动一个全新的只读事务,挂起任何现有事务”)。默认的@Transactional设置如下:

  • 传播设置为PROPAGATION_REQUIRED.

  • 隔离级别为ISOLATION_DEFAULT.

  • 事务是可读写的。

  • 事务超时默认为基础事务系统的默认超时,如果不支持超时,则为零。

  • 任何RuntimeException都会触发回滚,而任何选中的Exception都不会。

你可以更改这些默认设置。下表总结了@Transactional注释的各种属性:

Property Type 说明
value String 指定要使用的事务管理器的可选限定符。
propagation enum: Propagation 可选的传播设置。
isolation enum: Isolation 可选隔离级别。仅适用于REQUIREDREQUIRES_NEW的传播值。
timeout int (in seconds of granularity) 可选事务超时。仅适用于REQUIREDREQUIRES_NEW的传播值。
readOnly boolean 读写与只读事务。仅适用于REQUIREDREQUIRES_NEW的值。
rollbackFor Array of Class objects, which must be derived from Throwable. 必须引起回滚的异常类的可选数组.
rollbackForClassName Array of class names. The classes must be derived from Throwable. 必须引起回滚的异常类名称的可选数组。
noRollbackFor Array of Class objects, which must be derived from Throwable. 不能导致回滚的异常类的可选数组.
noRollbackForClassName Array of String class names, which must be derived from Throwable. 不能导致回滚的异常类名称的可选数组。
label Array of String labels to add an expressive description to the transaction. 标签可以由事务管理器进行评估,以将
实现特定的行为与实际事务相关联。

目前,你无法对事务的名称进行显式控制,其中“name”是指事务监视器(如果适用)(例如,WebLogic 的事务监视器)和日志输出中出现的事务名称。对于声明式事务,事务名称始终是完全限定类名称 +.+ 事务建议类的方法名称。例如,如果BusinessService类的handlePayment(..)方法启动了一个事务,那么事务的名称将是:com.example.BusinessService.handlePayment

# 具有@Transactional的多个事务管理器

Spring 大多数应用程序只需要一个事务管理器,但是可能存在希望在单个应用程序中有多个独立事务管理器的情况。你可以使用valuetransactionManager注释的属性来指定要使用的TransactionManager的标识。这可以是 Bean 名称,也可以是事务管理器 Bean 的限定符值。例如,使用限定符表示法,你可以在应用程序上下文中将以下 爪哇 代码与以下事务管理器 Bean 声明结合起来:

爪哇

public class TransactionalService {

    @Transactional("order")
    public void setSomething(String name) { ... }

    @Transactional("account")
    public void doSomething() { ... }

    @Transactional("reactive-account")
    public Mono<Void> doSomethingReactive() { ... }
}

Kotlin

class TransactionalService {

    @Transactional("order")
    fun setSomething(name: String) {
        // ...
    }

    @Transactional("account")
    fun doSomething() {
        // ...
    }

    @Transactional("reactive-account")
    fun doSomethingReactive(): Mono<Void> {
        // ...
    }
}

下面的清单显示了 Bean 项声明:

<tx:annotation-driven/>

    <bean id="transactionManager1" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        ...
        <qualifier value="order"/>
    </bean>

    <bean id="transactionManager2" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        ...
        <qualifier value="account"/>
    </bean>

    <bean id="transactionManager3" class="org.springframework.data.r2dbc.connectionfactory.R2dbcTransactionManager">
        ...
        <qualifier value="reactive-account"/>
    </bean>

在这种情况下,TransactionalService上的各个方法在单独的事务管理器下运行,由orderaccountreactive-account限定符区分。如果没有找到特别限定的TransactionManager Bean,则仍然使用默认的<tx:annotation-driven>目标名称transactionManager

# 自定义合成注释

如果你发现在许多不同的方法上重复使用与@Transactional相同的属性,Spring’s meta-annotation support允许你为特定的用例定义自定义组合注释。例如,考虑以下注释定义:

爪哇

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Transactional(transactionManager = "order", label = "causal-consistency")
public @interface OrderTx {
}

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Transactional(transactionManager = "account", label = "retryable")
public @interface AccountTx {
}

Kotlin

@Target(AnnotationTarget.FUNCTION, AnnotationTarget.TYPE)
@Retention(AnnotationRetention.RUNTIME)
@Transactional(transactionManager = "order", label = ["causal-consistency"])
annotation class OrderTx

@Target(AnnotationTarget.FUNCTION, AnnotationTarget.TYPE)
@Retention(AnnotationRetention.RUNTIME)
@Transactional(transactionManager = "account", label = ["retryable"])
annotation class AccountTx

前面的注释让我们将上一节的示例写如下:

爪哇

public class TransactionalService {

    @OrderTx
    public void setSomething(String name) {
        // ...
    }

    @AccountTx
    public void doSomething() {
        // ...
    }
}

Kotlin

class TransactionalService {

    @OrderTx
    fun setSomething(name: String) {
        // ...
    }

    @AccountTx
    fun doSomething() {
        // ...
    }
}

在前面的示例中,我们使用语法来定义事务管理器限定符和事务标签,但也可以包括传播行为、回滚规则、超时和其他功能。

# 1.4.7.事务传播

本节描述 Spring 中事务传播的一些语义。请注意,本节不是事务传播的适当介绍。相反,它详细介绍了 Spring 中有关事务传播的一些语义。

在 Spring-管理事务中,要注意物理事务和逻辑事务之间的区别,以及传播设置如何应用于这种区别。

# 理解PROPAGATION_REQUIRED

需要 TX 道具

PROPAGATION_REQUIRED强制执行一个物理事务,如果当前范围还不存在事务,则在本地执行,或者参与为更大范围定义的现有“外部”事务。这是相同线程内的公共调用堆栈安排中的一个很好的默认设置(例如,一个服务 facade,它将委托给几个存储库方法,在这些存储库中,所有底层资源都必须参与服务级事务)。

默认情况下,参与事务将加入外部作用域的特性,
静默地忽略本地隔离级别、超时值,或只读标志(如果有的话)。
如果你希望在参与
具有不同隔离级别的现有事务时拒绝隔离级别声明,请考虑在事务validateExistingTransactions管理器上将true标志切换为true。这种不宽松的模式还
拒绝只读不匹配(即,试图参与
只读外部作用域的内部读写事务)。

当传播设置为PROPAGATION_REQUIRED时,将为应用该设置的每个方法创建一个逻辑事务范围。每个这样的逻辑事务作用域可以单独确定仅回滚状态,外部事务作用域在逻辑上独立于内部事务作用域。在标准PROPAGATION_REQUIRED行为的情况下,所有这些作用域都映射到相同的物理事务。因此,内部事务范围中设置的只回滚标记确实会影响外部事务实际提交的机会。

但是,在内部事务作用域设置仅回滚标记的情况下,外部事务本身并未决定回滚,因此回滚(由内部事务作用域静默触发)是意外的。在该点抛出相应的UnexpectedRollbackException。这是一种预期的行为,这样事务的调用者就永远不会被误导,以为提交是在它实际上没有执行的时候执行的。因此,如果内部事务(外部调用方不知道该事务)静默地将事务标记为只回滚,则外部调用方仍然调用 commit。外部调用者需要接收UnexpectedRollbackException,以清楚地表明执行了回滚。

# 理解PROPAGATION_REQUIRES_NEW

TX Prop 需要新的

PROPAGATION_REQUIRES_NEW,与PROPAGATION_REQUIRED相反,总是对每个受影响的事务范围使用独立的物理事务,而不是参与外部范围的现有事务。在这种安排中,底层资源事务是不同的,因此可以独立地提交或回滚,外部事务不受内部事务的回滚状态的影响,内部事务的锁在完成后立即释放。这种独立的内部事务还可以声明自己的隔离级别、超时和只读设置,而不会继承外部事务的特征。

# 理解PROPAGATION_NESTED

PROPAGATION_NESTED使用一个具有多个保存点的单个物理事务,它可以回滚到这些保存点。这样的部分回滚让内部事务作用域触发其作用域的回滚,外部事务能够继续物理事务,尽管有些操作已经回滚。该设置通常映射到 JDBC 保存点上,因此它仅适用于 JDBC 资源事务。参见 Spring 的[DataSourceTransactionManager](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/jdbc/datasource/datasourcetransactionmanager.html)。

# 1.4.8.为交易业务提供咨询

假设你希望同时运行事务操作和一些基本的分析建议。在<tx:annotation-driven/>的上下文中,如何实现这一点?

调用updateFoo(Foo)方法时,你希望看到以下操作:

  • 配置的分析方面开始。

  • 事务性建议运行。

  • 被建议对象上的方法运行。

  • 事务提交。

  • 分析方面报告了整个事务方法调用的确切持续时间。

本章不涉及对 AOP 进行任何详细的解释(除非
适用于交易)。关于 AOP
配置和 AOP 配置的详细覆盖范围,请参见AOP

下面的代码展示了前面讨论的简单分析方面:

爪哇

package x.y;

import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.util.StopWatch;
import org.springframework.core.Ordered;

public class SimpleProfiler implements Ordered {

    private int order;

    // allows us to control the ordering of advice
    public int getOrder() {
        return this.order;
    }

    public void setOrder(int order) {
        this.order = order;
    }

    // this method is the around advice
    public Object profile(ProceedingJoinPoint call) throws Throwable {
        Object returnValue;
        StopWatch clock = new StopWatch(getClass().getName());
        try {
            clock.start(call.toShortString());
            returnValue = call.proceed();
        } finally {
            clock.stop();
            System.out.println(clock.prettyPrint());
        }
        return returnValue;
    }
}

Kotlin

class SimpleProfiler : Ordered {

    private var order: Int = 0

    // allows us to control the ordering of advice
    override fun getOrder(): Int {
        return this.order
    }

    fun setOrder(order: Int) {
        this.order = order
    }

    // this method is the around advice
    fun profile(call: ProceedingJoinPoint): Any {
        var returnValue: Any
        val clock = StopWatch(javaClass.name)
        try {
            clock.start(call.toShortString())
            returnValue = call.proceed()
        } finally {
            clock.stop()
            println(clock.prettyPrint())
        }
        return returnValue
    }
}

建议的排序是通过Ordered接口控制的。有关建议订购的详细信息,请参见建议订购

下面的配置创建了一个fooService Bean,该配置具有按所需顺序应用于它的分析和事务方面:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/tx
        https://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">

    <bean id="fooService" class="x.y.service.DefaultFooService"/>

    <!-- this is the aspect -->
    <bean id="profiler" class="x.y.SimpleProfiler">
        <!-- run before the transactional advice (hence the lower order number) -->
        <property name="order" value="1"/>
    </bean>

    <tx:annotation-driven transaction-manager="txManager" order="200"/>

    <aop:config>
            <!-- this advice runs around the transactional advice -->
            <aop:aspect id="profilingAspect" ref="profiler">
                <aop:pointcut id="serviceMethodWithReturnValue"
                        expression="execution(!void x.y..*Service.*(..))"/>
                <aop:around method="profile" pointcut-ref="serviceMethodWithReturnValue"/>
            </aop:aspect>
    </aop:config>

    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="oracle.jdbc.driver.OracleDriver"/>
        <property name="url" value="jdbc:oracle:thin:@rj-t42:1521:elvis"/>
        <property name="username" value="scott"/>
        <property name="password" value="tiger"/>
    </bean>

    <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>

</beans>

你可以以类似的方式配置任意数量的附加方面。

下面的示例创建了与前两个示例相同的设置,但使用了纯 XML 声明式方法:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/tx
        https://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">

    <bean id="fooService" class="x.y.service.DefaultFooService"/>

    <!-- the profiling advice -->
    <bean id="profiler" class="x.y.SimpleProfiler">
        <!-- run before the transactional advice (hence the lower order number) -->
        <property name="order" value="1"/>
    </bean>

    <aop:config>
        <aop:pointcut id="entryPointMethod" expression="execution(* x.y..*Service.*(..))"/>
        <!-- runs after the profiling advice (cf. the order attribute) -->

        <aop:advisor advice-ref="txAdvice" pointcut-ref="entryPointMethod" order="2"/>
        <!-- order value is higher than the profiling aspect -->

        <aop:aspect id="profilingAspect" ref="profiler">
            <aop:pointcut id="serviceMethodWithReturnValue"
                    expression="execution(!void x.y..*Service.*(..))"/>
            <aop:around method="profile" pointcut-ref="serviceMethodWithReturnValue"/>
        </aop:aspect>

    </aop:config>

    <tx:advice id="txAdvice" transaction-manager="txManager">
        <tx:attributes>
            <tx:method name="get*" read-only="true"/>
            <tx:method name="*"/>
        </tx:attributes>
    </tx:advice>

    <!-- other <bean/> definitions such as a DataSource and a TransactionManager here -->

</beans>

上述配置的结果是一个fooService Bean,它具有按该顺序应用于它的分析和事务方面。如果你希望在进入事务建议之后和退出事务建议之前运行分析建议,则可以交换分析方面 Bean 的order属性的值,使其高于事务建议的订单值。

你可以以类似的方式配置其他方面。

# 1.4.9.使用@Transactional与 AspectJ

你还可以通过 AspectJ 方面在 Spring 容器之外使用 Spring 框架的@Transactional支持。为此,首先使用@Transactional注释对类(以及可选的类的方法)进行注释,然后使用spring-aspects.jar文件中定义的org.springframework.transaction.aspectj.AnnotationTransactionAspect链接(编织)应用程序。你还必须使用事务管理器配置方面。你可以使用 Spring 框架的 IOC 容器来处理依赖关系-注入方面。配置事务管理方面的最简单方法是使用<tx:annotation-driven/>元素,并将mode属性指定为aspectj,如[using@Transactional](#transaction-declarative-annotations)中所述。因为我们在这里关注的是在 Spring 容器之外运行的应用程序,所以我们向你展示了如何以编程方式完成它。

在继续之前,你可能希望分别阅读[使用@Transactional](#transaction-declarative-annotations)和AOP

下面的示例展示了如何创建事务管理器并配置AnnotationTransactionAspect来使用它:

爪哇

// construct an appropriate transaction manager
DataSourceTransactionManager txManager = new DataSourceTransactionManager(getDataSource());

// configure the AnnotationTransactionAspect to use it; this must be done before executing any transactional methods
AnnotationTransactionAspect.aspectOf().setTransactionManager(txManager);

Kotlin

// construct an appropriate transaction manager
val txManager = DataSourceTransactionManager(getDataSource())

// configure the AnnotationTransactionAspect to use it; this must be done before executing any transactional methods
AnnotationTransactionAspect.aspectOf().transactionManager = txManager
当你使用这个方面时,你必须注释实现类(或该类中的方法
或两者),而不是该类实现的接口(如果有的话)。AspectJ
遵循 爪哇 的规则,即接口上的注释不会被继承。

类上的@Transactional注释指定了类中任何公共方法的执行的默认事务语义。

类中方法上的@Transactional注释重写了类注释给出的默认事务语义(如果存在的话)。无论可见性如何,你都可以对任何方法进行注释。

要使用AnnotationTransactionAspect编织应用程序,你必须使用 AspectJ 构建应用程序(请参见AspectJ 开发指南 (opens new window))或使用加载时编织。请参阅Load-time weaving with AspectJ in the Spring Framework,以了解有关使用 AspectJ 进行加载时编织的讨论。

# 1.5.程序化事务管理

Spring 框架通过使用以下工具提供了两种程序化交易管理手段:

  • TransactionTemplateTransactionalOperator

  • 直接实现TransactionManager

Spring 团队通常推荐用于命令流中的程序化事务管理的TransactionTemplate和用于反应性代码的TransactionalOperator。第二种方法类似于使用 JTAUserTransactionAPI,尽管异常处理不那么麻烦。

# 1.5.1.使用TransactionTemplate

TransactionTemplate采用了与其他 Spring 模板相同的方法,例如JdbcTemplate。它使用一种回调方法(将应用程序代码从必须执行样板获取和释放事务资源的过程中解放出来),并生成意图驱动的代码,因为你的代码只关注你想要做的事情。

正如下面的示例所示,使用TransactionTemplate绝对
将你与 Spring 的事务基础设施和 API 结合在一起。程序化的
事务管理是否适合你的开发需求是你自己必须做出的决定。

必须在事务上下文中运行并显式使用TransactionTemplate的应用程序代码类似于下一个示例。作为应用程序开发人员,你可以编写TransactionCallback实现(通常表示为匿名内部类),其中包含你需要在事务上下文中运行的代码。然后,你可以将自定义TransactionCallback的实例传递到execute(..)方法上公开的TransactionTemplate方法。下面的示例展示了如何做到这一点:

爪哇

public class SimpleService implements Service {

    // single TransactionTemplate shared amongst all methods in this instance
    private final TransactionTemplate transactionTemplate;

    // use constructor-injection to supply the PlatformTransactionManager
    public SimpleService(PlatformTransactionManager transactionManager) {
        this.transactionTemplate = new TransactionTemplate(transactionManager);
    }

    public Object someServiceMethod() {
        return transactionTemplate.execute(new TransactionCallback() {
            // the code in this method runs in a transactional context
            public Object doInTransaction(TransactionStatus status) {
                updateOperation1();
                return resultOfUpdateOperation2();
            }
        });
    }
}

Kotlin

// use constructor-injection to supply the PlatformTransactionManager
class SimpleService(transactionManager: PlatformTransactionManager) : Service {

    // single TransactionTemplate shared amongst all methods in this instance
    private val transactionTemplate = TransactionTemplate(transactionManager)

    fun someServiceMethod() = transactionTemplate.execute<Any?> {
        updateOperation1()
        resultOfUpdateOperation2()
    }
}

如果没有返回值,你可以使用带有匿名类的方便的TransactionCallbackWithoutResult类,如下所示:

爪哇

transactionTemplate.execute(new TransactionCallbackWithoutResult() {
    protected void doInTransactionWithoutResult(TransactionStatus status) {
        updateOperation1();
        updateOperation2();
    }
});

Kotlin

transactionTemplate.execute(object : TransactionCallbackWithoutResult() {
    override fun doInTransactionWithoutResult(status: TransactionStatus) {
        updateOperation1()
        updateOperation2()
    }
})

回调中的代码可以通过在提供的TransactionStatus对象上调用setRollbackOnly()方法回滚事务,如下所示:

爪哇

transactionTemplate.execute(new TransactionCallbackWithoutResult() {

    protected void doInTransactionWithoutResult(TransactionStatus status) {
        try {
            updateOperation1();
            updateOperation2();
        } catch (SomeBusinessException ex) {
            status.setRollbackOnly();
        }
    }
});

Kotlin

transactionTemplate.execute(object : TransactionCallbackWithoutResult() {

    override fun doInTransactionWithoutResult(status: TransactionStatus) {
        try {
            updateOperation1()
            updateOperation2()
        } catch (ex: SomeBusinessException) {
            status.setRollbackOnly()
        }
    }
})
# 指定事务设置

你可以在TransactionTemplate上以编程方式或在配置中指定事务设置(例如传播模式、隔离级别、超时等等)。默认情况下,TransactionTemplate实例具有默认事务设置。下面的示例显示了对特定TransactionTemplate:的事务设置的程序化定制。

爪哇

public class SimpleService implements Service {

    private final TransactionTemplate transactionTemplate;

    public SimpleService(PlatformTransactionManager transactionManager) {
        this.transactionTemplate = new TransactionTemplate(transactionManager);

        // the transaction settings can be set here explicitly if so desired
        this.transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_UNCOMMITTED);
        this.transactionTemplate.setTimeout(30); // 30 seconds
        // and so forth...
    }
}

Kotlin

class SimpleService(transactionManager: PlatformTransactionManager) : Service {

    private val transactionTemplate = TransactionTemplate(transactionManager).apply {
        // the transaction settings can be set here explicitly if so desired
        isolationLevel = TransactionDefinition.ISOLATION_READ_UNCOMMITTED
        timeout = 30 // 30 seconds
        // and so forth...
    }
}

下面的示例通过使用 Spring XML 配置定义带有一些自定义事务设置的TransactionTemplate:

<bean id="sharedTransactionTemplate"
        class="org.springframework.transaction.support.TransactionTemplate">
    <property name="isolationLevelName" value="ISOLATION_READ_UNCOMMITTED"/>
    <property name="timeout" value="30"/>
</bean>

然后,你可以将sharedTransactionTemplate注入到所需的尽可能多的服务中。

最后,TransactionTemplate类的实例是线程安全的,因为实例不维护任何会话状态。TransactionTemplate实例确实保持配置状态。因此,虽然许多类可能共享TransactionTemplate的单个实例,但是如果一个类需要使用具有不同设置(例如,不同的隔离级别)的TransactionTemplate,则需要创建两个不同的TransactionTemplate实例。

# 1.5.2.使用TransactionOperator

TransactionOperator遵循类似于其他无功运算符的运算符设计。它使用一种回调方法(将应用程序代码从必须执行样板获取和释放事务资源的过程中解放出来),并生成意图驱动的代码,因为你的代码只关注你想要做的事情。

正如下面的示例所示,使用TransactionOperator绝对
将你与 Spring 的事务基础设施和 API 结合在一起。程序化的
事务管理是否适合你的开发需求是你自己必须做出的决定。

必须在事务上下文中运行并显式使用TransactionOperator的应用程序代码类似于下一个示例:

爪哇

public class SimpleService implements Service {

    // single TransactionOperator shared amongst all methods in this instance
    private final TransactionalOperator transactionalOperator;

    // use constructor-injection to supply the ReactiveTransactionManager
    public SimpleService(ReactiveTransactionManager transactionManager) {
        this.transactionOperator = TransactionalOperator.create(transactionManager);
    }

    public Mono<Object> someServiceMethod() {

        // the code in this method runs in a transactional context

        Mono<Object> update = updateOperation1();

        return update.then(resultOfUpdateOperation2).as(transactionalOperator::transactional);
    }
}

Kotlin

// use constructor-injection to supply the ReactiveTransactionManager
class SimpleService(transactionManager: ReactiveTransactionManager) : Service {

    // single TransactionalOperator shared amongst all methods in this instance
    private val transactionalOperator = TransactionalOperator.create(transactionManager)

    suspend fun someServiceMethod() = transactionalOperator.executeAndAwait<Any?> {
        updateOperation1()
        resultOfUpdateOperation2()
    }
}

TransactionalOperator可以通过两种方式使用:

  • 操作员式使用项目反应堆类型(mono.as(transactionalOperator::transactional)

  • 其他情况的回调样式(transactionalOperator.execute(TransactionCallback<T>)

回调中的代码可以通过在提供的ReactiveTransaction对象上调用setRollbackOnly()方法回滚事务,如下所示:

爪哇

transactionalOperator.execute(new TransactionCallback<>() {

    public Mono<Object> doInTransaction(ReactiveTransaction status) {
        return updateOperation1().then(updateOperation2)
                    .doOnError(SomeBusinessException.class, e -> status.setRollbackOnly());
        }
    }
});

Kotlin

transactionalOperator.execute(object : TransactionCallback() {

    override fun doInTransactionWithoutResult(status: ReactiveTransaction) {
        updateOperation1().then(updateOperation2)
                    .doOnError(SomeBusinessException.class, e -> status.setRollbackOnly())
    }
})
# 取消信号

在反应流中,aSubscriber可以取消其Subscription并停止其Publisher。Project Reactor 中的操作员,以及其他库中的操作员,例如next()take(long)timeout(Duration),以及其他人可以发出取消通知。没有办法知道取消的原因,无论是由于错误还是仅仅是缺乏进一步消费的兴趣。由于版本 5.3 取消信号导致回滚。因此,重要的是要考虑从事务Publisher向下游使用的运算符。特别是在 aFlux或其它多值Publisher的情况下,必须消耗完整的输出以允许事务完成。

# 指定事务设置

你可以为TransactionalOperator指定事务设置(例如传播模式、隔离级别、超时等等)。默认情况下,TransactionalOperator实例具有默认事务设置。下面的示例显示了针对特定TransactionalOperator:的事务设置的自定义。

Java

public class SimpleService implements Service {

    private final TransactionalOperator transactionalOperator;

    public SimpleService(ReactiveTransactionManager transactionManager) {
        DefaultTransactionDefinition definition = new DefaultTransactionDefinition();

        // the transaction settings can be set here explicitly if so desired
        definition.setIsolationLevel(TransactionDefinition.ISOLATION_READ_UNCOMMITTED);
        definition.setTimeout(30); // 30 seconds
        // and so forth...

        this.transactionalOperator = TransactionalOperator.create(transactionManager, definition);
    }
}

Kotlin

class SimpleService(transactionManager: ReactiveTransactionManager) : Service {

    private val definition = DefaultTransactionDefinition().apply {
        // the transaction settings can be set here explicitly if so desired
        isolationLevel = TransactionDefinition.ISOLATION_READ_UNCOMMITTED
        timeout = 30 // 30 seconds
        // and so forth...
    }
    private val transactionalOperator = TransactionalOperator(transactionManager, definition)
}

# 1.5.3.使用TransactionManager

下面的部分解释命令式事务管理器和反应式事务管理器的编程使用。

# 使用PlatformTransactionManager

对于命令式事务,可以直接使用org.springframework.transaction.PlatformTransactionManager来管理事务。要做到这一点,通过 Bean 引用将你使用的PlatformTransactionManager的实现传递给你的 Bean。然后,通过使用TransactionDefinitionTransactionStatus对象,你可以发起事务、回滚和提交。下面的示例展示了如何做到这一点:

Java

DefaultTransactionDefinition def = new DefaultTransactionDefinition();
// explicitly setting the transaction name is something that can be done only programmatically
def.setName("SomeTxName");
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);

TransactionStatus status = txManager.getTransaction(def);
try {
    // put your business logic here
} catch (MyException ex) {
    txManager.rollback(status);
    throw ex;
}
txManager.commit(status);

Kotlin

val def = DefaultTransactionDefinition()
// explicitly setting the transaction name is something that can be done only programmatically
def.setName("SomeTxName")
def.propagationBehavior = TransactionDefinition.PROPAGATION_REQUIRED

val status = txManager.getTransaction(def)
try {
    // put your business logic here
} catch (ex: MyException) {
    txManager.rollback(status)
    throw ex
}

txManager.commit(status)
# 使用ReactiveTransactionManager

在处理反应性事务时,可以直接使用org.springframework.transaction.ReactiveTransactionManager来管理事务。要做到这一点,通过 Bean 引用将你使用的ReactiveTransactionManager的实现传递给你的 Bean。然后,通过使用TransactionDefinitionReactiveTransaction对象,你可以发起事务、回滚和提交。下面的示例展示了如何做到这一点:

Java

DefaultTransactionDefinition def = new DefaultTransactionDefinition();
// explicitly setting the transaction name is something that can be done only programmatically
def.setName("SomeTxName");
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);

Mono<ReactiveTransaction> reactiveTx = txManager.getReactiveTransaction(def);

reactiveTx.flatMap(status -> {

    Mono<Object> tx = ...; // put your business logic here

    return tx.then(txManager.commit(status))
            .onErrorResume(ex -> txManager.rollback(status).then(Mono.error(ex)));
});

Kotlin

val def = DefaultTransactionDefinition()
// explicitly setting the transaction name is something that can be done only programmatically
def.setName("SomeTxName")
def.propagationBehavior = TransactionDefinition.PROPAGATION_REQUIRED

val reactiveTx = txManager.getReactiveTransaction(def)
reactiveTx.flatMap { status ->

    val tx = ... // put your business logic here

    tx.then(txManager.commit(status))
            .onErrorResume { ex -> txManager.rollback(status).then(Mono.error(ex)) }
}

# 1.6.在程序化事务管理和声明式事务管理之间进行选择

只有当你有少量的事务操作时,程序化事务管理通常才是一个好主意。例如,如果你有一个 Web 应用程序,该应用程序仅需要用于某些更新操作的事务,那么你可能不希望通过使用 Spring 或任何其他技术来设置事务代理。在这种情况下,使用TransactionTemplate可能是一种很好的方法。能够显式地设置事务名称也只能通过使用程序化的事务管理方法来完成。

另一方面,如果你的应用程序有许多事务操作,声明式事务管理通常是值得的。它使事务管理脱离了业务逻辑,并且不难配置。 Spring 当使用框架而不是 EJB CMT 时,声明式事务管理的配置成本大大降低。

# 1.7.事务绑定事件

从 Spring 4.2 开始,事件的侦听器可以绑定到事务的一个阶段。典型的示例是在事务成功完成时处理事件。当当前事务的结果对侦听器确实很重要时,这样做可以更灵活地使用事件。

你可以使用@EventListener注释来注册一个常规的事件侦听器。如果需要将其绑定到事务,请使用@TransactionalEventListener。这样做时,默认情况下侦听器将绑定到事务的提交阶段。

下一个示例展示了这个概念。假设一个组件发布了一个命令创建的事件,并且我们希望定义一个侦听器,该侦听器只应在发布该组件的事务成功提交之后处理该事件。下面的示例设置了这样的事件侦听器:

Java

@Component
public class MyComponent {

    @TransactionalEventListener
    public void handleOrderCreatedEvent(CreationEvent<Order> creationEvent) {
        // ...
    }
}

Kotlin

@Component
class MyComponent {

    @TransactionalEventListener
    fun handleOrderCreatedEvent(creationEvent: CreationEvent<Order>) {
        // ...
    }
}

@TransactionalEventListener注释公开了一个phase属性,该属性允许你定制侦听器应该绑定到的事务的阶段。有效的阶段是BEFORE_COMMITAFTER_COMMIT(默认),AFTER_ROLLBACK,以及AFTER_COMPLETION,它们聚合了事务完成(无论是提交还是回滚)。

如果没有事务正在运行,则根本不会调用侦听器,因为我们无法遵守所需的语义。但是,你可以通过将注释的fallbackExecution属性设置为true来覆盖该行为。

@TransactionalEventListener仅适用于由PlatformTransactionManager管理的线程绑定事务。由ReactiveTransactionManager管理的反应性事务使用反应器上下文而不是线程本地属性,因此从
事件侦听器的角度来看,它不能参与兼容的活动事务。

# 1.8.特定于应用服务器的集成

Spring 的事务抽象通常与应用服务器无关。此外, Spring 的JtaTransactionManager类(它可以选择性地为 JTAUserTransactionTransactionManager对象执行 JNDI 查找)自动检测后一个对象的位置,该位置因应用服务器的不同而不同。对 JTATransactionManager的访问允许增强事务语义——特别是支持事务挂起。详情见[JtaTransactionManager](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/transaction/jta/jtatransactionmanager.html)Javadoc。

Spring 的JtaTransactionManager是在 Java EE 应用程序服务器上运行的标准选择,并且已知可在所有公共服务器上运行。高级功能(如事务挂起)也可以在许多服务器上运行(包括 GlassFish、JBoss 和 Geronimo),而不需要任何特殊配置。然而,对于完全支持的事务挂起和进一步的高级集成, Spring 包括用于 WebLogic Server 和 WebSphere 的特殊适配器。这些适配器将在下面的小节中进行讨论。

对于包括 WebLogic Server 和 WebSphere 在内的标准场景,可以考虑使用方便的<tx:jta-transaction-manager/>配置元素。在进行配置时,此元素会自动检测基础服务器,并为平台选择最佳的事务管理器。这意味着你不需要显式地配置特定于服务器的适配器类(如以下小节中讨论的那样)。相反,它们是自动选择的,标准的JtaTransactionManager是默认的回退。

# 1.8.1.IBMWebSphere

在 WebSphere6.1.0.9 及以上版本中,推荐使用的 Spring JTA 事务管理器是WebSphereUowTransactionManager。这个特殊的适配器使用 IBM 的UOWManagerAPI,它在 WebSphereApplicationServer6.1.0.9 及更高版本中可用。有了这个适配器, Spring 驱动的事务暂停(由PROPAGATION_REQUIRES_NEW发起的挂起和恢复)得到了 IBM 的正式支持。

# 1.8.2.Oracle WebLogic 服务器

在 WebLogic Server9.0 或更高版本中,通常使用WebLogicJtaTransactionManager而不是 stockJtaTransactionManager类。普通JtaTransactionManager的这个特殊的特定于 WebLogic 的子类在 WebLogic 管理的事务环境中支持 Spring 事务定义的全部功能,而不是标准的 JTA 语义。功能包括事务名称、每个事务隔离级别,以及在所有情况下正确地恢复事务。

# 1.9.常见问题的解决方案

这一节描述了一些常见问题的解决方案。

# 1.9.1.对特定的DataSource#### 使用错误的事务管理器

根据事务技术和需求的选择,使用正确的PlatformTransactionManager实现。 Spring 框架使用得当,仅提供了一种直接的、可移植的抽象。如果使用全局事务,则必须对所有事务操作使用org.springframework.transaction.jta.JtaTransactionManager类(或其特定于应用服务器的子类)。否则,事务基础结构将尝试在容器DataSource实例等资源上执行本地事务。这样的本地事务是没有意义的,一个好的应用程序服务器会将它们视为错误。

# 1.10.更多资源

有关 Spring 框架的事务支持的更多信息,请参见:

# 2. DAO 支持

Spring 中的数据访问对象支持旨在使其易于以一致的方式与数据访问技术(例如 JDBC、 Hibernate 或 JPA)一起工作。这使你可以相当容易地在上述持久性技术之间进行切换,并且还允许你编写代码,而无需担心捕获特定于每种技术的异常。

# 2.1.一致的异常层次结构

Spring 提供了一种从技术特定的异常(例如)到其自身的异常类层次结构的方便转换,其具有将作为根异常的特性。这些异常包装了原始异常,这样就不会有任何风险,你可能会丢失任何有关可能出了什么问题的信息。

除了 JDBC 异常之外, Spring 还可以包装 JPA 和 Hibernate 特定的异常,将它们转换为一组集中的运行时异常。这样,你就可以只在适当的层中处理大多数不可恢复的持久性异常,而不需要在 DAO 中有烦人的样板抓取和抛出块和异常声明。正如上面提到的,JDBC 异常(包括特定于数据库的方言)也被转换为相同的层次结构,这意味着你可以在一致的编程模型中使用 JDBC 执行一些操作。

前面的讨论适用于 Spring 对各种 ORM 框架的支持中的各种模板类。如果使用基于拦截器的类,则应用程序必须关心如何处理HibernateExceptionsPersistenceExceptions本身,最好是将其分别委托给convertHibernateAccessException(..)convertJpaAccessException(..)中的SessionFactoryUtils方法。这些方法将异常转换为与org.springframework.dao异常层次结构中的异常兼容的异常。由于PersistenceExceptions未被选中,它们也可能被抛出(尽管在异常方面牺牲了泛型 DAO 抽象)。

下面的图像显示了 Spring 提供的异常层次结构。(请注意,图像中详细介绍的类层次结构只显示了整个DataAccessException层次结构的一个子集。

DataAccessCeption

# 2.2.用于配置 DAO 或存储库类的注释

保证你的数据访问对象或存储库提供异常转换的最佳方法是使用@Repository注释。这种注释还允许组件扫描支持查找和配置你的 DAO 和存储库,而无需为它们提供 XML 配置条目。下面的示例展示了如何使用@Repository注释:

Java

@Repository (1)
public class SomeMovieFinder implements MovieFinder {
    // ...
}
1 @Repository注释。

Kotlin

@Repository (1)
class SomeMovieFinder : MovieFinder {
    // ...
}
1 @Repository注释。

任何 DAO 或存储库实现都需要访问持久性资源,这取决于所使用的持久性技术。例如,基于 JDBC 的存储库需要访问 JDBCDataSource,而基于 JPA 的存储库需要访问EntityManager。实现这一点的最简单方法是通过使用@Autowired@Inject@Resource@PersistenceContext注释中的一个来注入这种资源依赖关系。下面的示例适用于 JPA 存储库:

Java

@Repository
public class JpaMovieFinder implements MovieFinder {

    @PersistenceContext
    private EntityManager entityManager;

    // ...
}

Kotlin

@Repository
class JpaMovieFinder : MovieFinder {

    @PersistenceContext
    private lateinit var entityManager: EntityManager

    // ...
}

如果使用经典的 Hibernate API,则可以插入SessionFactory,如下例所示:

Java

@Repository
public class HibernateMovieFinder implements MovieFinder {

    private SessionFactory sessionFactory;

    @Autowired
    public void setSessionFactory(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

    // ...
}

Kotlin

@Repository
class HibernateMovieFinder(private val sessionFactory: SessionFactory) : MovieFinder {
    // ...
}

我们在这里展示的最后一个示例是典型的 JDBC 支持。你可以将DataSource注入到初始化方法或构造函数中,在这里你将通过使用JdbcTemplate创建JdbcTemplate和其他数据访问支持类(例如SimpleJdbcCall和其他类)。以下示例自动连接DataSource:

Java

@Repository
public class JdbcMovieFinder implements MovieFinder {

    private JdbcTemplate jdbcTemplate;

    @Autowired
    public void init(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }

    // ...
}

Kotlin

@Repository
class JdbcMovieFinder(dataSource: DataSource) : MovieFinder {

    private val jdbcTemplate = JdbcTemplate(dataSource)

    // ...
}
有关如何
配置应用程序上下文以利用这些注释的详细信息,请参见每个持久性技术的具体覆盖范围。

# 3. 使用 JDBC 进行数据访问

Spring 框架 JDBC 抽象所提供的值可能最好通过下表中概述的动作序列来显示。该表显示了 Spring 处理哪些操作以及哪些操作是你的责任。

行动 Spring You
定义连接参数。 X
打开连接。 X
指定 SQL 语句。 X
声明参数并提供参数值 X
准备并运行该语句。 X
设置循环以遍历结果(如果有的话)。 X
完成每个迭代的工作。 X
处理任何异常。 X
处理交易。 X
关闭连接、语句和结果集。 X

Spring 框架负责处理所有可能使 JDBC 成为如此乏味的 API 的底层细节。

# 3.1.一种 JDBC 数据库访问方法的选择

你可以在几种方法中进行选择,以形成你的 JDBC 数据库访问的基础。除了JdbcTemplate的三种风格之外,新的SimpleJdbcInsertSimpleJdbcCall方法优化了数据库元数据,并且 RDBMS 对象风格采用了一种类似于 JDO 查询设计的更面向对象的方法。一旦你开始使用这些方法之一,你仍然可以进行混合和匹配,以包含来自不同方法的功能。所有的方法都需要一个符合 JDBC2.0 的驱动程序,而一些高级特性则需要一个 JDBC3.0 驱动程序。

  • JdbcTemplate是经典且最流行的 Spring JDBC 方法。这种“最底层”的方法和所有其他方法都使用了 JDBCtemplate。

  • NamedParameterJdbcTemplate封装一个JdbcTemplate以提供命名参数,而不是传统的 JDBC?占位符。当你对 SQL 语句有多个参数时,这种方法提供了更好的文档和易用性。

  • SimpleJdbcInsertSimpleJdbcCall优化数据库元数据,以限制所需配置的数量。这种方法简化了编码,因此只需要提供表或过程的名称,并提供匹配列名称的参数映射。只有当数据库提供了足够的元数据时,这种方法才会起作用。如果数据库不提供此元数据,则必须提供参数的显式配置。

  • RDBMS 对象——包括MappingSqlQuerySqlUpdateStoredProcedure——要求你在数据访问层的初始化过程中创建可重用和线程安全的对象。这种方法是仿照 JDO 查询建模的,在 JDO 查询中,你定义查询字符串、声明参数并编译查询。一旦这样做,execute(…​)update(…​)findObject(…​)方法就可以使用各种参数值多次调用。

# 3.2.包层次结构

Spring 框架的 JDBC 抽象框架由四个不同的包组成:

  • core:org.springframework.jdbc.core包包含JdbcTemplate类及其各种回调接口,以及各种相关的类。名为org.springframework.jdbc.core.simple的子包包含SimpleJdbcInsertSimpleJdbcCall类。另一个名为org.springframework.jdbc.core.namedparam的子包包含NamedParameterJdbcTemplate类和相关的支持类。参见使用 JDBC 核心类来控制基本的 JDBC 处理和错误处理JDBC 批处理操作,以及[用SimpleJdbc类简化 JDBC 操作]。

  • datasource:org.springframework.jdbc.datasource包包含一个用于 easyDataSource访问的实用程序类和各种简单的DataSource实现,你可以使用这些实现在 Java EE 容器之外测试和运行未经修改的 JDBC 代码。一个名为org.springfamework.jdbc.datasource.embedded的子包提供了通过使用 Java 数据库引擎创建嵌入式数据库的支持,例如 HSQL、H2 和 Derby。见控制数据库连接嵌入式数据库支持

  • object:org.springframework.jdbc.object包包含表示 RDBMS 查询、更新和存储过程的类,这些类是线程安全的、可重用的对象。见将 JDBC 操作建模为 Java 对象。这种方法是由 JDO 建模的,尽管查询返回的对象自然地与数据库断开连接。这个较高级别的 JDBC 抽象依赖于org.springframework.jdbc.core包中的较低级别的抽象。

  • support:org.springframework.jdbc.support包提供了SQLException翻译功能和一些实用程序类。在 JDBC 处理过程中抛出的异常被转换为org.springframework.dao包中定义的异常。这意味着使用 Spring JDBC 抽象层的代码不需要实现 JDBC 或 RDBMS 特定的错误处理。所有转换后的异常都未被选中,这使你可以选择捕获异常,以便在允许将其他异常传播给调用方的同时恢复这些异常。参见[使用SQLExceptionTranslator]。

# 3.3.使用 JDBC 核心类来控制基本的 JDBC 处理和错误处理

本节介绍如何使用 JDBC 核心类来控制基本的 JDBC 处理,包括错误处理。它包括以下主题:

# 3.3.1.使用JdbcTemplate

JdbcTemplate是 JDBC 核心包中的中心类。它处理资源的创建和释放,这有助于你避免常见的错误,例如忘记关闭连接。它执行核心 JDBC 工作流的基本任务(例如语句的创建和执行),留下应用程序代码来提供 SQL 和提取结果。JdbcTemplate类:

  • 运行 SQL 查询

  • 更新语句和存储过程调用

  • ResultSet实例上执行迭代并提取返回的参数值。

  • 捕获 JDBC 异常,并将它们转换为在org.springframework.dao包中定义的通用的、信息量更大的异常层次结构。(见一致的异常层次结构

当你为代码使用JdbcTemplate时,你只需要实现回调接口,并为它们提供一个明确定义的契约。给定由JdbcTemplate类提供的ConnectionPreparedStatementCreator回调接口将创建一个准备好的语句,提供 SQL 和任何必要的参数。对于创建可调用语句的CallableStatementCreator接口也是如此。RowCallbackHandler接口从ResultSet的每一行中提取值。

你可以通过使用DataSource引用的直接实例化在 DAO 实现中使用JdbcTemplate,或者可以在 Spring IoC 容器中配置它,并将其作为 Bean 引用提供给 DAO。

DataSource应该始终配置为 Spring IOC 容器中的 Bean。在
中,第一种情况将 Bean 直接赋予服务;在第二种情况下,将
赋予准备好的模板。

这个类发布的所有 SQL 都记录在DEBUG级别的目录下,该目录对应于模板实例的完全限定类名称(通常为JdbcTemplate,但如果使用JdbcTemplate类的自定义子类,则可能会有所不同)。

下面的部分提供了JdbcTemplate用法的一些示例。这些示例并不是JdbcTemplate公开的所有功能的详尽列表。参见乘务员javadoc (opens new window)

# 查询(SELECT

下面的查询获取关系中的行数:

Java

int rowCount = this.jdbcTemplate.queryForObject("select count(*) from t_actor", Integer.class);

Kotlin

val rowCount = jdbcTemplate.queryForObject<Int>("select count(*) from t_actor")!!

以下查询使用了一个绑定变量:

Java

int countOfActorsNamedJoe = this.jdbcTemplate.queryForObject(
        "select count(*) from t_actor where first_name = ?", Integer.class, "Joe");

Kotlin

val countOfActorsNamedJoe = jdbcTemplate.queryForObject<Int>(
        "select count(*) from t_actor where first_name = ?", arrayOf("Joe"))!!

下面的查询查找String:

Java

String lastName = this.jdbcTemplate.queryForObject(
        "select last_name from t_actor where id = ?",
        String.class, 1212L);

Kotlin

val lastName = this.jdbcTemplate.queryForObject<String>(
        "select last_name from t_actor where id = ?",
        arrayOf(1212L))!!

下面的查询查找并填充一个域对象:

Java

Actor actor = jdbcTemplate.queryForObject(
        "select first_name, last_name from t_actor where id = ?",
        (resultSet, rowNum) -> {
            Actor newActor = new Actor();
            newActor.setFirstName(resultSet.getString("first_name"));
            newActor.setLastName(resultSet.getString("last_name"));
            return newActor;
        },
        1212L);

Kotlin

val actor = jdbcTemplate.queryForObject(
            "select first_name, last_name from t_actor where id = ?",
            arrayOf(1212L)) { rs, _ ->
        Actor(rs.getString("first_name"), rs.getString("last_name"))
    }

以下查询查找并填充域对象列表:

Java

List<Actor> actors = this.jdbcTemplate.query(
        "select first_name, last_name from t_actor",
        (resultSet, rowNum) -> {
            Actor actor = new Actor();
            actor.setFirstName(resultSet.getString("first_name"));
            actor.setLastName(resultSet.getString("last_name"));
            return actor;
        });

Kotlin

val actors = jdbcTemplate.query("select first_name, last_name from t_actor") { rs, _ ->
        Actor(rs.getString("first_name"), rs.getString("last_name"))

如果最后两个代码片段实际上存在于同一个应用程序中,那么删除两个RowMapperlambda 表达式中存在的重复并将它们提取到一个字段中是有意义的,然后可以根据需要由 DAO 方法引用。例如,将前面的代码片段编写如下可能更好:

Java

private final RowMapper<Actor> actorRowMapper = (resultSet, rowNum) -> {
    Actor actor = new Actor();
    actor.setFirstName(resultSet.getString("first_name"));
    actor.setLastName(resultSet.getString("last_name"));
    return actor;
};

public List<Actor> findAllActors() {
    return this.jdbcTemplate.query("select first_name, last_name from t_actor", actorRowMapper);
}

Kotlin

val actorMapper = RowMapper<Actor> { rs: ResultSet, rowNum: Int ->
    Actor(rs.getString("first_name"), rs.getString("last_name"))
}

fun findAllActors(): List<Actor> {
    return jdbcTemplate.query("select first_name, last_name from t_actor", actorMapper)
}
# 更新(INSERTUPDATE,和DELETE)与JdbcTemplate

可以使用update(..)方法执行插入、更新和删除操作。参数值通常以变量参数或对象数组的形式提供。

下面的示例插入一个新条目:

Java

this.jdbcTemplate.update(
        "insert into t_actor (first_name, last_name) values (?, ?)",
        "Leonor", "Watling");

Kotlin

jdbcTemplate.update(
        "insert into t_actor (first_name, last_name) values (?, ?)",
        "Leonor", "Watling")

下面的示例更新了一个现有条目:

Java

this.jdbcTemplate.update(
        "update t_actor set last_name = ? where id = ?",
        "Banjo", 5276L);

Kotlin

jdbcTemplate.update(
        "update t_actor set last_name = ? where id = ?",
        "Banjo", 5276L)

下面的示例删除一个条目:

Java

this.jdbcTemplate.update(
        "delete from t_actor where id = ?",
        Long.valueOf(actorId));

Kotlin

jdbcTemplate.update("delete from t_actor where id = ?", actorId.toLong())
# 其它JdbcTemplate操作

你可以使用execute(..)方法来运行任意 SQL。因此,该方法通常用于 DDL 语句。它被接受回调接口、绑定变量数组等的变量严重超载。下面的示例创建了一个表:

Java

this.jdbcTemplate.execute("create table mytable (id integer, name varchar(100))");

Kotlin

jdbcTemplate.execute("create table mytable (id integer, name varchar(100))")

下面的示例调用一个存储过程:

爪哇

this.jdbcTemplate.update(
        "call SUPPORT.REFRESH_ACTORS_SUMMARY(?)",
        Long.valueOf(unionId));

Kotlin

jdbcTemplate.update(
        "call SUPPORT.REFRESH_ACTORS_SUMMARY(?)",
        unionId.toLong())

更复杂的存储过程支持是稍后覆盖

# JdbcTemplate最佳实践

一旦配置好,JdbcTemplate类的实例是线程安全的。这很重要,因为这意味着你可以配置JdbcTemplate的单个实例,然后将这个共享引用安全地注入到多个 DAO(或存储库)中。JdbcTemplate是有状态的,因为它维护了对DataSource的引用,但该状态不是会话状态。

在使用JdbcTemplate类(以及相关的[NamedParameterJdbcTemplate](#jdbc-namedParameterJDbcTemplate)类时,一种常见的做法是在 Spring 配置文件中配置DataSource,然后在依赖项中-将共享的DataSource Bean 注入到 DAO 类中。在 setter 中为DataSource创建了JdbcTemplate。这导致 DAO 类似于以下内容:

爪哇

public class JdbcCorporateEventDao implements CorporateEventDao {

    private JdbcTemplate jdbcTemplate;

    public void setDataSource(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }

    // JDBC-backed implementations of the methods on the CorporateEventDao follow...
}

Kotlin

class JdbcCorporateEventDao(dataSource: DataSource) : CorporateEventDao {

    private val jdbcTemplate = JdbcTemplate(dataSource)

    // JDBC-backed implementations of the methods on the CorporateEventDao follow...
}

下面的示例展示了相应的 XML 配置:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">

    <bean id="corporateEventDao" class="com.example.JdbcCorporateEventDao">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="${jdbc.driverClassName}"/>
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>

    <context:property-placeholder location="jdbc.properties"/>

</beans>

显式配置的另一种选择是使用组件扫描和注释支持来进行依赖项注入。在这种情况下,你可以使用@Repository对类进行注释(这使它成为组件扫描的候选项),并使用DataSourcesetter 方法对@Autowired进行注释。下面的示例展示了如何做到这一点:

爪哇

@Repository (1)
public class JdbcCorporateEventDao implements CorporateEventDao {

    private JdbcTemplate jdbcTemplate;

    @Autowired (2)
    public void setDataSource(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource); (3)
    }

    // JDBC-backed implementations of the methods on the CorporateEventDao follow...
}
1 @Repository注释类。
2 @Autowired注释DataSourcesetter 方法。
3 DataSource创建一个新的JdbcTemplate

Kotlin

@Repository (1)
class JdbcCorporateEventDao(dataSource: DataSource) : CorporateEventDao { (2)

    private val jdbcTemplate = JdbcTemplate(dataSource) (3)

    // JDBC-backed implementations of the methods on the CorporateEventDao follow...
}
1 @Repository注释类。
2 构造函数注入DataSource
3 创建一个新的JdbcTemplate,并使用NamedParameterJdbcTemplate

下面的示例展示了相应的 XML 配置:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">

    <!-- Scans within the base package of the application for @Component classes to configure as beans -->
    <context:component-scan base-package="org.springframework.docs.test" />

    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="${jdbc.driverClassName}"/>
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>

    <context:property-placeholder location="jdbc.properties"/>

</beans>

如果你使用 Spring 的JdbcDaoSupport类,并且你的各种支持 JDBC 的 DAO 类从中扩展,那么你的子类将从setDataSource(..)类继承一个setDataSource(..)方法。你可以选择是否从这个类继承。JdbcDaoSupport类仅作为一种方便而提供。

无论你选择使用(或不使用)上述哪种模板初始化样式,每次要运行 SQL 时,都很少需要创建JdbcTemplate类的新实例。一旦配置好,JdbcTemplate实例就是线程安全的。如果你的应用程序访问多个数据库,你可能需要多个JdbcTemplate实例,这需要多个DataSources实例,然后需要多个配置不同的JdbcTemplate实例。

# 3.3.2.使用NamedParameterJdbcTemplate

NamedParameterJdbcTemplate类增加了对使用命名参数编程 JDBC 语句的支持,而不是仅使用经典占位符('?')参数编程 JDBC 语句。NamedParameterJdbcTemplate类包装了 aJdbcTemplate,并将其委托给包装好的JdbcTemplate,以完成其大部分工作。本节仅描述NamedParameterJdbcTemplate类中与JdbcTemplate本身不同的区域——即通过使用命名参数编程 JDBC 语句。下面的示例展示了如何使用NamedParameterJdbcTemplate:

爪哇

// some JDBC-backed DAO class...
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;

public void setDataSource(DataSource dataSource) {
    this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
}

public int countOfActorsByFirstName(String firstName) {

    String sql = "select count(*) from T_ACTOR where first_name = :first_name";

    SqlParameterSource namedParameters = new MapSqlParameterSource("first_name", firstName);

    return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Integer.class);
}

Kotlin

private val namedParameterJdbcTemplate = NamedParameterJdbcTemplate(dataSource)

fun countOfActorsByFirstName(firstName: String): Int {
    val sql = "select count(*) from T_ACTOR where first_name = :first_name"
    val namedParameters = MapSqlParameterSource("first_name", firstName)
    return namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Int::class.java)!!
}

请注意,在分配给sql变量的值和插入namedParameters变量(类型MapSqlParameterSource)的对应值中使用了命名参数表示法。

或者,你可以使用基于Map的样式,将命名参数及其对应值传递给NamedParameterJdbcTemplate实例。由NamedParameterJdbcOperations公开并由NamedParameterJdbcTemplate类实现的其余方法遵循类似的模式,在此不涉及。

下面的示例展示了基于Map的样式的使用:

爪哇

// some JDBC-backed DAO class...
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;

public void setDataSource(DataSource dataSource) {
    this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
}

public int countOfActorsByFirstName(String firstName) {

    String sql = "select count(*) from T_ACTOR where first_name = :first_name";

    Map<String, String> namedParameters = Collections.singletonMap("first_name", firstName);

    return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters,  Integer.class);
}

Kotlin

// some JDBC-backed DAO class...
private val namedParameterJdbcTemplate = NamedParameterJdbcTemplate(dataSource)

fun countOfActorsByFirstName(firstName: String): Int {
    val sql = "select count(*) from T_ACTOR where first_name = :first_name"
    val namedParameters = mapOf("first_name" to firstName)
    return namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Int::class.java)!!
}

NamedParameterJdbcTemplate(存在于同一个 爪哇 包中)相关的一个不错的特性是SqlParameterSource接口。你已经在前面的代码片段(MapSqlParameterSource类)中看到了该接口实现的示例。SqlParameterSourceNamedParameterJdbcTemplate的命名参数值的来源。MapSqlParameterSource类是一个简单的实现,它是一个围绕java.util.Map的适配器,其中键是参数名称,值是参数值。

另一个SqlParameterSource实现是BeanPropertySqlParameterSource类。这个类包装一个任意的 爪哇Bean(即一个坚持爪哇Bean 公约 (opens new window)的类的实例),并使用包装好的 爪哇Bean 的属性作为命名参数值的源。

下面的示例展示了一个典型的 爪哇Bean:

爪哇

public class Actor {

    private Long id;
    private String firstName;
    private String lastName;

    public String getFirstName() {
        return this.firstName;
    }

    public String getLastName() {
        return this.lastName;
    }

    public Long getId() {
        return this.id;
    }

    // setters omitted...

}

Kotlin

data class Actor(val id: Long, val firstName: String, val lastName: String)

下面的示例使用NamedParameterJdbcTemplate返回前面示例中所示的类成员的计数:

爪哇

// some JDBC-backed DAO class...
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;

public void setDataSource(DataSource dataSource) {
    this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
}

public int countOfActors(Actor exampleActor) {

    // notice how the named parameters match the properties of the above 'Actor' class
    String sql = "select count(*) from T_ACTOR where first_name = :firstName and last_name = :lastName";

    SqlParameterSource namedParameters = new BeanPropertySqlParameterSource(exampleActor);

    return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Integer.class);
}

Kotlin

// some JDBC-backed DAO class...
private val namedParameterJdbcTemplate = NamedParameterJdbcTemplate(dataSource)

private val namedParameterJdbcTemplate = NamedParameterJdbcTemplate(dataSource)

fun countOfActors(exampleActor: Actor): Int {
    // notice how the named parameters match the properties of the above 'Actor' class
    val sql = "select count(*) from T_ACTOR where first_name = :firstName and last_name = :lastName"
    val namedParameters = BeanPropertySqlParameterSource(exampleActor)
    return namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Int::class.java)!!
}

请记住,NamedParameterJdbcTemplate类封装了一个经典的JdbcTemplate模板。如果你需要访问打包的JdbcTemplate实例以访问仅在JdbcTemplate类中存在的功能,则可以使用getJdbcOperations()方法通过JdbcOperations接口访问打包的JdbcTemplate实例。

另请参阅[JdbcTemplate最佳实践](#jdbc-jdbctemplate-idioms),以获取在应用程序上下文中使用NamedParameterJdbcTemplate类的指导方针。

# 3.3.3.使用SQLExceptionTranslator

NamedParameterJdbcTemplate是一个由类实现的接口,它可以在SQLExceptions 和 Spring 自己的org.springframework.dao.DataAccessException之间转换,这在数据访问策略方面是不可知的。实现可以是通用的(例如,为 JDBC 使用 SQLState 代码),也可以是专有的(例如,使用 Oracle 错误代码),以获得更高的精度。

SQLErrorCodeSQLExceptionTranslator是默认使用的SQLExceptionTranslator的实现。该实现使用特定的供应商代码。它比SQLState实现更精确。错误代码转换是基于一个名为SQLErrorCodes的 爪哇Bean 类型类中的代码。该类由SQLErrorCodesFactory创建和填充,它(顾名思义)是一个工厂,用于基于名为sql-error-codes.xml的配置文件的内容创建SQLErrorCodes。该文件填充了供应商代码,并基于DatabaseProductName取自DatabaseMetaData。使用了你正在使用的实际数据库的代码。

SQLErrorCodeSQLExceptionTranslator按以下顺序应用匹配规则:

  1. 由子类实现的任何自定义转换。通常情况下,使用提供的具体SQLErrorCodeSQLExceptionTranslator,因此此规则不适用。它仅在你实际提供了一个子类实现的情况下才适用。

  2. 作为SQLErrorCodes类的customSqlExceptionTranslator属性提供的SQLExceptionTranslator接口的任何自定义实现。

  3. 搜索CustomSQLErrorCodesTranslation类的实例列表(为SQLErrorCodes类的customTranslations属性提供)以查找匹配项。

  4. 应用了错误码匹配。

  5. 使用后备翻译程序。SQLExceptionSubclassTranslator是默认的后备翻译程序。如果此翻译不可用,则下一个后备翻译程序是SQLStateSQLExceptionTranslator

默认情况下,SQLErrorCodesFactory用于定义Error代码和自定义异常
翻译。它们是在一个名为sql-error-codes.xml的文件中从
Classpath 中查找的,并且匹配的SQLErrorCodes实例是基于数据库中所使用的数据库元数据的SimpleJdbcCall中的名称。

你可以扩展SQLErrorCodeSQLExceptionTranslator,如下例所示:

爪哇

public class CustomSQLErrorCodesTranslator extends SQLErrorCodeSQLExceptionTranslator {

    protected DataAccessException customTranslate(String task, String sql, SQLException sqlEx) {
        if (sqlEx.getErrorCode() == -12345) {
            return new DeadlockLoserDataAccessException(task, sqlEx);
        }
        return null;
    }
}

Kotlin

class CustomSQLErrorCodesTranslator : SQLErrorCodeSQLExceptionTranslator() {

    override fun customTranslate(task: String, sql: String?, sqlEx: SQLException): DataAccessException? {
        if (sqlEx.errorCode == -12345) {
            return DeadlockLoserDataAccessException(task, sqlEx)
        }
        return null
    }
}

在前面的示例中,将翻译特定的错误代码(-12345),而其他错误将由默认的翻译实现来翻译。要使用此自定义转换器,必须通过方法setExceptionTranslator将其传递给JdbcTemplate,并且必须将此JdbcTemplate用于需要此转换器的所有数据访问处理。下面的示例展示了如何使用这个自定义转换器:

爪哇

private JdbcTemplate jdbcTemplate;

public void setDataSource(DataSource dataSource) {

    // create a JdbcTemplate and set data source
    this.jdbcTemplate = new JdbcTemplate();
    this.jdbcTemplate.setDataSource(dataSource);

    // create a custom translator and set the DataSource for the default translation lookup
    CustomSQLErrorCodesTranslator tr = new CustomSQLErrorCodesTranslator();
    tr.setDataSource(dataSource);
    this.jdbcTemplate.setExceptionTranslator(tr);

}

public void updateShippingCharge(long orderId, long pct) {
    // use the prepared JdbcTemplate for this update
    this.jdbcTemplate.update("update orders" +
        " set shipping_charge = shipping_charge * ? / 100" +
        " where id = ?", pct, orderId);
}

Kotlin

// create a JdbcTemplate and set data source
private val jdbcTemplate = JdbcTemplate(dataSource).apply {
    // create a custom translator and set the DataSource for the default translation lookup
    exceptionTranslator = CustomSQLErrorCodesTranslator().apply {
        this.dataSource = dataSource
    }
}

fun updateShippingCharge(orderId: Long, pct: Long) {
    // use the prepared JdbcTemplate for this update
    this.jdbcTemplate!!.update("update orders" +
            " set shipping_charge = shipping_charge * ? / 100" +
            " where id = ?", pct, orderId)
}

sql-error-codes.xml中,将向自定义转换器传递一个数据源,以便查找错误代码。

# 3.3.4.正在运行的语句

运行 SQL 语句只需要很少的代码。你需要一个DataSource和一个JdbcTemplate,包括JdbcTemplate提供的方便方法。下面的示例展示了创建一个新表的最小但功能齐全的类需要包括哪些内容:

爪哇

import javax.sql.DataSource;
import org.springframework.jdbc.core.JdbcTemplate;

public class ExecuteAStatement {

    private JdbcTemplate jdbcTemplate;

    public void setDataSource(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }

    public void doExecute() {
        this.jdbcTemplate.execute("create table mytable (id integer, name varchar(100))");
    }
}

Kotlin

import javax.sql.DataSource
import org.springframework.jdbc.core.JdbcTemplate

class ExecuteAStatement(dataSource: DataSource) {

    private val jdbcTemplate = JdbcTemplate(dataSource)

    fun doExecute() {
        jdbcTemplate.execute("create table mytable (id integer, name varchar(100))")
    }
}

# 3.3.5.正在运行的查询

一些查询方法返回一个值。要从一行中检索计数或特定值,请使用queryForObject(..)。后者将返回的 JDBCType转换为作为参数传入的 爪哇 类。如果类型转换无效,则抛出InvalidDataAccessApiUsageException。下面的示例包含两个查询方法,一个用于查询int,另一个用于查询String:

爪哇

import javax.sql.DataSource;
import org.springframework.jdbc.core.JdbcTemplate;

public class RunAQuery {

    private JdbcTemplate jdbcTemplate;

    public void setDataSource(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }

    public int getCount() {
        return this.jdbcTemplate.queryForObject("select count(*) from mytable", Integer.class);
    }

    public String getName() {
        return this.jdbcTemplate.queryForObject("select name from mytable", String.class);
    }
}

Kotlin

import javax.sql.DataSource
import org.springframework.jdbc.core.JdbcTemplate

class RunAQuery(dataSource: DataSource) {

    private val jdbcTemplate = JdbcTemplate(dataSource)

    val count: Int
        get() = jdbcTemplate.queryForObject("select count(*) from mytable")!!

    val name: String?
        get() = jdbcTemplate.queryForObject("select name from mytable")
}

除了单个结果查询方法外,还有几个方法返回一个列表,其中包含查询返回的每一行的条目。最通用的方法是queryForList(..),它返回一个List,其中每个元素都是Map,包含每个列的一个条目,并使用列名作为键。如果在前面的示例中添加一个方法来检索所有行的列表,它可能如下:

爪哇

private JdbcTemplate jdbcTemplate;

public void setDataSource(DataSource dataSource) {
    this.jdbcTemplate = new JdbcTemplate(dataSource);
}

public List<Map<String, Object>> getList() {
    return this.jdbcTemplate.queryForList("select * from mytable");
}

Kotlin

private val jdbcTemplate = JdbcTemplate(dataSource)

fun getList(): List<Map<String, Any>> {
    return jdbcTemplate.queryForList("select * from mytable")
}

返回的列表类似于以下内容:

[{name=Bob, id=1}, {name=Mary, id=2}]

# 3.3.6.更新数据库

下面的示例更新了某个主键的列:

爪哇

import javax.sql.DataSource;
import org.springframework.jdbc.core.JdbcTemplate;

public class ExecuteAnUpdate {

    private JdbcTemplate jdbcTemplate;

    public void setDataSource(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }

    public void setName(int id, String name) {
        this.jdbcTemplate.update("update mytable set name = ? where id = ?", name, id);
    }
}

Kotlin

import javax.sql.DataSource
import org.springframework.jdbc.core.JdbcTemplate

class ExecuteAnUpdate(dataSource: DataSource) {

    private val jdbcTemplate = JdbcTemplate(dataSource)

    fun setName(id: Int, name: String) {
        jdbcTemplate.update("update mytable set name = ? where id = ?", name, id)
    }
}

在前面的示例中,SQL 语句具有行参数的占位符。你可以将参数值以 varargs 的形式传递,也可以以对象数组的形式传递。因此,你应该在原语包装器类中显式地包装原语,或者应该使用自动装箱。

# 3.3.7.检索自动生成的密钥

一种update()方便的方法支持对数据库生成的主键进行检索。这种支持是 JDBC3.0 标准的一部分。有关详细信息,请参见说明书第 13.6 章。该方法以PreparedStatementCreator作为其第一个参数,这就是指定所需 INSERT 语句的方式。另一个参数是KeyHolder,它包含在更新成功返回时生成的键。没有标准的单一方法来创建适当的PreparedStatement(这解释了为什么方法签名是这样的)。以下示例可以在 Oracle 上运行,但可能无法在其他平台上运行:

爪哇

final String INSERT_SQL = "insert into my_test (name) values(?)";
final String name = "Rob";

KeyHolder keyHolder = new GeneratedKeyHolder();
jdbcTemplate.update(connection -> {
    PreparedStatement ps = connection.prepareStatement(INSERT_SQL, new String[] { "id" });
    ps.setString(1, name);
    return ps;
}, keyHolder);

// keyHolder.getKey() now contains the generated key

Kotlin

val INSERT_SQL = "insert into my_test (name) values(?)"
val name = "Rob"

val keyHolder = GeneratedKeyHolder()
jdbcTemplate.update({
    it.prepareStatement (INSERT_SQL, arrayOf("id")).apply { setString(1, name) }
}, keyHolder)

// keyHolder.getKey() now contains the generated key

# 3.4.控制数据库连接

本节内容包括:

  • [使用DataSource]

  • [使用DataSourceUtils](#jdbc-datasourceutils)

  • [实现SmartDataSource]

  • [扩展AbstractDataSource]

  • [使用SingleConnectionDataSource](#JDBC-SingleConnectionDataSource)

  • [使用DriverManagerDataSource]

  • [使用TransactionAwareDataSourceProxy]

  • [使用DataSourceTransactionManager]

# 3.4.1.使用InvalidDataAccessApiUsageException

Spring 通过
获得到数据库的连接。aDataSource是 JDBC 规范的一部分,是一个通用的连接工厂。它允许容器或框架从应用程序代码中隐藏连接池和事务管理问题。作为开发人员,你不需要了解有关如何连接到数据库的详细信息。这是设置数据源的管理员的责任。在开发和测试代码时,你最有可能同时填充这两个角色,但是你并不一定要知道生产数据源是如何配置的。

当你使用 Spring 的 JDBC 层时,你可以从 JNDI 获得数据源,也可以使用第三方提供的连接池实现来配置你自己的数据源。传统的选择是带有 Bean 样式DataSource类的 ApacheCommonsDBCP 和 C3P0;对于现代的 JDBC 连接池,请考虑使用其 Builder 样式的 API 的 HikarICP。

你应该使用AbstractDataSourceSimpleDriverDataSource
(包含在 Spring 发行版中)仅用于测试目的!这些变体不
提供池,并且在发出多个连接请求时性能很差。

下面的部分使用 Spring 的DriverManagerDataSource实现。后面将介绍其他几种DataSource变体。

要配置DriverManagerDataSource:

  1. 获得与DriverManagerDataSource的连接,就像你通常获得 JDBC 连接一样。

  2. 指定 JDBC 驱动程序的完全限定类名,以便DriverManager可以加载驱动程序类。

  3. 提供不同于 JDBC 驱动程序的 URL。(请参阅驱动程序的文档以获得正确的值。

  4. 提供连接到数据库的用户名和密码。

下面的示例展示了如何在 爪哇 中配置DriverManagerDataSource:

爪哇

DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("org.hsqldb.jdbcDriver");
dataSource.setUrl("jdbc:hsqldb:hsql://localhost:");
dataSource.setUsername("sa");
dataSource.setPassword("");

Kotlin

val dataSource = DriverManagerDataSource().apply {
    setDriverClassName("org.hsqldb.jdbcDriver")
    url = "jdbc:hsqldb:hsql://localhost:"
    username = "sa"
    password = ""
}

下面的示例展示了相应的 XML 配置:

<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
    <property name="driverClassName" value="${jdbc.driverClassName}"/>
    <property name="url" value="${jdbc.url}"/>
    <property name="username" value="${jdbc.username}"/>
    <property name="password" value="${jdbc.password}"/>
</bean>

<context:property-placeholder location="jdbc.properties"/>

接下来的两个示例展示了 DBCP 和 C3P0 的基本连接和配置。要了解有助于控制池特性的更多选项,请参阅相应的连接池实现的产品文档。

下面的示例展示了 DBCP 配置:

<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <property name="driverClassName" value="${jdbc.driverClassName}"/>
    <property name="url" value="${jdbc.url}"/>
    <property name="username" value="${jdbc.username}"/>
    <property name="password" value="${jdbc.password}"/>
</bean>

<context:property-placeholder location="jdbc.properties"/>

下面的示例展示了 C3P0 配置:

<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close">
    <property name="driverClass" value="${jdbc.driverClassName}"/>
    <property name="jdbcUrl" value="${jdbc.url}"/>
    <property name="user" value="${jdbc.username}"/>
    <property name="password" value="${jdbc.password}"/>
</bean>

<context:property-placeholder location="jdbc.properties"/>

# 3.4.2.使用DataSourceUtils

DataSourceUtils类是一个方便且功能强大的助手类,它提供static方法来从 JNDI 获得连接,并在必要时提供关闭连接。它支持线程绑定连接,例如,DataSourceTransactionManager

# 3.4.3.实现SmartDataSource

SmartDataSource接口应该由能够提供到关系数据库的连接的类来实现。它扩展了String接口,让使用它的类查询在给定的操作之后是否应该关闭连接。当你知道需要重用某个连接时,这种用法是有效的。

# 3.4.4.扩展AbstractDataSource

AbstractDataSource是 Spring 的abstract实现的基类。它实现了所有DataSource实现所共有的代码。如果你编写自己的DataSource实现,则应该扩展AbstractDataSource类。

# 3.4.5.使用SingleConnectionDataSource

DataSource类是SmartDataSource接口的一个实现,该接口封装单个Connection,该接口在每次使用后都不会关闭。这不是多线程能力。

如果任何客户机代码在假定池连接的情况下调用close(如使用持久性工具时),则应将suppressClose属性设置为true。此设置返回一个封装物理连接的关闭抑制代理。请注意,你不能再将其强制转换为原生 OracleConnection或类似的对象。

SingleConnectionDataSource主要是一个测试类。它通常支持在应用程序服务器之外,结合简单的 JNDI 环境,对代码进行简单的测试。与DriverManagerDataSource相反,它始终重用相同的连接,避免了过多地创建物理连接。

# 3.4.6.使用DriverManagerDataSource

DriverManagerDataSource类是标准DataSource接口的实现,该接口通过 Bean 属性配置一个普通的 JDBC 驱动程序,并且每次返回一个新的Connection

这种实现对于 爪哇 EE 容器之外的测试和独立环境非常有用,可以作为 Spring IoC 容器中的DataSource Bean,也可以与简单的 JNDI 环境结合使用。池-假设DataSource调用关闭连接,那么任何DataSource可感知的持久性代码都应该工作。然而,使用 爪哇Bean 风格的连接池(例如commons-dbcp)非常容易,即使在测试环境中也是如此,因此在DriverManagerDataSource上使用这样的连接池几乎总是更好。

# 3.4.7.使用TransactionAwareDataSourceProxy

TransactionAwareDataSourceProxy是目标DataSource的代理。代理封装了目标DataSource,以添加对 Spring 管理的事务的感知。在这方面,它类似于事务 JNDIDataSource,由 爪哇 EE 服务器提供。

很少希望使用这个类,除非已经存在的代码必须被
调用并传递一个标准的 JDBCDataSourceUtils接口实现。在这种情况下,
仍然可以使该代码可用,同时,使该代码
参与 Spring 托管事务。通常更好的做法是使用用于资源管理的更高级抽象来编写你的
自己的新代码,例如JdbcTemplateDataSourceUtils

参见[STRUCT](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/jdbc/datasource/transactionawardatasourceproxy.html)爪哇doc 以获取更多细节。

# 3.4.8.使用DataSourceTransactionManager

DataSourceTransactionManager类是单个 JDBC 数据源的PlatformTransactionManager实现。它将来自指定数据源的 JDBC 连接绑定到当前正在执行的线程,可能允许每个数据源有一个线程连接。

需要应用程序代码来通过DataSourceUtils.getConnection(DataSource)而不是 爪哇 EE 的标准SimpleJdbcCall检索 JDBC 连接。它抛出未选中的org.springframework.dao异常,而不是选中的SQLExceptions。所有框架类(如JdbcTemplate)都隐式地使用此策略。如果不与此事务管理器一起使用,则查找策略的行为与常见策略完全相同。因此,它可以在任何情况下使用。

DataSourceTransactionManager类支持自定义隔离级别和超时,这些级别和超时将作为适当的 JDBC 语句查询超时应用。为了支持后者,应用程序代码必须使用JdbcTemplate,或者为每个创建的语句调用DataSourceUtils.applyTransactionTimeout(..)方法。

在单资源情况下,你可以使用这个实现,而不是JtaTransactionManager,因为它不需要容器支持 JTA。在两者之间切换只是一个配置问题,只要你坚持所需的连接查找模式。JTA 不支持自定义隔离级别。

# 3.5.JDBC 批处理操作

如果对同一条准备好的语句进行多个调用的批处理,大多数 JDBC 驱动程序都会提供更好的性能。通过将更新分组为批,可以限制往返数据库的次数。

# 3.5.1.带有JdbcTemplate的基本批处理操作

通过实现一个特殊接口的两个方法BatchPreparedStatementSetter,并将该实现作为batchUpdate方法调用中的第二个参数传入,可以完成JdbcTemplate批处理。你可以使用getBatchSize方法来提供当前批处理的大小。你可以使用setValues方法来设置 prepared 语句的参数的值。这个方法被称为在getBatchSize调用中指定的次数。下面的示例基于列表中的条目更新t_actor表,并将整个列表用作批处理:

爪哇

public class JdbcActorDao implements ActorDao {

    private JdbcTemplate jdbcTemplate;

    public void setDataSource(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }

    public int[] batchUpdate(final List<Actor> actors) {
        return this.jdbcTemplate.batchUpdate(
                "update t_actor set first_name = ?, last_name = ? where id = ?",
                new BatchPreparedStatementSetter() {
                    public void setValues(PreparedStatement ps, int i) throws SQLException {
                        Actor actor = actors.get(i);
                        ps.setString(1, actor.getFirstName());
                        ps.setString(2, actor.getLastName());
                        ps.setLong(3, actor.getId().longValue());
                    }
                    public int getBatchSize() {
                        return actors.size();
                    }
                });
    }

    // ... additional methods
}

Kotlin

class JdbcActorDao(dataSource: DataSource) : ActorDao {

    private val jdbcTemplate = JdbcTemplate(dataSource)

    fun batchUpdate(actors: List<Actor>): IntArray {
        return jdbcTemplate.batchUpdate(
                "update t_actor set first_name = ?, last_name = ? where id = ?",
                object: BatchPreparedStatementSetter {
                    override fun setValues(ps: PreparedStatement, i: Int) {
                        ps.setString(1, actors[i].firstName)
                        ps.setString(2, actors[i].lastName)
                        ps.setLong(3, actors[i].id)
                    }

                    override fun getBatchSize() = actors.size
                })
    }

    // ... additional methods
}

如果你处理一个更新流或从一个文件中读取数据,那么你可能有一个首选批处理大小,但是最后一个批处理可能没有那么多条目。在这种情况下,你可以使用InterruptibleBatchPreparedStatementSetter接口,它允许你在输入源耗尽时中断批处理。isBatchExhausted方法让你发出批处理结束的信号。

# 3.5.2.具有对象列表的批处理操作

JdbcTemplateNamedParameterJdbcTemplate都提供了一种提供批更新的替代方式。你不是实现一个特殊的批处理接口,而是以列表的形式提供调用中的所有参数值。框架在这些值上循环,并使用内部准备的语句设置器。API 会有所不同,这取决于你是否使用命名参数。对于已命名的参数,你提供一个SqlParameterSource的数组,这是批处理的每个成员的一个条目。你可以使用SqlParameterSourceUtils.createBatch便利方法来创建这个数组,传入一个由 Bean 样式的对象组成的数组(具有与参数对应的 getter 方法),String-keyedMap实例(包含相应的参数作为值),或者两者的混合。

下面的示例显示了使用命名参数的批处理更新:

爪哇

public class JdbcActorDao implements ActorDao {

    private NamedParameterTemplate namedParameterJdbcTemplate;

    public void setDataSource(DataSource dataSource) {
        this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
    }

    public int[] batchUpdate(List<Actor> actors) {
        return this.namedParameterJdbcTemplate.batchUpdate(
                "update t_actor set first_name = :firstName, last_name = :lastName where id = :id",
                SqlParameterSourceUtils.createBatch(actors));
    }

    // ... additional methods
}

Kotlin

class JdbcActorDao(dataSource: DataSource) : ActorDao {

    private val namedParameterJdbcTemplate = NamedParameterJdbcTemplate(dataSource)

    fun batchUpdate(actors: List<Actor>): IntArray {
        return this.namedParameterJdbcTemplate.batchUpdate(
                "update t_actor set first_name = :firstName, last_name = :lastName where id = :id",
                SqlParameterSourceUtils.createBatch(actors));
    }

        // ... additional methods
}

对于使用经典?占位符的 SQL 语句,你将传入一个包含带更新值的对象数组的列表。这个对象数组必须为 SQL 语句中的每个占位符有一个条目,并且它们的顺序必须与 SQL 语句中定义的顺序相同。

下面的示例与前面的示例相同,只是它使用了经典的 JDBC?占位符:

爪哇

public class JdbcActorDao implements ActorDao {

    private JdbcTemplate jdbcTemplate;

    public void setDataSource(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }

    public int[] batchUpdate(final List<Actor> actors) {
        List<Object[]> batch = new ArrayList<Object[]>();
        for (Actor actor : actors) {
            Object[] values = new Object[] {
                    actor.getFirstName(), actor.getLastName(), actor.getId()};
            batch.add(values);
        }
        return this.jdbcTemplate.batchUpdate(
                "update t_actor set first_name = ?, last_name = ? where id = ?",
                batch);
    }

    // ... additional methods
}

Kotlin

class JdbcActorDao(dataSource: DataSource) : ActorDao {

    private val jdbcTemplate = JdbcTemplate(dataSource)

    fun batchUpdate(actors: List<Actor>): IntArray {
        val batch = mutableListOf<Array<Any>>()
        for (actor in actors) {
            batch.add(arrayOf(actor.firstName, actor.lastName, actor.id))
        }
        return jdbcTemplate.batchUpdate(
                "update t_actor set first_name = ?, last_name = ? where id = ?", batch)
    }

    // ... additional methods
}

我们前面描述的所有批更新方法都返回一个int数组,该数组包含每个批处理条目的受影响行数。这个计数是由 JDBC 驱动程序报告的。如果计数不可用,则 JDBC 驱动程序返回一个-2的值。

在这样的场景中,在底层PreparedStatement上自动设定值,
每个值对应的 JDBC 类型需要从给定的 爪哇 类型派生。
虽然这通常工作得很好,但可能会出现问题(例如,对于包含映射的null值)。 Spring,默认情况下,在这样的ParameterMetaData.getParameterType情况下调用
,这在使用你的 JDBC 驱动程序时可能是昂贵的。你应该使用最近的驱动程序
版本,并考虑将spring.jdbc.getParameterType.ignore属性设置为true(作为 JVM 系统属性或通过[SpringProperties](Appendix.html#Appendix- Spring-properties)机制),如果你遇到
性能问题(正如 Oracle12c、JBoss 和 PostgreSQL 所报告的那样)。

或者,你可以考虑通过BatchPreparedStatementSetter(如前面所示),通过显式类型
,通过List<Object[]>调用的显式类型
,显式地指定相应的 JDBC 类型,通过registerSqlType调用
自定义MapSqlParameterSource实例,或者通过BeanPropertySqlParameterSource从 爪哇 声明的属性类型派生 SQL 类型,甚至对于空值也是如此。

# 3.5.3.具有多批处理的批处理操作

前面的批更新示例处理的批是如此之大,以至于你希望将它们分解为几个较小的批。你可以通过多次调用batchUpdate方法来使用前面提到的方法来实现这一点,但是现在有了一个更方便的方法。除了 SQL 语句之外,该方法还接受一个Collection的对象,这些对象包含参数,每个批处理要进行的更新的数量,以及一个ParameterizedPreparedStatementSetter来设置准备语句的参数值。框架在提供的值上循环,并将更新调用分成指定大小的批。

下面的示例显示了一个使用批处理大小为 100 的批处理更新:

爪哇

public class JdbcActorDao implements ActorDao {

    private JdbcTemplate jdbcTemplate;

    public void setDataSource(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }

    public int[][] batchUpdate(final Collection<Actor> actors) {
        int[][] updateCounts = jdbcTemplate.batchUpdate(
                "update t_actor set first_name = ?, last_name = ? where id = ?",
                actors,
                100,
                (PreparedStatement ps, Actor actor) -> {
                    ps.setString(1, actor.getFirstName());
                    ps.setString(2, actor.getLastName());
                    ps.setLong(3, actor.getId().longValue());
                });
        return updateCounts;
    }

    // ... additional methods
}

Kotlin

class JdbcActorDao(dataSource: DataSource) : ActorDao {

    private val jdbcTemplate = JdbcTemplate(dataSource)

    fun batchUpdate(actors: List<Actor>): Array<IntArray> {
        return jdbcTemplate.batchUpdate(
                    "update t_actor set first_name = ?, last_name = ? where id = ?",
                    actors, 100) { ps, argument ->
            ps.setString(1, argument.firstName)
            ps.setString(2, argument.lastName)
            ps.setLong(3, argument.id)
        }
    }

    // ... additional methods
}

此调用的批更新方法返回一个int数组的数组,其中包含每个批处理的数组条目,以及每个更新的受影响行数组。顶级数组的长度表示运行的批处理的数量,而二级数组的长度表示该批处理中的更新数量。每个批处理中的更新数量应该是为所有批处理提供的批处理大小(除了最后一个可能更小),这取决于所提供的更新对象的总数。每个更新语句的更新计数是 JDBC 驱动程序报告的。如果计数不可用,则 JDBC 驱动程序返回一个-2的值。

# 3.6.使用SimpleJdbc类简化 JDBC 操作

SimpleJdbcInsertSimpleJdbcCall类利用可以通过 JDBC 驱动程序检索的数据库元数据,提供了一种简化的配置。这意味着你需要预先配置的内容较少,但是如果你希望在代码中提供所有细节,则可以重写或关闭元数据处理。

# 3.6.1.使用SimpleJdbcInsert插入数据

我们首先查看配置选项最少的SimpleJdbcInsert类。你应该在数据访问层的初始化方法中实例化SimpleJdbcInsert。对于这个示例,初始化方法是setDataSource方法。你不需要子类SimpleJdbcInsert类。相反,你可以使用withTableName方法创建一个新实例并设置表名。该类的配置方法遵循fluid样式,该样式返回withTableName的实例,它允许你链接所有配置方法。下面的示例只使用一种配置方法(稍后我们将展示多个方法的示例):

爪哇

public class JdbcActorDao implements ActorDao {

    private SimpleJdbcInsert insertActor;

    public void setDataSource(DataSource dataSource) {
        this.insertActor = new SimpleJdbcInsert(dataSource).withTableName("t_actor");
    }

    public void add(Actor actor) {
        Map<String, Object> parameters = new HashMap<String, Object>(3);
        parameters.put("id", actor.getId());
        parameters.put("first_name", actor.getFirstName());
        parameters.put("last_name", actor.getLastName());
        insertActor.execute(parameters);
    }

    // ... additional methods
}

Kotlin

class JdbcActorDao(dataSource: DataSource) : ActorDao {

    private val insertActor = SimpleJdbcInsert(dataSource).withTableName("t_actor")

    fun add(actor: Actor) {
        val parameters = mutableMapOf<String, Any>()
        parameters["id"] = actor.id
        parameters["first_name"] = actor.firstName
        parameters["last_name"] = actor.lastName
        insertActor.execute(parameters)
    }

    // ... additional methods
}

这里使用的execute方法将一个普通的java.util.Map作为其唯一参数。这里需要注意的重要事项是,用于withTableName的键必须与数据库中定义的表的列名匹配。这是因为我们读取元数据来构造实际的 INSERT 语句。

# 3.6.2.使用SimpleJdbcInsert检索自动生成的密钥

下一个示例使用与前一个示例相同的 INSERT,但是,它不是传入id,而是检索自动生成的键并将其设置在新的withTableName对象上。当它创建SimpleJdbcInsert时,除了指定表名外,它还使用usingGeneratedKeyColumns方法指定生成的键列的名称。下面的清单展示了它的工作原理:

爪哇

public class JdbcActorDao implements ActorDao {

    private SimpleJdbcInsert insertActor;

    public void setDataSource(DataSource dataSource) {
        this.insertActor = new SimpleJdbcInsert(dataSource)
                .withTableName("t_actor")
                .usingGeneratedKeyColumns("id");
    }

    public void add(Actor actor) {
        Map<String, Object> parameters = new HashMap<String, Object>(2);
        parameters.put("first_name", actor.getFirstName());
        parameters.put("last_name", actor.getLastName());
        Number newId = insertActor.executeAndReturnKey(parameters);
        actor.setId(newId.longValue());
    }

    // ... additional methods
}

Kotlin

class JdbcActorDao(dataSource: DataSource) : ActorDao {

    private val insertActor = SimpleJdbcInsert(dataSource)
            .withTableName("t_actor").usingGeneratedKeyColumns("id")

    fun add(actor: Actor): Actor {
        val parameters = mapOf(
                "first_name" to actor.firstName,
                "last_name" to actor.lastName)
        val newId = insertActor.executeAndReturnKey(parameters);
        return actor.copy(id = newId.toLong())
    }

    // ... additional methods
}

在使用第二种方法运行 INSERT 时,主要的区别是你没有将id添加到Map中,而是调用executeAndReturnKey方法。这返回一个java.lang.Number对象,你可以使用它创建域类中使用的数值类型的实例。在这里,你不能依赖所有的数据库来返回特定的 爪哇 类。java.lang.Number是你可以依赖的基类。如果有多个自动生成的列,或者生成的值是非数字的,则可以使用从executeAndReturnKeyHolder方法返回的KeyHolder

# 3.6.3.指定executeAndReturnKeyHolder的列

可以通过使用usingColumns方法指定列名列表来限制插入的列,如下例所示:

爪哇

public class JdbcActorDao implements ActorDao {

    private SimpleJdbcInsert insertActor;

    public void setDataSource(DataSource dataSource) {
        this.insertActor = new SimpleJdbcInsert(dataSource)
                .withTableName("t_actor")
                .usingColumns("first_name", "last_name")
                .usingGeneratedKeyColumns("id");
    }

    public void add(Actor actor) {
        Map<String, Object> parameters = new HashMap<String, Object>(2);
        parameters.put("first_name", actor.getFirstName());
        parameters.put("last_name", actor.getLastName());
        Number newId = insertActor.executeAndReturnKey(parameters);
        actor.setId(newId.longValue());
    }

    // ... additional methods
}

Kotlin

class JdbcActorDao(dataSource: DataSource) : ActorDao {

    private val insertActor = SimpleJdbcInsert(dataSource)
            .withTableName("t_actor")
            .usingColumns("first_name", "last_name")
            .usingGeneratedKeyColumns("id")

    fun add(actor: Actor): Actor {
        val parameters = mapOf(
                "first_name" to actor.firstName,
                "last_name" to actor.lastName)
        val newId = insertActor.executeAndReturnKey(parameters);
        return actor.copy(id = newId.toLong())
    }

    // ... additional methods
}

INSERT 的执行与依赖元数据来确定要使用哪些列的情况相同。

# 3.6.4.使用SqlParameterSource提供参数值

使用Map提供参数值可以很好地工作,但它不是使用最方便的类。 Spring 提供了可以替代使用的SqlParameterSource接口的两个实现方式。第一个是BeanPropertySqlParameterSource,如果你有一个兼容 爪哇Bean 的类,其中包含你的值,那么它是一个非常方便的类。它采用相应的 getter 方法提取参数值。下面的示例展示了如何使用BeanPropertySqlParameterSource:

爪哇

public class JdbcActorDao implements ActorDao {

    private SimpleJdbcInsert insertActor;

    public void setDataSource(DataSource dataSource) {
        this.insertActor = new SimpleJdbcInsert(dataSource)
                .withTableName("t_actor")
                .usingGeneratedKeyColumns("id");
    }

    public void add(Actor actor) {
        SqlParameterSource parameters = new BeanPropertySqlParameterSource(actor);
        Number newId = insertActor.executeAndReturnKey(parameters);
        actor.setId(newId.longValue());
    }

    // ... additional methods
}

Kotlin

class JdbcActorDao(dataSource: DataSource) : ActorDao {

    private val insertActor = SimpleJdbcInsert(dataSource)
            .withTableName("t_actor")
            .usingGeneratedKeyColumns("id")

    fun add(actor: Actor): Actor {
        val parameters = BeanPropertySqlParameterSource(actor)
        val newId = insertActor.executeAndReturnKey(parameters)
        return actor.copy(id = newId.toLong())
    }

    // ... additional methods
}

另一个选项是MapSqlParameterSource,它类似于Map,但提供了一个更方便的addValue方法,可以链接。下面的示例展示了如何使用它:

爪哇

public class JdbcActorDao implements ActorDao {

    private SimpleJdbcInsert insertActor;

    public void setDataSource(DataSource dataSource) {
        this.insertActor = new SimpleJdbcInsert(dataSource)
                .withTableName("t_actor")
                .usingGeneratedKeyColumns("id");
    }

    public void add(Actor actor) {
        SqlParameterSource parameters = new MapSqlParameterSource()
                .addValue("first_name", actor.getFirstName())
                .addValue("last_name", actor.getLastName());
        Number newId = insertActor.executeAndReturnKey(parameters);
        actor.setId(newId.longValue());
    }

    // ... additional methods
}

Kotlin

class JdbcActorDao(dataSource: DataSource) : ActorDao {

    private val insertActor = SimpleJdbcInsert(dataSource)
            .withTableName("t_actor")
            .usingGeneratedKeyColumns("id")

    fun add(actor: Actor): Actor {
        val parameters = MapSqlParameterSource()
                    .addValue("first_name", actor.firstName)
                    .addValue("last_name", actor.lastName)
        val newId = insertActor.executeAndReturnKey(parameters)
        return actor.copy(id = newId.toLong())
    }

    // ... additional methods
}

正如你所看到的,配置是相同的。只有执行代码必须更改才能使用这些可选的输入类。

# 3.6.5.用SimpleJdbcCall调用存储过程

SimpleJdbcCall类使用数据库中的元数据查找inSqlParameterSource参数的名称,这样你就不必显式地声明它们。如果你愿意这样做,或者如果你的参数(例如ARRAYSTRUCT)没有自动映射到 爪哇 类,则可以声明参数。第一个示例显示了一个简单的过程,该过程仅从 MySQL 数据库返回VARCHARDATE格式中的标量值。示例过程读取指定的参与者条目,并以out参数的形式返回first_namelast_namebirth_date列。下面的清单展示了第一个示例:

CREATE PROCEDURE read_actor (
    IN in_id INTEGER,
    OUT out_first_name VARCHAR(100),
    OUT out_last_name VARCHAR(100),
    OUT out_birth_date DATE)
BEGIN
    SELECT first_name, last_name, birth_date
    INTO out_first_name, out_last_name, out_birth_date
    FROM t_actor where id = in_id;
END;

in_id参数包含你正在查找的参与者的idSTRUCT参数返回从表中读取的数据。

可以以类似于声明SimpleJdbcInsert的方式声明SimpleJdbcCall。你应该在数据访问层的初始化方法中实例化和配置类。与StoredProcedure类相比,不需要创建子类,也不需要声明可以在数据库元数据中查找的参数。下面的SimpleJdbcCall配置示例使用了前面的存储过程(除了DataSource之外,唯一的配置选项是存储过程的名称):

爪哇

public class JdbcActorDao implements ActorDao {

    private SimpleJdbcCall procReadActor;

    public void setDataSource(DataSource dataSource) {
        this.procReadActor = new SimpleJdbcCall(dataSource)
                .withProcedureName("read_actor");
    }

    public Actor readActor(Long id) {
        SqlParameterSource in = new MapSqlParameterSource()
                .addValue("in_id", id);
        Map out = procReadActor.execute(in);
        Actor actor = new Actor();
        actor.setId(id);
        actor.setFirstName((String) out.get("out_first_name"));
        actor.setLastName((String) out.get("out_last_name"));
        actor.setBirthDate((Date) out.get("out_birth_date"));
        return actor;
    }

    // ... additional methods
}

Kotlin

class JdbcActorDao(dataSource: DataSource) : ActorDao {

    private val procReadActor = SimpleJdbcCall(dataSource)
            .withProcedureName("read_actor")

    fun readActor(id: Long): Actor {
        val source = MapSqlParameterSource().addValue("in_id", id)
        val output = procReadActor.execute(source)
        return Actor(
                id,
                output["out_first_name"] as String,
                output["out_last_name"] as String,
                output["out_birth_date"] as Date)
    }

        // ... additional methods
}

你为执行调用而编写的代码涉及创建一个包含 IN 参数的SqlParameterSource。你必须将为输入值提供的名称与存储过程中声明的参数名称的名称匹配。这种情况不一定要匹配,因为你使用元数据来确定在存储过程中应该如何引用数据库对象。源文件中为存储过程指定的内容不一定是存储在数据库中的方式。一些数据库将名称转换为所有大写,而另一些数据库则使用小写或使用指定的大小写。

execute方法接受 IN 参数并返回一个Map,该参数包含由名称键入的任何out参数,如在存储过程中指定的那样。在这种情况下,它们是out_first_nameout_last_nameout_birth_date

execute方法的最后一部分将创建一个Actor实例,用于返回检索到的数据。同样,使用在存储过程中声明的out参数的名称也很重要。而且,存储在结果映射中的out参数的名称与数据库中的SqlParameterSourceUtils.createBatch参数名称的情况匹配,这在不同的数据库中可能会有所不同。为了使代码更具可移植性,你应该进行不区分大小写的查找,或者指示 Spring 使用LinkedCaseInsensitiveMap。要实现后者,你可以创建自己的JdbcTemplate,并将setResultsMapCaseInsensitive属性设置为true。然后,你可以将这个定制的JdbcTemplate实例传递到你的SimpleJdbcCall的构造函数中。下面的示例展示了这种配置:

爪哇

public class JdbcActorDao implements ActorDao {

    private SimpleJdbcCall procReadActor;

    public void setDataSource(DataSource dataSource) {
        JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
        jdbcTemplate.setResultsMapCaseInsensitive(true);
        this.procReadActor = new SimpleJdbcCall(jdbcTemplate)
                .withProcedureName("read_actor");
    }

    // ... additional methods
}

Kotlin

class JdbcActorDao(dataSource: DataSource) : ActorDao {

    private var procReadActor = SimpleJdbcCall(JdbcTemplate(dataSource).apply {
        isResultsMapCaseInsensitive = true
    }).withProcedureName("read_actor")

    // ... additional methods
}

通过执行此操作,可以避免在用于返回的out参数的名称的情况下发生冲突。

# 3.6.6.显式声明用于SimpleJdbcCall的参数

在本章的前面,我们描述了参数是如何从元数据推导出来的,但是如果你愿意,你可以显式地声明它们。你可以通过使用declareParameters方法创建和配置SimpleJdbcCall来实现这一点,该方法接受数量可变的SqlParameter对象作为输入。有关如何定义SqlParameter的详细信息,请参见下一节

如果你使用的数据库不是支持 Spring 的
数据库,则需要显式声明。目前, Spring 支持
以下数据库的存储过程调用的元数据查找:Apache Derby、DB2、MySQL、Microsoft SQL Server、Oracle 和 Sybase。
我们还支持 MySQL、Microsoft SQL Server、
和 Oracle 的存储函数的元数据查找。

你可以 OPT 显式地声明一个、一些或所有参数。在未显式声明参数的情况下,仍使用参数元数据。要绕过对潜在参数的元数据查找的所有处理并仅使用声明的参数,你可以调用方法withoutProcedureColumnMetaDataAccess作为声明的一部分。假设你为一个数据库函数声明了两个或多个不同的调用签名。在这种情况下,调用useInParameterNames来指定给定签名要包含的 IN 参数名列表。

下面的示例展示了一个完全声明的过程调用,并使用了前面示例中的信息:

爪哇

public class JdbcActorDao implements ActorDao {

    private SimpleJdbcCall procReadActor;

    public void setDataSource(DataSource dataSource) {
        JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
        jdbcTemplate.setResultsMapCaseInsensitive(true);
        this.procReadActor = new SimpleJdbcCall(jdbcTemplate)
                .withProcedureName("read_actor")
                .withoutProcedureColumnMetaDataAccess()
                .useInParameterNames("in_id")
                .declareParameters(
                        new SqlParameter("in_id", Types.NUMERIC),
                        new SqlOutParameter("out_first_name", Types.VARCHAR),
                        new SqlOutParameter("out_last_name", Types.VARCHAR),
                        new SqlOutParameter("out_birth_date", Types.DATE)
                );
    }

    // ... additional methods
}

Kotlin

class JdbcActorDao(dataSource: DataSource) : ActorDao {

        private val procReadActor = SimpleJdbcCall(JdbcTemplate(dataSource).apply {
            isResultsMapCaseInsensitive = true
        }).withProcedureName("read_actor")
                .withoutProcedureColumnMetaDataAccess()
                .useInParameterNames("in_id")
                .declareParameters(
                        SqlParameter("in_id", Types.NUMERIC),
                        SqlOutParameter("out_first_name", Types.VARCHAR),
                        SqlOutParameter("out_last_name", Types.VARCHAR),
                        SqlOutParameter("out_birth_date", Types.DATE)
    )

        // ... additional methods
}

这两个示例的执行和最终结果是相同的。第二个示例显式地指定了所有细节,而不是依赖于元数据。

# 3.6.7.如何定义SqlParameters

要为SimpleJdbc类和 RDBMS 操作类(在将 JDBC 操作建模为 爪哇 对象中涵盖)定义参数,你可以使用SqlParameter或它的一个子类。为此,你通常在构造函数中指定参数名和 SQL 类型。SQL 类型是通过使用java.sql.Types常量指定的。在本章的前面,我们看到了类似的声明:

爪哇

new SqlParameter("in_id", Types.NUMERIC),
new SqlOutParameter("out_first_name", Types.VARCHAR),

Kotlin

SqlParameter("in_id", Types.NUMERIC),
SqlOutParameter("out_first_name", Types.VARCHAR),

带有SqlParameter的第一行声明了一个 IN 参数。你可以通过使用SqlQuery及其子类(在[understandingSqlQuery](#JDBC-SQLQuery)中涵盖),将参数用于存储过程调用和查询。

第二行(带有SqlOutParameter)声明一个out参数,该参数将在存储过程调用中使用。对于InOut参数也有SqlInOutParameter(这些参数为过程提供了一个 IN 值,并且还返回了一个值)。

只有声明为SqlParameterSqlInOutParameter的参数用于
提供输入值。这与StoredProcedure类不同,后者(出于
向后兼容性的原因)允许为参数
提供输入值,声明为SqlOutParameter

对于 IN 参数,除了名称和 SQL 类型之外,你还可以为数字数据指定一个比例,或者为自定义数据库类型指定一个类型名称。对于out参数,可以提供SqlParameters来处理从REF游标返回的行的映射。另一种选择是指定SqlReturnType,它提供了定义对返回值的定制处理的机会。

# 3.6.8.使用SimpleJdbcCall调用存储函数

你可以以调用存储过程的几乎相同的方式调用存储函数,只是提供了一个函数名而不是过程名。将withFunctionName方法用作配置的一部分,以指示你想要对函数进行调用,并为函数调用生成相应的字符串。一个专门的调用(executeFunction)用于运行函数,它以指定类型的对象返回函数返回值,这意味着你不必从结果映射中检索返回值。对于只有一个out参数的存储过程,也可以使用类似的方便方法(名为executeObject)。下面的示例(对于 MySQL)基于一个名为get_actor_name的存储函数,该函数返回参与者的全名:

CREATE FUNCTION get_actor_name (in_id INTEGER)
RETURNS VARCHAR(200) READS SQL DATA
BEGIN
    DECLARE out_name VARCHAR(200);
    SELECT concat(first_name, ' ', last_name)
        INTO out_name
        FROM t_actor where id = in_id;
    RETURN out_name;
END;

要调用这个函数,我们再次在初始化方法中创建SimpleJdbcCall,如下例所示:

爪哇

public class JdbcActorDao implements ActorDao {

    private SimpleJdbcCall funcGetActorName;

    public void setDataSource(DataSource dataSource) {
        JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
        jdbcTemplate.setResultsMapCaseInsensitive(true);
        this.funcGetActorName = new SimpleJdbcCall(jdbcTemplate)
                .withFunctionName("get_actor_name");
    }

    public String getActorName(Long id) {
        SqlParameterSource in = new MapSqlParameterSource()
                .addValue("in_id", id);
        String name = funcGetActorName.executeFunction(String.class, in);
        return name;
    }

    // ... additional methods
}

Kotlin

class JdbcActorDao(dataSource: DataSource) : ActorDao {

    private val jdbcTemplate = JdbcTemplate(dataSource).apply {
        isResultsMapCaseInsensitive = true
    }
    private val funcGetActorName = SimpleJdbcCall(jdbcTemplate)
            .withFunctionName("get_actor_name")

    fun getActorName(id: Long): String {
        val source = MapSqlParameterSource().addValue("in_id", id)
        return funcGetActorName.executeFunction(String::class.java, source)
    }

    // ... additional methods
}

使用的executeFunction方法返回一个String,它包含函数调用的返回值。

# 3.6.9.从SimpleJdbcCall返回RowMapper或 ref 游标

调用返回结果集的存储过程或函数有点困难。一些数据库在 JDBC 结果处理过程中返回结果集,而另一些数据库则需要显式注册特定类型的out参数。这两种方法都需要额外的处理来循环结果集并处理返回的行。使用SimpleJdbcCall,你可以使用returningResultSet方法并声明用于特定参数的RowMapper实现。如果在结果处理过程中返回了结果集,则没有定义名称,因此返回的结果必须与声明RowMapper实现的顺序匹配。指定的名称仍用于将已处理的结果列表存储在从execute语句返回的结果映射中。

下一个示例(对于 MySQL)使用一个存储过程,该过程接受 no in 参数并返回t_actor表中的所有行:

CREATE PROCEDURE read_all_actors()
BEGIN
 SELECT a.id, a.first_name, a.last_name, a.birth_date FROM t_actor a;
END;

要调用这个过程,你可以声明RowMapper。因为要映射到的类遵循 爪哇Bean 规则,所以可以使用BeanPropertyRowMapper,它是通过在newInstance方法中传递要映射到的类来创建的。下面的示例展示了如何做到这一点:

爪哇

public class JdbcActorDao implements ActorDao {

    private SimpleJdbcCall procReadAllActors;

    public void setDataSource(DataSource dataSource) {
        JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
        jdbcTemplate.setResultsMapCaseInsensitive(true);
        this.procReadAllActors = new SimpleJdbcCall(jdbcTemplate)
                .withProcedureName("read_all_actors")
                .returningResultSet("actors",
                BeanPropertyRowMapper.newInstance(Actor.class));
    }

    public List getActorsList() {
        Map m = procReadAllActors.execute(new HashMap<String, Object>(0));
        return (List) m.get("actors");
    }

    // ... additional methods
}

Kotlin

class JdbcActorDao(dataSource: DataSource) : ActorDao {

        private val procReadAllActors = SimpleJdbcCall(JdbcTemplate(dataSource).apply {
            isResultsMapCaseInsensitive = true
        }).withProcedureName("read_all_actors")
                .returningResultSet("actors",
                        BeanPropertyRowMapper.newInstance(Actor::class.java))

    fun getActorsList(): List<Actor> {
        val m = procReadAllActors.execute(mapOf<String, Any>())
        return m["actors"] as List<Actor>
    }

    // ... additional methods
}

execute调用传入一个空的Map,因为这个调用不接受任何参数。然后从结果映射中检索参与者列表,并将其返回给调用者。

# 3.7.将 JDBC 操作建模为 爪哇 对象

org.springframework.jdbc.object包包含允许你以更面向对象的方式访问数据库的类。例如,你可以运行查询并以列表的形式获得结果,该列表包含业务对象,并将关系列数据映射到业务对象的属性。你还可以运行存储过程和运行更新、删除和插入语句。

许多 Spring 开发人员认为,下面描述的各种 RDBMS 操作类
(除了[StoredProcedure](#JDBC-storedProcedure)类)通常可以将
替换为直接的JdbcTemplate调用。通常,编写一个直接在JdbcTemplate上调用方法的 DAO
方法更简单(而不是
将查询封装为一个成熟的类)。

但是,如果你正在通过使用 RDBMS 操作类获得可测量的值,
你应该继续使用这些类。

# 3.7.1.理解SqlQuery

SqlQuery是一个可重用的、线程安全的类,它封装了一个 SQL 查询。子类必须实现SqlParameterSource方法,以提供一个RowMapper实例,该实例可以通过在执行查询期间创建的ResultSet上进行迭代,为每行创建一个对象。很少直接使用SqlQuery类,因为MappingSqlQuery子类为将行映射到 Java 类提供了一个更方便的实现。扩展SqlQuery的其他实现是MappingSqlQueryWithParametersUpdatableSqlQuery

# 3.7.2.使用MappingSqlQuery

MappingSqlQuery是一个可重用的查询,在这个查询中,具体的子类必须实现抽象的mapRow(..)方法,以将提供的ResultSet的每一行转换为指定类型的对象。下面的示例显示了一个自定义查询,该查询将来自t_actor关系的数据映射到Actor类的实例:

Java

public class ActorMappingQuery extends MappingSqlQuery<Actor> {

    public ActorMappingQuery(DataSource ds) {
        super(ds, "select id, first_name, last_name from t_actor where id = ?");
        declareParameter(new SqlParameter("id", Types.INTEGER));
        compile();
    }

    @Override
    protected Actor mapRow(ResultSet rs, int rowNumber) throws SQLException {
        Actor actor = new Actor();
        actor.setId(rs.getLong("id"));
        actor.setFirstName(rs.getString("first_name"));
        actor.setLastName(rs.getString("last_name"));
        return actor;
    }
}

Kotlin

class ActorMappingQuery(ds: DataSource) : MappingSqlQuery<Actor>(ds, "select id, first_name, last_name from t_actor where id = ?") {

    init {
        declareParameter(SqlParameter("id", Types.INTEGER))
        compile()
    }

    override fun mapRow(rs: ResultSet, rowNumber: Int) = Actor(
            rs.getLong("id"),
            rs.getString("first_name"),
            rs.getString("last_name")
    )
}

该类扩展了MappingSqlQuery参数化的Actor类型。此客户查询的构造函数将DataSource作为唯一的参数。在此构造函数中,你可以使用DataSource调用超类上的构造函数,并调用应该运行的 SQL 来检索此查询的行。此 SQL 用于创建PreparedStatement,因此它可能包含用于在执行过程中传递的任何参数的占位符。你必须使用declareParameter方法声明每个参数,并传入SqlParameterSqlParameter接受一个名称和在java.sql.Types中定义的 JDBC 类型。在定义了所有参数之后,你可以调用compile()方法,这样就可以准备语句并在以后运行它。这个类在编译后是线程安全的,因此,只要在初始化 DAO 时创建了这些实例,它们就可以保留为实例变量并被重用。下面的示例展示了如何定义这样的类:

Java

private ActorMappingQuery actorMappingQuery;

@Autowired
public void setDataSource(DataSource dataSource) {
    this.actorMappingQuery = new ActorMappingQuery(dataSource);
}

public Customer getCustomer(Long id) {
    return actorMappingQuery.findObject(id);
}

Kotlin

private val actorMappingQuery = ActorMappingQuery(dataSource)

fun getCustomer(id: Long) = actorMappingQuery.findObject(id)

前面示例中的方法使用id检索作为唯一参数传入的客户。由于我们只希望返回一个对象,所以我们调用findObject便利方法,并以id作为参数。如果我们有一个返回对象列表并获取附加参数的查询,那么我们将使用execute方法中的一个,该方法接受以 vargs 形式传递的参数值数组。下面的示例展示了这样的方法:

Java

public List<Actor> searchForActors(int age, String namePattern) {
    List<Actor> actors = actorSearchMappingQuery.execute(age, namePattern);
    return actors;
}

Kotlin

fun searchForActors(age: Int, namePattern: String) =
            actorSearchMappingQuery.execute(age, namePattern)

# 3.7.3.使用SqlUpdate

SqlUpdate类封装了一个 SQL 更新。与查询一样,更新对象是可重用的,并且,与所有RdbmsOperation类一样,更新可以有参数,并在 SQL 中定义。这个类提供了许多update(..)方法,类似于查询对象的execute(..)方法。SqlUpdate类是具体的。它可以被子类——例如,添加一个自定义更新方法。但是,你不必子类SqlUpdate类,因为可以通过设置 SQL 和声明参数来轻松地对它进行参数化。下面的示例创建了一个名为execute的自定义更新方法:

Java

import java.sql.Types;
import javax.sql.DataSource;
import org.springframework.jdbc.core.SqlParameter;
import org.springframework.jdbc.object.SqlUpdate;

public class UpdateCreditRating extends SqlUpdate {

    public UpdateCreditRating(DataSource ds) {
        setDataSource(ds);
        setSql("update customer set credit_rating = ? where id = ?");
        declareParameter(new SqlParameter("creditRating", Types.NUMERIC));
        declareParameter(new SqlParameter("id", Types.NUMERIC));
        compile();
    }

    /**
     * @param id for the Customer to be updated
     * @param rating the new value for credit rating
     * @return number of rows updated
     */
    public int execute(int id, int rating) {
        return update(rating, id);
    }
}

Kotlin

import java.sql.Types
import javax.sql.DataSource
import org.springframework.jdbc.core.SqlParameter
import org.springframework.jdbc.object.SqlUpdate

class UpdateCreditRating(ds: DataSource) : SqlUpdate() {

    init {
        setDataSource(ds)
        sql = "update customer set credit_rating = ? where id = ?"
        declareParameter(SqlParameter("creditRating", Types.NUMERIC))
        declareParameter(SqlParameter("id", Types.NUMERIC))
        compile()
    }

    /**
     * @param id for the Customer to be updated
     * @param rating the new value for credit rating
     * @return number of rows updated
     */
    fun execute(id: Int, rating: Int): Int {
        return update(rating, id)
    }
}

# 3.7.4.使用StoredProcedure

StoredProcedure类是用于 RDBMS 存储过程的对象抽象的abstract超类。

继承的sql属性是 RDBMS 中存储过程的名称。

要为StoredProcedure类定义参数,可以使用SqlParameter或它的一个子类。你必须在构造函数中指定参数名和 SQL 类型,如下列代码片段所示:

Java

new SqlParameter("in_id", Types.NUMERIC),
new SqlOutParameter("out_first_name", Types.VARCHAR),

Kotlin

SqlParameter("in_id", Types.NUMERIC),
SqlOutParameter("out_first_name", Types.VARCHAR),

SQL 类型是使用java.sql.Types常量指定的。

第一行(带有SqlParameter)声明一个 IN 参数。你可以在参数中使用SqlQuery及其子类的存储过程调用和查询(在[understandingSqlQuery](#JDBC-SQLQuery)中涉及)。

第二行(带有SqlOutParameter)声明一个out参数,该参数将在存储过程调用中使用。对于InOut参数也有SqlInOutParameter(这些参数为过程提供了一个in值,并且还返回了一个值)。

对于in参数,除了名称和 SQL 类型之外,还可以为数字数据指定一个比例,或者为自定义数据库类型指定一个类型名称。对于out参数,可以提供RowMapper来处理从REF游标返回的行的映射。另一个选项是指定SqlReturnType,它允许你定义对返回值的定制处理。

简单 DAO 的下一个示例使用StoredProcedure调用函数(sysdate()),该函数与任何 Oracle 数据库一起提供。要使用存储过程功能,你必须创建一个扩展StoredProcedure的类。在本例中,StoredProcedure类是一个内部类。但是,如果需要重用StoredProcedure,则可以将其声明为顶级类。这个示例没有输入参数,但是通过使用SqlOutParameter类将输出参数声明为日期类型。execute()方法运行过程,并从结果Map中提取返回的日期。通过使用参数名称作为键,resultsMap为每个声明的输出参数(在本例中只有一个)有一个条目。下面的清单显示了我们的自定义存储过程类:

Java

import java.sql.Types;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.SqlOutParameter;
import org.springframework.jdbc.object.StoredProcedure;

public class StoredProcedureDao {

    private GetSysdateProcedure getSysdate;

    @Autowired
    public void init(DataSource dataSource) {
        this.getSysdate = new GetSysdateProcedure(dataSource);
    }

    public Date getSysdate() {
        return getSysdate.execute();
    }

    private class GetSysdateProcedure extends StoredProcedure {

        private static final String SQL = "sysdate";

        public GetSysdateProcedure(DataSource dataSource) {
            setDataSource(dataSource);
            setFunction(true);
            setSql(SQL);
            declareParameter(new SqlOutParameter("date", Types.DATE));
            compile();
        }

        public Date execute() {
            // the 'sysdate' sproc has no input parameters, so an empty Map is supplied...
            Map<String, Object> results = execute(new HashMap<String, Object>());
            Date sysdate = (Date) results.get("date");
            return sysdate;
        }
    }

}

Kotlin

import java.sql.Types
import java.util.Date
import java.util.Map
import javax.sql.DataSource
import org.springframework.jdbc.core.SqlOutParameter
import org.springframework.jdbc.object.StoredProcedure

class StoredProcedureDao(dataSource: DataSource) {

    private val SQL = "sysdate"

    private val getSysdate = GetSysdateProcedure(dataSource)

    val sysdate: Date
        get() = getSysdate.execute()

    private inner class GetSysdateProcedure(dataSource: DataSource) : StoredProcedure() {

        init {
            setDataSource(dataSource)
            isFunction = true
            sql = SQL
            declareParameter(SqlOutParameter("date", Types.DATE))
            compile()
        }

        fun execute(): Date {
            // the 'sysdate' sproc has no input parameters, so an empty Map is supplied...
            val results = execute(mutableMapOf<String, Any>())
            return results["date"] as Date
        }
    }
}

下面的StoredProcedure示例具有两个输出参数(在本例中是 Oracle Ref 游标):

Java

import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import oracle.jdbc.OracleTypes;
import org.springframework.jdbc.core.SqlOutParameter;
import org.springframework.jdbc.object.StoredProcedure;

public class TitlesAndGenresStoredProcedure extends StoredProcedure {

    private static final String SPROC_NAME = "AllTitlesAndGenres";

    public TitlesAndGenresStoredProcedure(DataSource dataSource) {
        super(dataSource, SPROC_NAME);
        declareParameter(new SqlOutParameter("titles", OracleTypes.CURSOR, new TitleMapper()));
        declareParameter(new SqlOutParameter("genres", OracleTypes.CURSOR, new GenreMapper()));
        compile();
    }

    public Map<String, Object> execute() {
        // again, this sproc has no input parameters, so an empty Map is supplied
        return super.execute(new HashMap<String, Object>());
    }
}

Kotlin

import java.util.HashMap
import javax.sql.DataSource
import oracle.jdbc.OracleTypes
import org.springframework.jdbc.core.SqlOutParameter
import org.springframework.jdbc.object.StoredProcedure

class TitlesAndGenresStoredProcedure(dataSource: DataSource) : StoredProcedure(dataSource, SPROC_NAME) {

    companion object {
        private const val SPROC_NAME = "AllTitlesAndGenres"
    }

    init {
        declareParameter(SqlOutParameter("titles", OracleTypes.CURSOR, TitleMapper()))
        declareParameter(SqlOutParameter("genres", OracleTypes.CURSOR, GenreMapper()))
        compile()
    }

    fun execute(): Map<String, Any> {
        // again, this sproc has no input parameters, so an empty Map is supplied
        return super.execute(HashMap<String, Any>())
    }
}

请注意,在TitlesAndGenresStoredProcedure构造函数中使用的declareParameter(..)方法的重载变量是如何传递RowMapper实现实例的。这是一种非常方便且功能强大的重用现有功能的方法。接下来的两个示例提供了两个RowMapper实现的代码。

对于所提供的ResultSet中的每一行,TitleMapper类将ResultSet映射到Title域对象,如下所示:

Java

import java.sql.ResultSet;
import java.sql.SQLException;
import com.foo.domain.Title;
import org.springframework.jdbc.core.RowMapper;

public final class TitleMapper implements RowMapper<Title> {

    public Title mapRow(ResultSet rs, int rowNum) throws SQLException {
        Title title = new Title();
        title.setId(rs.getLong("id"));
        title.setName(rs.getString("name"));
        return title;
    }
}

Kotlin

import java.sql.ResultSet
import com.foo.domain.Title
import org.springframework.jdbc.core.RowMapper

class TitleMapper : RowMapper<Title> {

    override fun mapRow(rs: ResultSet, rowNum: Int) =
            Title(rs.getLong("id"), rs.getString("name"))
}

对于所提供的ResultSet中的每一行,GenreMapper类将ResultSet映射到Genre域对象,如下所示:

Java

import java.sql.ResultSet;
import java.sql.SQLException;
import com.foo.domain.Genre;
import org.springframework.jdbc.core.RowMapper;

public final class GenreMapper implements RowMapper<Genre> {

    public Genre mapRow(ResultSet rs, int rowNum) throws SQLException {
        return new Genre(rs.getString("name"));
    }
}

Kotlin

import java.sql.ResultSet
import com.foo.domain.Genre
import org.springframework.jdbc.core.RowMapper

class GenreMapper : RowMapper<Genre> {

    override fun mapRow(rs: ResultSet, rowNum: Int): Genre {
        return Genre(rs.getString("name"))
    }
}

要将参数传递给在 RDBMS 中的定义中具有一个或多个输入参数的存储过程,可以对强类型execute(..)方法进行编码,该方法将委托给超类中的非类型execute(Map)方法,如下例所示:

Java

import java.sql.Types;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import oracle.jdbc.OracleTypes;
import org.springframework.jdbc.core.SqlOutParameter;
import org.springframework.jdbc.core.SqlParameter;
import org.springframework.jdbc.object.StoredProcedure;

public class TitlesAfterDateStoredProcedure extends StoredProcedure {

    private static final String SPROC_NAME = "TitlesAfterDate";
    private static final String CUTOFF_DATE_PARAM = "cutoffDate";

    public TitlesAfterDateStoredProcedure(DataSource dataSource) {
        super(dataSource, SPROC_NAME);
        declareParameter(new SqlParameter(CUTOFF_DATE_PARAM, Types.DATE);
        declareParameter(new SqlOutParameter("titles", OracleTypes.CURSOR, new TitleMapper()));
        compile();
    }

    public Map<String, Object> execute(Date cutoffDate) {
        Map<String, Object> inputs = new HashMap<String, Object>();
        inputs.put(CUTOFF_DATE_PARAM, cutoffDate);
        return super.execute(inputs);
    }
}

Kotlin

import java.sql.Types
import java.util.Date
import javax.sql.DataSource
import oracle.jdbc.OracleTypes
import org.springframework.jdbc.core.SqlOutParameter
import org.springframework.jdbc.core.SqlParameter
import org.springframework.jdbc.object.StoredProcedure

class TitlesAfterDateStoredProcedure(dataSource: DataSource) : StoredProcedure(dataSource, SPROC_NAME) {

    companion object {
        private const val SPROC_NAME = "TitlesAfterDate"
        private const val CUTOFF_DATE_PARAM = "cutoffDate"
    }

    init {
        declareParameter(SqlParameter(CUTOFF_DATE_PARAM, Types.DATE))
        declareParameter(SqlOutParameter("titles", OracleTypes.CURSOR, TitleMapper()))
        compile()
    }

    fun execute(cutoffDate: Date) = super.execute(
            mapOf<String, Any>(CUTOFF_DATE_PARAM to cutoffDate))
}

# 3.8.参数和数据值处理的常见问题

Spring Framework 的 JDBC 支持所提供的不同方法中存在参数和数据值的常见问题。本节介绍如何解决这些问题。

# 3.8.1.为参数提供 SQL 类型信息

通常, Spring 根据传入的参数的类型来确定参数的 SQL 类型。在设置参数值时,可以显式地提供要使用的 SQL 类型。这有时是正确设置NULL值所必需的。

你可以通过以下几种方式提供 SQL 类型信息:

  • JdbcTemplate的许多更新和查询方法以int数组的形式接受一个额外的参数。此数组用于通过使用来自java.sql.Types类的常量值来指示相应参数的 SQL 类型。为每个参数提供一个条目。

  • 你可以使用SqlParameterValue类来包装需要此附加信息的参数值。为此,为每个值创建一个新实例,并在构造函数中传入 SQL 类型和参数值。你还可以为数值提供一个可选的缩放参数。

  • 对于使用命名参数的方法,可以使用SqlParameterSource类,BeanPropertySqlParameterSourceMapSqlParameterSource。它们都有为任何命名参数值注册 SQL 类型的方法。

# 3.8.2.处理 BLOB 和 CLOB 对象

你可以在数据库中存储图像、其他二进制数据和大量文本。这些大对象被称为 BLOBS(二进制大对象),用于二进制数据,而 CLOBS(字符大对象),用于字符数据。在 Spring 中,可以通过直接使用JdbcTemplate来处理这些大型对象,也可以在使用 RDBMS 对象和SimpleJdbc类提供的更高抽象时处理这些对象。所有这些方法都使用LobHandler接口的实现,用于实际管理 LOB(大对象)数据。LobHandler提供了对LobCreator类的访问,通过getLobCreator方法,即用于创建要插入的新的 LOB 对象。

LobCreatorLobHandler为 LOB 输入和输出提供以下支持:

  • BLOB

    • byte[]:getBlobAsBytessetBlobAsBytes

    • InputStream:getBlobAsBinaryStreamsetBlobAsBinaryStream

  • CLOB

    • String:getClobAsStringsetClobAsString

    • InputStream:getClobAsAsciiStreamsetClobAsAsciiStream

    • Reader:getClobAsCharacterStreamsetClobAsCharacterStream

下一个示例展示了如何创建和插入 BLOB。稍后,我们将展示如何从数据库中读回它。

这个示例使用JdbcTemplateAbstractLobCreatingPreparedStatementCallback的实现。它实现了一种方法setValues。这个方法提供了LobCreator,我们使用它来设置 SQL INSERT 语句中 LOB 列的值。

对于这个示例,我们假设有一个变量lobHandler,它已经被设置为DefaultLobHandler的实例。你通常通过依赖项注入来设置该值。

下面的示例展示了如何创建和插入 BLOB:

Java

final File blobIn = new File("spring2004.jpg");
final InputStream blobIs = new FileInputStream(blobIn);
final File clobIn = new File("large.txt");
final InputStream clobIs = new FileInputStream(clobIn);
final InputStreamReader clobReader = new InputStreamReader(clobIs);

jdbcTemplate.execute(
    "INSERT INTO lob_table (id, a_clob, a_blob) VALUES (?, ?, ?)",
    new AbstractLobCreatingPreparedStatementCallback(lobHandler) {  (1)
        protected void setValues(PreparedStatement ps, LobCreator lobCreator) throws SQLException {
            ps.setLong(1, 1L);
            lobCreator.setClobAsCharacterStream(ps, 2, clobReader, (int)clobIn.length());  (2)
            lobCreator.setBlobAsBinaryStream(ps, 3, blobIs, (int)blobIn.length());  (3)
        }
    }
);

blobIs.close();
clobReader.close();
1 lobHandler中传递(在本例中)是一个普通的DefaultLobHandler
2 使用setClobAsCharacterStream方法传入 CLOB 的内容。
3 使用setBlobAsBinaryStream方法传入 BLOB 的内容。

Kotlin

val blobIn = File("spring2004.jpg")
val blobIs = FileInputStream(blobIn)
val clobIn = File("large.txt")
val clobIs = FileInputStream(clobIn)
val clobReader = InputStreamReader(clobIs)

jdbcTemplate.execute(
        "INSERT INTO lob_table (id, a_clob, a_blob) VALUES (?, ?, ?)",
        object: AbstractLobCreatingPreparedStatementCallback(lobHandler) {  (1)
            override fun setValues(ps: PreparedStatement, lobCreator: LobCreator) {
                ps.setLong(1, 1L)
                lobCreator.setClobAsCharacterStream(ps, 2, clobReader, clobIn.length().toInt())  (2)
                lobCreator.setBlobAsBinaryStream(ps, 3, blobIs, blobIn.length().toInt())  (3)
            }
        }
)
blobIs.close()
clobReader.close()
1 lobHandler中传递(在本例中)是一个普通的DefaultLobHandler
2 使用setClobAsCharacterStream的方法来传入 CLOB 的内容。
3 使用setBlobAsBinaryStream方法传入 BLOB 的内容。
如果在LobCreator返回的DefaultLobHandler.getLobCreator()方法上调用setBlobAsBinaryStreamsetClobAsAsciiStreamsetClobAsCharacterStream方法,则可以选择为contentLength参数指定负值。如果指定的内容长度为负,则DefaultLobHandler使用 set-stream 方法的 JDBC4.0 变体,而不使用
长度参数。否则,它会将指定的长度传递给驱动程序。

查看用于验证它是否支持流
LOB 而不提供内容长度的 JDBC 驱动程序的文档。

现在是从数据库中读取 LOB 数据的时候了。同样,使用带有相同实例变量JdbcTemplatelobHandler和对DefaultLobHandler的引用。下面的示例展示了如何做到这一点:

Java

List<Map<String, Object>> l = jdbcTemplate.query("select id, a_clob, a_blob from lob_table",
    new RowMapper<Map<String, Object>>() {
        public Map<String, Object> mapRow(ResultSet rs, int i) throws SQLException {
            Map<String, Object> results = new HashMap<String, Object>();
            String clobText = lobHandler.getClobAsString(rs, "a_clob");  (1)
            results.put("CLOB", clobText);
            byte[] blobBytes = lobHandler.getBlobAsBytes(rs, "a_blob");  (2)
            results.put("BLOB", blobBytes);
            return results;
        }
    });
1 使用方法getClobAsString检索 CLOB 的内容。
2 使用方法getBlobAsBytes检索 BLOB 的内容。

Kotlin

val l = jdbcTemplate.query("select id, a_clob, a_blob from lob_table") { rs, _ ->
    val clobText = lobHandler.getClobAsString(rs, "a_clob")  (1)
    val blobBytes = lobHandler.getBlobAsBytes(rs, "a_blob")  (2)
    mapOf("CLOB" to clobText, "BLOB" to blobBytes)
}
1 使用方法getClobAsString检索 CLOB 的内容。
2 使用方法getBlobAsBytes检索 BLOB 的内容。

# 3.8.3.传入 IN 子句的值列表

SQL 标准允许基于包含变量值列表的表达式选择行。一个典型的例子是select * from T_ACTOR where id in (1, 2, 3)。JDBC 标准对准备好的语句不直接支持此变量列表。不能声明数量可变的占位符。你需要使用所需数量的占位符来进行一些更改,或者,一旦知道需要多少个占位符,就需要动态地生成 SQL 字符串。在NamedParameterJdbcTemplateJdbcTemplate中提供的命名参数支持采用后一种方法。可以将这些值作为基元对象的java.util.List传入。这个列表用于插入所需的占位符,并在语句执行期间传入这些值。

在传递许多值时要小心。JDBC 标准并不保证
表达式列表可以使用超过 100 个值。各种数据库都超过了
的值,但是它们通常对允许的值有一个严格的限制。例如,甲骨文的
极限是 1000。

除了 value 列表中的基本值之外,还可以创建java.util.List的对象数组。这个列表可以支持为in子句定义的多个表达式,例如select * from T_ACTOR where (id, last_name) in ((1, 'Johnson'), (2, 'Harrop'))。当然,这需要你的数据库支持这种语法。

# 3.8.4.处理存储过程调用的复杂类型

当调用存储过程时,有时可以使用特定于数据库的复杂类型。为了适应这些类型, Spring 提供了一个SqlReturnType,用于在从存储过程调用返回它们时处理它们,以及在将它们作为参数传入存储过程时处理SqlTypeValue

SqlReturnType接口有一个必须实现的方法(名为getTypeValue)。该接口用作SqlOutParameter声明的一部分。下面的示例显示返回用户声明类型ITEM_TYPE的 OracleSTRUCT对象的值:

Java

public class TestItemStoredProcedure extends StoredProcedure {

    public TestItemStoredProcedure(DataSource dataSource) {
        // ...
        declareParameter(new SqlOutParameter("item", OracleTypes.STRUCT, "ITEM_TYPE",
            (CallableStatement cs, int colIndx, int sqlType, String typeName) -> {
                STRUCT struct = (STRUCT) cs.getObject(colIndx);
                Object[] attr = struct.getAttributes();
                TestItem item = new TestItem();
                item.setId(((Number) attr[0]).longValue());
                item.setDescription((String) attr[1]);
                item.setExpirationDate((java.util.Date) attr[2]);
                return item;
            }));
        // ...
    }

Kotlin

class TestItemStoredProcedure(dataSource: DataSource) : StoredProcedure() {

    init {
        // ...
        declareParameter(SqlOutParameter("item", OracleTypes.STRUCT, "ITEM_TYPE") { cs, colIndx, sqlType, typeName ->
            val struct = cs.getObject(colIndx) as STRUCT
            val attr = struct.getAttributes()
            TestItem((attr[0] as Long, attr[1] as String, attr[2] as Date)
        })
        // ...
    }
}

可以使用SqlTypeValue将 Java 对象的值(例如TestItem)传递给存储过程。SqlTypeValue接口有一个必须实现的方法(名为createTypeValue)。活动连接被传入,你可以使用它创建特定于数据库的对象,例如StructDescriptor实例或ArrayDescriptor实例。下面的示例创建了StructDescriptor实例:

Java

final TestItem testItem = new TestItem(123L, "A test item",
        new SimpleDateFormat("yyyy-M-d").parse("2010-12-31"));

SqlTypeValue value = new AbstractSqlTypeValue() {
    protected Object createTypeValue(Connection conn, int sqlType, String typeName) throws SQLException {
        StructDescriptor itemDescriptor = new StructDescriptor(typeName, conn);
        Struct item = new STRUCT(itemDescriptor, conn,
        new Object[] {
            testItem.getId(),
            testItem.getDescription(),
            new java.sql.Date(testItem.getExpirationDate().getTime())
        });
        return item;
    }
};

Kotlin

val (id, description, expirationDate) = TestItem(123L, "A test item",
        SimpleDateFormat("yyyy-M-d").parse("2010-12-31"))

val value = object : AbstractSqlTypeValue() {
    override fun createTypeValue(conn: Connection, sqlType: Int, typeName: String?): Any {
        val itemDescriptor = StructDescriptor(typeName, conn)
        return STRUCT(itemDescriptor, conn,
                arrayOf(id, description, java.sql.Date(expirationDate.time)))
    }
}

现在可以将此SqlTypeValue添加到Map中,该参数包含存储过程的execute调用的输入参数。

SqlTypeValue的另一种用法是将一组值传递给 Oracle 存储过程。Oracle 有自己的内部ARRAY类,在这种情况下必须使用它,你可以使用SqlTypeValue来创建 OracleARRAY的实例,并用来自 JavaARRAY的值填充它,如下例所示:

Java

final Long[] ids = new Long[] {1L, 2L};

SqlTypeValue value = new AbstractSqlTypeValue() {
    protected Object createTypeValue(Connection conn, int sqlType, String typeName) throws SQLException {
        ArrayDescriptor arrayDescriptor = new ArrayDescriptor(typeName, conn);
        ARRAY idArray = new ARRAY(arrayDescriptor, conn, ids);
        return idArray;
    }
};

Kotlin

class TestItemStoredProcedure(dataSource: DataSource) : StoredProcedure() {

    init {
        val ids = arrayOf(1L, 2L)
        val value = object : AbstractSqlTypeValue() {
            override fun createTypeValue(conn: Connection, sqlType: Int, typeName: String?): Any {
                val arrayDescriptor = ArrayDescriptor(typeName, conn)
                return ARRAY(arrayDescriptor, conn, ids)
            }
        }
    }
}

# 3.9.嵌入式数据库支持

org.springframework.jdbc.datasource.embedded包提供了对嵌入式 Java 数据库引擎的支持。本地提供了对HSQL (opens new window)H2 (opens new window)Derby (opens new window)的支持。你还可以使用可扩展的 API 来插入新的嵌入式数据库类型和DataSource实现。

# 3.9.1.为什么要使用嵌入式数据库?

由于嵌入式数据库的轻量级特性,它在项目的开发阶段非常有用。好处包括易于配置、快速启动时间、可测试性以及在开发过程中快速改进 SQL 的能力。

# 3.9.2.使用 Spring XML 创建嵌入式数据库

如果希望在 Spring ApplicationContext中以 Bean 的形式公开嵌入式数据库实例,则可以在embedded-database名称空间中使用embedded-database标记:

<jdbc:embedded-database id="dataSource" generate-name="true">
    <jdbc:script location="classpath:schema.sql"/>
    <jdbc:script location="classpath:test-data.sql"/>
</jdbc:embedded-database>

前面的配置创建了一个嵌入式 HSQL 数据库,该数据库由来自 Classpath 根中schema.sqltest-data.sql资源的 SQL 填充。此外,作为一种最佳实践,嵌入式数据库被分配一个唯一生成的名称。将嵌入式数据库作为类型javax.sql.DataSource的 Bean 容器提供给 Spring 容器,然后可以根据需要将其注入到数据访问对象中。

# 3.9.3.以编程方式创建嵌入式数据库

EmbeddedDatabaseBuilder类为以编程方式构建嵌入式数据库提供了一个 Fluent API。当你需要在独立环境或独立集成测试中创建嵌入式数据库时,可以使用此方法,如下例所示:

Java

EmbeddedDatabase db = new EmbeddedDatabaseBuilder()
        .generateUniqueName(true)
        .setType(H2)
        .setScriptEncoding("UTF-8")
        .ignoreFailedDrops(true)
        .addScript("schema.sql")
        .addScripts("user_data.sql", "country_data.sql")
        .build();

// perform actions against the db (EmbeddedDatabase extends javax.sql.DataSource)

db.shutdown()

Kotlin

val db = EmbeddedDatabaseBuilder()
        .generateUniqueName(true)
        .setType(H2)
        .setScriptEncoding("UTF-8")
        .ignoreFailedDrops(true)
        .addScript("schema.sql")
        .addScripts("user_data.sql", "country_data.sql")
        .build()

// perform actions against the db (EmbeddedDatabase extends javax.sql.DataSource)

db.shutdown()

有关所有支持的选项的更多详细信息,请参见[Javadoc forEmbeddedDatabaseBuilder](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/jdbc/datasource/embedded/embeddatabasebuilder.html)。

你还可以使用EmbeddedDatabaseBuilder通过使用 Java 配置来创建嵌入式数据库,如下例所示:

Java

@Configuration
public class DataSourceConfig {

    @Bean
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
                .generateUniqueName(true)
                .setType(H2)
                .setScriptEncoding("UTF-8")
                .ignoreFailedDrops(true)
                .addScript("schema.sql")
                .addScripts("user_data.sql", "country_data.sql")
                .build();
    }
}

Kotlin

@Configuration
class DataSourceConfig {

    @Bean
    fun dataSource(): DataSource {
        return EmbeddedDatabaseBuilder()
                .generateUniqueName(true)
                .setType(H2)
                .setScriptEncoding("UTF-8")
                .ignoreFailedDrops(true)
                .addScript("schema.sql")
                .addScripts("user_data.sql", "country_data.sql")
                .build()
    }
}

# 3.9.4.选择嵌入式数据库类型

本节介绍如何在 Spring 支持的三个嵌入式数据库中选择一个。它包括以下主题:

# Using HSQL

Spring 支持 HSQL1.8.0 及以上版本。如果没有显式指定类型,则 HSQL 是默认的嵌入式数据库。要显式指定 HSQL,请将embedded-database标记的type属性设置为HSQL。如果使用 Builder API,则使用EmbeddedDatabaseType.HSQL调用setType(EmbeddedDatabaseType)方法。

# Using H2

Spring 支持 H2 数据库。要启用 H2,请将embedded-database标记的type属性设置为H2。如果使用 Builder API,则使用EmbeddedDatabaseType.H2调用setType(EmbeddedDatabaseType)方法。

# 使用 Derby

Spring 支持 Apache Derby10.5 及以上版本。要启用 Derby,请将embedded-database标记的type属性设置为DERBY。如果使用 Builder API,则使用EmbeddedDatabaseType.DERBY调用setType(EmbeddedDatabaseType)方法。

# 3.9.5.用嵌入式数据库测试数据访问逻辑

嵌入式数据库提供了一种轻量级的方式来测试数据访问代码。下一个示例是一个使用嵌入式数据库的数据访问集成测试模板。当嵌入式数据库不需要跨测试类重用时,使用这样的模板对于一次性操作很有用。但是,如果你希望创建一个在测试套件中共享的嵌入式数据库,可以考虑使用Spring TestContext Framework并在<91]中将嵌入式数据库配置为 Bean,如Creating an Embedded Database by Using Spring XML以编程方式创建嵌入式数据库中所述。下面的清单显示了测试模板:

Java

public class DataAccessIntegrationTestTemplate {

    private EmbeddedDatabase db;

    @BeforeEach
    public void setUp() {
        // creates an HSQL in-memory database populated from default scripts
        // classpath:schema.sql and classpath:data.sql
        db = new EmbeddedDatabaseBuilder()
                .generateUniqueName(true)
                .addDefaultScripts()
                .build();
    }

    @Test
    public void testDataAccess() {
        JdbcTemplate template = new JdbcTemplate(db);
        template.query( /* ... */ );
    }

    @AfterEach
    public void tearDown() {
        db.shutdown();
    }

}

Kotlin

class DataAccessIntegrationTestTemplate {

    private lateinit var db: EmbeddedDatabase

    @BeforeEach
    fun setUp() {
        // creates an HSQL in-memory database populated from default scripts
        // classpath:schema.sql and classpath:data.sql
        db = EmbeddedDatabaseBuilder()
                .generateUniqueName(true)
                .addDefaultScripts()
                .build()
    }

    @Test
    fun testDataAccess() {
        val template = JdbcTemplate(db)
        template.query( /* ... */)
    }

    @AfterEach
    fun tearDown() {
        db.shutdown()
    }
}

# 3.9.6.为嵌入式数据库生成唯一的名称

如果开发团队的测试套件无意中试图重新创建同一数据库的其他实例,那么在使用嵌入式数据库时,开发团队经常会遇到错误。如果 XML 配置文件或@Configuration类负责创建嵌入式数据库,并且相应的配置随后在相同的测试套件(即在相同的 JVM 流程中)的多个测试场景中重用,那么这种情况就很容易发生,例如,针对嵌入式数据库的集成测试,其ApplicationContext配置仅在 Bean 定义配置文件是活动的方面存在差异。

此类错误的根本原因是 Spring 的EmbeddedDatabaseFactory(用于 Java 配置的<jdbc:embedded-database>XML 名称空间元素和EmbeddedDatabaseBuilder内部都使用)将嵌入式数据库的名称设置为testdb(如果没有另外指定的话)。对于<jdbc:embedded-database>的情况,嵌入式数据库通常被分配一个等于 Bean 的id的名称(通常,类似于dataSource)。因此,随后创建嵌入式数据库的尝试不会产生新的数据库。相反,相同的 JDBC 连接 URL 被重用,并且试图创建新的嵌入式数据库实际上指向从相同配置创建的现有嵌入式数据库。

Spring 为了解决这个常见的问题,Framework4.2 提供了为嵌入式数据库生成唯一名称的支持。要启用生成的名称,请使用以下选项之一。

  • EmbeddedDatabaseFactory.setGenerateUniqueDatabaseName()

  • EmbeddedDatabaseBuilder.generateUniqueName()

  • <jdbc:embedded-database generate-name="true" …​ >

# 3.9.7.扩展嵌入式数据库支持

你可以通过两种方式扩展 Spring JDBC 嵌入式数据库支持:

  • 实现EmbeddedDatabaseConfigurer以支持新的嵌入式数据库类型。

  • 实现DataSourceFactory以支持新的DataSource实现,例如连接池来管理嵌入式数据库连接。

我们鼓励你在GitHub 问题 (opens new window)上为 Spring 社区提供扩展。

# 3.10.初始化DataSource

org.springframework.jdbc.datasource.init包支持初始化现有的DataSource。嵌入式数据库支持提供了一种用于为应用程序创建和初始化DataSource的选项。然而,有时你可能需要初始化在某个服务器上运行的实例。

# 3.10.1.使用 Spring XML 初始化数据库

如果要初始化数据库,并且可以提供对DataSource Bean 的引用,则可以在initialize-database名称空间中使用initialize-database标记:

<jdbc:initialize-database data-source="dataSource">
    <jdbc:script location="classpath:com/foo/sql/db-schema.sql"/>
    <jdbc:script location="classpath:com/foo/sql/db-test-data.sql"/>
</jdbc:initialize-database>

前面的示例针对数据库运行两个指定的脚本。第一个脚本创建一个模式,第二个脚本使用测试数据集填充表。脚本位置也可以是带有通配符的模式,该通配符具有用于 Spring 中的资源的通常的 Ant 样式(例如,classpath*:/com/foo/**/sql/*-data.sql)。如果使用模式,那么脚本将按照其 URL 或文件名的词法顺序运行。

数据库初始化器的默认行为是无条件地运行所提供的脚本。这可能并不总是你想要的——例如,如果你针对已经包含测试数据的数据库运行脚本。通过遵循先创建表然后插入数据的常见模式(如前面所示),可以减少意外删除数据的可能性。如果表已经存在,则第一步失败。

然而,为了获得对现有数据的创建和删除的更多控制,XML 名称空间提供了一些附加选项。第一个是用来打开和关闭初始化的标志。你可以根据环境来设置这个值(例如,从系统属性或环境中提取布尔值 Bean)。下面的示例从系统属性获得一个值:

<jdbc:initialize-database data-source="dataSource"
    enabled="#{systemProperties.INITIALIZE_DATABASE}"> (1)
    <jdbc:script location="..."/>
</jdbc:initialize-database>
1 从一个名为INITIALIZE_DATABASE的系统属性获取enabled的值。

控制现有数据的第二个选择是更宽容地对待失败。为此,你可以控制初始化器的能力,以忽略它从脚本运行的 SQL 中的某些错误,如下例所示:

<jdbc:initialize-database data-source="dataSource" ignore-failures="DROPS">
    <jdbc:script location="..."/>
</jdbc:initialize-database>

在前面的示例中,我们认为,有时脚本是针对一个空数据库运行的,因此脚本中的一些DROP语句将会失败。因此失败的 SQLDROP语句将被忽略,但其他失败将导致异常。如果你的 SQL 方言不支持DROP …​ IF EXISTS(或类似),但你希望在重新创建测试数据之前无条件地删除所有测试数据,那么这是非常有用的。在这种情况下,第一个脚本通常是一组DROP语句,然后是一组CREATE语句。

ignore-failures选项可以设置为NONE(默认),DROPS(忽略失败的下降),或ALL(忽略所有失败)。

如果脚本中根本不存在;字符,则每条语句都应该用;或一条新的行隔开。你可以全局控制它,也可以通过脚本控制它,如下例所示:

<jdbc:initialize-database data-source="dataSource" separator="@@"> (1)
    <jdbc:script location="classpath:com/myapp/sql/db-schema.sql" separator=";"/> (2)
    <jdbc:script location="classpath:com/myapp/sql/db-test-data-1.sql"/>
    <jdbc:script location="classpath:com/myapp/sql/db-test-data-2.sql"/>
</jdbc:initialize-database>
1 将分隔符脚本设置为@@
2 db-schema.sql的分隔符设置为;

在这个示例中,两个test-data脚本使用@@作为语句分隔符,只有db-schema.sql使用;。此配置指定了@@的默认分隔符,并覆盖了db-schema脚本的默认分隔符。

如果你需要的控制比从 XML 命名空间获得的更多,那么你可以直接使用DataSourceInitializer并将其定义为应用程序中的组件。

# 依赖于数据库的其他组件的初始化

一大类应用程序(那些在 Spring 上下文启动后才使用数据库的应用程序)可以使用数据库初始化器,而不会有更多的麻烦。如果你的应用程序不是其中之一,那么你可能需要阅读本节的其余部分。

数据库初始化器依赖于DataSource实例,并运行其初始化回调中提供的脚本(类似于 XML Bean 定义中的init-method、组件中的@PostConstruct方法,或者实现InitializingBean的组件中的afterPropertiesSet()方法)。如果其他 bean 依赖于相同的数据源,并在初始化回调中使用数据源,则可能存在问题,因为数据尚未初始化。这方面的一个常见示例是一个缓存,它在应用程序启动时急切地初始化并从数据库加载数据。

要解决这个问题,你有两种选择:将缓存初始化策略更改到稍后的阶段,或者确保首先初始化数据库初始化器。

如果应用程序在你的控制范围内,而不是在其他情况下,更改缓存初始化策略可能很容易。关于如何实现这一点的一些建议包括:

  • 使缓存在第一次使用时就进行惰性初始化,从而提高了应用程序的启动时间。

  • 让你的缓存或单独的组件初始化缓存实现LifecycleSmartLifecycle。当应用程序上下文启动时,可以通过设置其autoStartup标志自动启动SmartLifecycle,也可以通过在封闭的上下文上调用Lifecycle手动启动Lifecycle

  • 使用 Spring ApplicationEvent或类似的自定义观察者机制来触发缓存初始化。ContextRefreshedEvent总是在上下文准备好使用时发布(在所有 bean 都已初始化之后),因此这通常是一个有用的钩子(这是SmartLifecycle默认情况下的工作方式)。

确保首先初始化数据库初始化器也很容易。关于如何实现这一点的一些建议包括:

  • 依赖于 Spring BeanFactory的默认行为,即以注册顺序初始化 bean。你可以通过在 XML 配置中采用一组<import/>元素的常见做法来轻松地进行安排,这些元素对应用程序模块进行排序,并确保首先列出数据库和数据库初始化。

  • 分离DataSource和使用它的业务组件,并通过将它们放在单独的ApplicationContext实例中来控制它们的启动顺序(例如,父上下文包含DataSource,子上下文包含业务组件)。这种结构在 Spring Web 应用程序中很常见,但可以更普遍地应用。

# 4. 使用 R2DBC 进行数据访问

R2DBC (opens new window)(“反应式关系数据库连接”)是一种社区驱动的规范工作,用于使用反应式模式标准化对 SQL 数据库的访问。

# 4.1.包层次结构

Spring 框架的 R2DBC 抽象框架由两个不同的包组成:

# 4.2.使用 R2DBC 核心类来控制基本的 R2DBC 处理和错误处理

本节介绍如何使用 R2DBC 核心类来控制基本的 R2DBC 处理,包括错误处理。它包括以下主题:

  • [使用DatabaseClient](#r2dbc-databaseclient)

  • 执行语句

  • [查询(SELECT)](#r2dbc-databaseclient-examples-query)

  • [用DatabaseClient更新(INSERTUPDATE,和DELETE)](#r2dbc-databaseclient-examples-update)

  • 语句过滤器

  • 检索自动生成的密钥

# 4.2.1.使用DatabaseClient

DatabaseClient是 R2DBC 核心包中的中心类。它处理资源的创建和释放,这有助于避免常见的错误,例如忘记关闭连接。它执行核心 R2DBC 工作流的基本任务(例如语句创建和执行),将应用程序代码留给提供 SQL 和提取结果。DatabaseClient类:

  • 运行 SQL 查询

  • 更新语句和存储过程调用

  • Result实例上执行迭代

  • 捕获 R2DBC 异常,并将其转换为在org.springframework.dao包中定义的通用的、信息量更大的异常层次结构。(见一致的异常层次结构

客户机有一个功能强大、流畅的 API,它使用了用于声明性组合的反应性类型。

当你使用DatabaseClient作为你的代码时,你只需要实现java.util.function接口,为它们提供一个明确定义的契约。给定一个由DatabaseClient类提供的Connection,一个Function回调将创建一个Publisher。对于提取Row结果的映射函数也是如此。

你可以通过使用ConnectionFactory引用的直接实例化在 DAO 实现中使用DatabaseClient,或者可以在 Spring IoC 容器中配置它,并将其作为 Bean 引用提供给 DAO。

创建DatabaseClient对象的最简单方法是通过静态工厂方法,如下所示:

Java

DatabaseClient client = DatabaseClient.create(connectionFactory);

Kotlin

val client = DatabaseClient.create(connectionFactory)
在 Spring IOC
容器中,应该始终将ConnectionFactory配置为 Bean。

前面的方法使用默认设置创建DatabaseClient

你还可以从DatabaseClient.builder()获得Builder实例。你可以通过调用以下方法来定制客户机:

  • ….bindMarkers(…):提供一个特定的BindMarkersFactory以将命名参数配置为数据库绑定标记转换。

  • ….executeFunction(…):设置ExecuteFunction如何运行Statement对象。

  • ….namedParameters(false):禁用命名参数展开。默认情况下启用。

方言是由[BindMarkersFactoryResolver](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/r2dbc/core/binding/bindmarkersfactoryresolver.html)从ConnectionFactory解析的,通常是通过检查ConnectionFactoryMetadata
你可以通过注册一个
实现org.springframework.r2dbc.core.binding.BindMarkersFactoryResolver$BindMarkerFactoryProvider的类来让 Spring 自动发现你的BindMarkersFactoryBindMarkersFactoryResolver
使用 Spring 的SpringFactoriesLoader的类路径中发现 bind 标记提供者实现。

目前支持的数据库有:

  • H2

  • 马里亚布

  • Microsoft SQL Server

  • MySQL

  • Postgres

这个类发出的所有 SQL 都记录在DEBUG级别下,该类别对应于客户端实例的完全限定类名称(通常DefaultDatabaseClient)。此外,每个执行都在响应序列中注册一个检查点,以帮助调试。

下面的部分提供了DatabaseClient用法的一些示例。这些示例并不是DatabaseClient公开的所有功能的详尽列表。关于这一点,请参见乘务员javadoc (opens new window)

# 执行语句

DatabaseClient提供了运行语句的基本功能。下面的示例展示了创建新表的最小但功能齐全的代码需要包括哪些内容:

Java

Mono<Void> completion = client.sql("CREATE TABLE person (id VARCHAR(255) PRIMARY KEY, name VARCHAR(255), age INTEGER);")
        .then();

Kotlin

client.sql("CREATE TABLE person (id VARCHAR(255) PRIMARY KEY, name VARCHAR(255), age INTEGER);")
        .await()

DatabaseClient是为方便、流畅的使用而设计的。它在执行规范的每个阶段公开中间方法、延续方法和终端方法。上面的示例使用then()返回一个补全Publisher,该补全在查询(或查询,如果 SQL 查询包含多个语句)完成后立即完成。

execute(…)接受 SQL 查询字符串或查询Supplier<String>,以将实际的查询创建推迟到执行时。
# 查询(SELECT

SQL 查询可以通过Row对象或受影响行数返回值。DatabaseClient可以返回更新行数或行本身,具体取决于发出的查询。

下面的查询从表中获取idname列:

Java

Mono<Map<String, Object>> first = client.sql("SELECT id, name FROM person")
        .fetch().first();

Kotlin

val first = client.sql("SELECT id, name FROM person")
        .fetch().awaitSingle()

以下查询使用了一个绑定变量:

爪哇

Mono<Map<String, Object>> first = client.sql("SELECT id, name FROM person WHERE first_name = :fn")
        .bind("fn", "Joe")
        .fetch().first();

Kotlin

val first = client.sql("SELECT id, name FROM person WHERE WHERE first_name = :fn")
        .bind("fn", "Joe")
        .fetch().awaitSingle()

你可能已经注意到在上面的示例中使用了fetch()fetch()是一个延续运算符,它允许你指定要消耗多少数据。

调用first()返回结果中的第一行,并丢弃剩余的行。你可以使用以下操作符来使用数据:

  • first()返回整个结果的第一行。它的 Kotlin 协程变量名为awaitSingle(),用于表示不可空的返回值,如果该值是可选的,则名为awaitSingleOrNull()

  • one()只返回一个结果,如果结果包含更多行,则该结果将失败。使用 Kotlin 协程,awaitOne()正好代表一个值,或者awaitOneOrNull()如果该值可能是null

  • all()返回结果的所有行。当使用 Kotlin 协程时,使用flow()

  • rowsUpdated()返回受影响的行数(INSERT/UPDATE/DELETE计数)。其 Kotlin 协程变种被命名为awaitRowsUpdated()

在不指定更多映射细节的情况下,查询以Map的形式返回表式结果,其键是不区分大小写的列名,这些列名映射到它们的列值。

你可以通过提供一个Function<Row, T>来控制结果映射,每个Row都会被调用,这样它就可以返回任意值(奇异值、集合和映射以及对象)。

下面的示例提取name列并发出其值:

爪哇

Flux<String> names = client.sql("SELECT name FROM person")
        .map(row -> row.get("name", String.class))
        .all();

Kotlin

val names = client.sql("SELECT name FROM person")
        .map{ row: Row -> row.get("name", String.class) }
        .flow()

那么null呢?

关系数据库结果可以包含null值。反应流规范禁止null值的发射。该需求要求在提取器函数中进行适当的null处理。虽然可以从Row中获得null值,但不能发出null值。你必须对对象中的任何null值进行包装(例如,对于奇异值,Optional),以确保提取函数永远不会直接返回null值。

# 更新(INSERTUPDATE,和DELETE)与DatabaseClient####

修改语句的唯一区别是,这些语句通常不返回表格数据,因此你可以使用rowsUpdated()来使用结果。

下面的示例显示了返回更新行数的UPDATE语句:

爪哇

Mono<Integer> affectedRows = client.sql("UPDATE person SET first_name = :fn")
        .bind("fn", "Joe")
        .fetch().rowsUpdated();

Kotlin

val affectedRows = client.sql("UPDATE person SET first_name = :fn")
        .bind("fn", "Joe")
        .fetch().awaitRowsUpdated()
# 将值绑定到查询

典型的应用程序需要参数化的 SQL 语句来根据某些输入选择或更新行。这些语句通常是SELECT语句,受WHERE子句或INSERTUPDATE语句的约束,这些语句接受输入参数。如果参数没有正确转义,参数化语句将承担 SQL 注入的风险。DatabaseClient利用 R2DBC 的bindAPI 来消除查询参数的 SQL 注入风险。你可以使用execute(…)操作符提供参数化 SQL 语句,并将参数绑定到实际的Statement。然后,你的 R2DBC 驱动程序通过使用准备好的语句和参数替换来运行该语句。

参数绑定支持两种绑定策略:

  • 通过索引,使用零基参数索引。

  • 按名称,使用占位符名称。

下面的示例展示了查询的参数绑定:

db.sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)")
    .bind("id", "joe")
    .bind("name", "Joe")
    .bind("age", 34);

R2DBC 原生结合标记

R2DBC 使用依赖于实际数据库供应商的数据库本地绑定标记。例如,Postgres 使用索引标记,如$1$2$n。另一个例子是 SQL Server,它使用带有@前缀的命名绑定标记。

这与 JDBC 不同,后者需要?作为绑定标记。在 JDBC 中,实际的驱动程序将?绑定标记转换为数据库本地标记,作为其语句执行的一部分。

Spring Framework 的 R2DBC 支持允许你使用本机绑定标记或带有:name语法的命名绑定标记。

命名参数支持利用BindMarkersFactory实例在执行查询时将命名参数扩展为本机绑定标记,这使你在不同的数据库供应商之间具有一定程度的查询可移植性。

查询预处理程序将名为Collection的参数展开到一系列绑定标记中,以消除基于参数数量的动态查询创建的需要。嵌套对象数组被扩展以允许使用(例如)选择列表。

考虑以下查询:

SELECT id, name, state FROM table WHERE (name, age) IN (('John', 35), ('Ann', 50))

前面的查询可以参数化并按以下方式运行:

爪哇

List<Object[]> tuples = new ArrayList<>();
tuples.add(new Object[] {"John", 35});
tuples.add(new Object[] {"Ann",  50});

client.sql("SELECT id, name, state FROM table WHERE (name, age) IN (:tuples)")
    .bind("tuples", tuples);

Kotlin

val tuples: MutableList<Array<Any>> = ArrayList()
tuples.add(arrayOf("John", 35))
tuples.add(arrayOf("Ann", 50))

client.sql("SELECT id, name, state FROM table WHERE (name, age) IN (:tuples)")
    .bind("tuples", tuples)
选择列表的使用依赖于供应商。

下面的示例显示了一个使用IN谓词的更简单的变体:

爪哇

client.sql("SELECT id, name, state FROM table WHERE age IN (:ages)")
    .bind("ages", Arrays.asList(35, 50));

Kotlin

val tuples: MutableList<Array<Any>> = ArrayList()
tuples.add(arrayOf("John", 35))
tuples.add(arrayOf("Ann", 50))

client.sql("SELECT id, name, state FROM table WHERE age IN (:ages)")
    .bind("tuples", arrayOf(35, 50))
R2DBC 本身不支持类集合的值。尽管如此,
在上面的示例中展开给定的List对于 Spring 的 R2DBC 支持中的命名参数
有效,例如,如上面所示,用于IN子句。
但是,插入或更新数组类型的列(例如在 postgres 中)
需要底层 R2DBC 驱动程序支持的数组类型:
通常是一个 爪哇 数组,例如。String[]以更新text[]列。
不要将Collection<String>等作为数组参数传递。
# 语句过滤器

有时,你需要在运行前对实际Statement上的选项进行微调。通过DatabaseClient注册一个Statement过滤器(StatementFilterFunction)来截取和修改语句在其执行中的位置,如下例所示:

爪哇

client.sql("INSERT INTO table (name, state) VALUES(:name, :state)")
    .filter((s, next) -> next.execute(s.returnGeneratedValues("id")))
    .bind("name", …)
    .bind("state", …);

Kotlin

client.sql("INSERT INTO table (name, state) VALUES(:name, :state)")
            .filter { s: Statement, next: ExecuteFunction -> next.execute(s.returnGeneratedValues("id")) }
            .bind("name", …)
            .bind("state", …)

DatabaseClient公开还简化了filter(…)接受Function<Statement, Statement>的过载:

爪哇

client.sql("INSERT INTO table (name, state) VALUES(:name, :state)")
    .filter(statement -> s.returnGeneratedValues("id"));

client.sql("SELECT id, name, state FROM table")
    .filter(statement -> s.fetchSize(25));

Kotlin

client.sql("INSERT INTO table (name, state) VALUES(:name, :state)")
    .filter { statement -> s.returnGeneratedValues("id") }

client.sql("SELECT id, name, state FROM table")
    .filter { statement -> s.fetchSize(25) }

StatementFilterFunction实现方式允许对Statement对象进行过滤和对Result对象进行过滤。

# DatabaseClient最佳实践

一旦配置好,DatabaseClient类的实例是线程安全的。这很重要,因为这意味着你可以配置DatabaseClient的单个实例,然后将这个共享引用安全地注入到多个 DAO(或存储库)中。DatabaseClient是有状态的,因为它保持了对ConnectionFactory的引用,但是这个状态不是会话状态。

在使用DatabaseClient类时,一种常见的做法是在 Spring 配置文件中配置ConnectionFactory,然后在依赖项中-将共享的ConnectionFactory注入到 DAO 类中。DatabaseClient是在 setter 中为ConnectionFactory创建的。这导致 DAO 类似于以下内容:

爪哇

public class R2dbcCorporateEventDao implements CorporateEventDao {

    private DatabaseClient databaseClient;

    public void setConnectionFactory(ConnectionFactory connectionFactory) {
        this.databaseClient = DatabaseClient.create(connectionFactory);
    }

    // R2DBC-backed implementations of the methods on the CorporateEventDao follow...
}

Kotlin

class R2dbcCorporateEventDao(connectionFactory: ConnectionFactory) : CorporateEventDao {

    private val databaseClient = DatabaseClient.create(connectionFactory)

    // R2DBC-backed implementations of the methods on the CorporateEventDao follow...
}

显式配置的一种替代方法是使用组件扫描和注释支持进行依赖注入。在这种情况下,你可以用@Component注释类(这使它成为组件扫描的候选),并用ConnectionFactorysetter 方法注解@Autowired。下面的示例展示了如何做到这一点:

爪哇

@Component (1)
public class R2dbcCorporateEventDao implements CorporateEventDao {

    private DatabaseClient databaseClient;

    @Autowired (2)
    public void setConnectionFactory(ConnectionFactory connectionFactory) {
        this.databaseClient = DatabaseClient.create(connectionFactory); (3)
    }

    // R2DBC-backed implementations of the methods on the CorporateEventDao follow...
}
1 @Component注释类。
2 @Autowired注释ConnectionFactorysetter 方法。
3 ConnectionFactory创建一个新的DatabaseClient

Kotlin

@Component (1)
class R2dbcCorporateEventDao(connectionFactory: ConnectionFactory) : CorporateEventDao { (2)

    private val databaseClient = DatabaseClient(connectionFactory) (3)

    // R2DBC-backed implementations of the methods on the CorporateEventDao follow...
}
1 @Component注释类。
2 ConnectionFactory的构造函数注入。
3 ConnectionFactory创建一个新的DatabaseClient

无论你选择使用(或不使用)上述哪种模板初始化样式,每次想要运行 SQL 时,都很少需要创建DatabaseClient类的新实例。一旦配置好,DatabaseClient实例就是线程安全的。如果你的应用程序访问多个数据库,你可能需要多个DatabaseClient实例,这需要多个ConnectionFactory实例,然后需要多个配置不同的DatabaseClient实例。

# 4.3.检索自动生成的密钥

INSERT语句在向定义自动增量或标识列的表中插入行时可能会生成键。要获得对要生成的列名的完全控制,只需注册一个StatementFilterFunction,它为所需的列请求生成的键。

爪哇

Mono<Integer> generatedId = client.sql("INSERT INTO table (name, state) VALUES(:name, :state)")
    .filter(statement -> s.returnGeneratedValues("id"))
        .map(row -> row.get("id", Integer.class))
        .first();

// generatedId emits the generated key once the INSERT statement has finished

Kotlin

val generatedId = client.sql("INSERT INTO table (name, state) VALUES(:name, :state)")
    .filter { statement -> s.returnGeneratedValues("id") }
        .map { row -> row.get("id", Integer.class) }
        .awaitOne()

// generatedId emits the generated key once the INSERT statement has finished

# 4.4.控制数据库连接

本节内容包括:

  • [使用ConnectionFactory](#r2dbc-connectionFactory)

  • [使用ConnectionFactoryUtils](#r2dbc-connectionfactoryutils)

  • [使用SingleConnectionFactory](#R2DBC-SingleConnectionFactory)

  • [使用TransactionAwareConnectionFactoryProxy](#r2dbc-transactionawareConnectionFactoryProxy)

  • [usingR2dbcTransactionManager](#r2dbc-r2dbctransactionmanager)

# 4.4.1.使用ConnectionFactory

Spring 通过ConnectionFactory获得到数据库的 R2DBC 连接。aConnectionFactory是 R2DBC 规范的一部分,是驱动程序的一个常见入口点。它允许容器或框架从应用程序代码中隐藏连接池和事务管理问题。作为开发人员,你不需要了解有关如何连接到数据库的详细信息。这是设置ConnectionFactory的管理员的责任。在开发和测试代码时,你最有可能同时填充这两个角色,但是你并不一定要知道生产数据源是如何配置的。

当你使用 Spring 的 R2DBC 层时,你可以使用第三方提供的连接池实现来配置你自己的连接池。一个流行的实现是 R2DBC 池()。 Spring 发行版中的实现仅用于测试目的,并不提供池。

要配置ConnectionFactory:

  1. 获得与ConnectionFactory的连接,就像你通常获得 R2dbcConnectionFactory一样。

  2. 提供一个 R2DBC URL(请参阅驱动程序的文档以获得正确的值)。

下面的示例展示了如何配置ConnectionFactory:

爪哇

ConnectionFactory factory = ConnectionFactories.get("r2dbc:h2:mem:///test?options=DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE");

Kotlin

val factory = ConnectionFactories.get("r2dbc:h2:mem:///test?options=DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE");

# 4.4.2.使用ConnectionFactoryUtils

ConnectionFactoryUtils类是一个方便且功能强大的助手类,它提供static方法来从ConnectionFactory获得连接和紧密连接(如果需要)。

它支持订阅者Context的绑定连接,例如R2dbcTransactionManager

# 4.4.3.使用SingleConnectionFactory

SingleConnectionFactory类是DelegatingConnectionFactory接口的一种实现,该接口封装单个Connection,该接口在每次使用后都不会关闭。

如果任何客户机代码在假定池连接的情况下调用close(如使用持久性工具时),则应将suppressClose属性设置为true。此设置返回一个封装物理连接的关闭抑制代理。请注意,你不能再将其强制转换为本机Connection或类似的对象。

SingleConnectionFactory主要是一个测试类,如果你的 R2DBC 驱动程序允许这样的使用,它可以用于特定的需求,例如流水线。与池ConnectionFactory相反,它始终重用相同的连接,避免了过多地创建物理连接。

# 4.4.4.使用TransactionAwareConnectionFactoryProxy

TransactionAwareConnectionFactoryProxy是目标ConnectionFactory的代理。代理包装的目标是ConnectionFactory,以添加对 Spring 管理的事务的感知。

如果你使用的 R2DBC 客户机不是集成的,则需要使用这个类,否则将支持 Spring 的 R2DBC。在这种情况下,你仍然可以使用这个客户机,并且在
的同时,让这个客户机参与 Spring 托管事务。通常情况下,
最好是将具有对ConnectionFactoryUtils的正确访问的 R2DBC 客户机集成在一起,以进行资源管理。

参见[TransactionAwareConnectionFactoryProxy](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/r2dbc/connection/transactionawareConnectionFactoryproxy.html)爪哇doc 获取更多细节。

# 4.4.5.使用R2dbcTransactionManager

R2dbcTransactionManager类是用于单个 R2DBC 数据源的ReactiveTransactionManager实现。它将从指定的连接工厂到订阅服务器Context的 R2DBC 连接绑定,可能允许每个连接工厂使用一个订阅服务器连接。

需要应用程序代码来通过ConnectionFactoryUtils.getConnection(ConnectionFactory)检索 R2DBC 连接,而不是 R2DBC 的标准ConnectionFactory.create()

所有框架类(如DatabaseClient)都隐式地使用此策略。如果不与此事务管理器一起使用,则查找策略的行为与常见策略完全相同。因此,它可以在任何情况下使用。

R2dbcTransactionManager类支持应用于连接的自定义隔离级别。

# 5. 对象关系映射数据访问

本节介绍使用对象关系映射时的数据访问。

# 5.1.ORM 简介 Spring

Spring 框架支持与 爪哇 持久性 API( JPA)的集成,并支持用于资源管理、数据访问对象实现和事务策略的本机 Hibernate。例如,对于 Hibernate,有几个方便的 IoC 特性的一流支持,这些特性解决了许多典型的 Hibernate 集成问题。你可以通过依赖项注入为 OR(对象关系)映射工具配置所有受支持的特性。它们可以参与 Spring 的资源和事务管理,并且符合 Spring 的通用事务和 DAO 异常层次结构。推荐的集成风格是针对普通的 Hibernate 或 JPA API 对 DAO 进行编码。

Spring 在创建数据访问应用程序时,为你选择的 ORM 层增加了显著的增强。你可以根据需要利用尽可能多的集成支持,并且应该将这种集成工作与内部构建类似基础设施的成本和风险进行比较。无论采用何种技术,你都可以像使用库一样使用大量的 ORM 支持,因为所有的东西都被设计为一组可重用的 爪哇Bean。 Spring IOC 容器中的 ORM 促进了配置和部署。因此,本节中的大多数示例都显示了 Spring 容器内的配置。

使用 Spring 框架创建 ORM DAO 的好处包括:

  • 更容易的测试。 Spring 的 IOC 方法使得很容易交换 Hibernate SessionFactory实例、JDBCDataSource实例、事务管理器和映射对象实现(如果需要)的实现和配置位置。这进而使得隔离地测试与持久性相关的每一段代码变得容易得多。

  • 常见的数据访问异常。 Spring 可以从你的 ORM 工具中包装异常,将它们从专有的(可能经过检查的)异常转换为公共的运行时DataAccessException层次结构。这个特性允许你仅在适当的层中处理大多数持久性异常(这些异常是不可恢复的),而不需要烦人的样板文件捕获、抛出和异常声明。你仍然可以根据需要捕获和处理异常。请记住,JDBC 异常(包括特定于 DB 的方言)也被转换为相同的层次结构,这意味着你可以在一致的编程模型中使用 JDBC 执行一些操作。

  • 一般资源管理。 Spring 应用程序上下文可以处理 Hibernate SessionFactory实例、 JPA EntityManagerFactory实例、JDBCDataSource实例的位置和配置,以及其他相关资源。这使得这些价值观易于管理和改变。 Spring 提供了对持久性资源的高效、简单和安全的处理。例如,使用 Hibernate 的相关代码通常需要使用相同的 Hibernate Session,以确保效率和适当的事务处理。 Spring 通过 Hibernate SessionFactory公开当前的Session,使得创建Session并将其透明地绑定到当前的线程变得容易。因此, Spring 解决了 Hibernate 典型使用的许多慢性问题,适用于任何本地或 JTA 事务环境。

  • **综合事务管理。**你可以通过@Transactional注释或通过在 XML 配置文件中显式配置事务 AOP 通知,用声明性的、面向方面的编程( AOP)风格的方法拦截器包装你的 ORM 代码。在这两种情况下,事务语义和异常处理(回滚等)都是为你处理的。正如资源和事务管理中所讨论的,你还可以交换各种事务管理器,而不会影响与 ORM 相关的代码。例如,你可以在本地事务和 JTA 之间进行交换,在这两个场景中都可以使用相同的完整服务(例如声明式事务)。此外,与 JDBC 相关的代码可以完全集成到用于执行 ORM 的代码中。这对于不适合 ORM 的数据访问(例如批处理和 BLOB 流)很有用,但仍然需要与 ORM 操作共享公共事务。

有关更全面的 ORM 支持,包括对替代数据库
技术(如 MongoDB)的支持,你可能想查看Spring Data (opens new window)项目套件。如果你是
JPA 用户,Getting Started Accessing
Data with JPA (opens new window)
中的https://spring.io (opens new window)指南提供了一个很好的介绍。

# 5.2.ORM 集成的一般考虑因素

本节重点介绍了适用于所有 ORM 技术的注意事项。Hibernate部分提供了更多细节,并在一个具体的上下文中显示了这些特性和配置。

Spring ORM 集成的主要目标是明确的应用程序分层(使用任何数据访问和事务技术),并实现应用程序对象的松耦合——不再依赖于数据访问或事务策略的业务服务,不再需要硬编码的资源查找,不再需要难以替换的单件件,没有更多的自定义服务注册中心。我们的目标是使用一种简单且一致的方法来连接应用程序对象,使它们尽可能地可重用且不依赖于容器。所有单独的数据访问功能都可以单独使用,但与 Spring 的应用程序上下文概念很好地集成在一起,提供了基于 XML 的配置和对普通 爪哇Bean 实例的交叉引用,这些实例不需要 Spring 感知。 Spring 在典型的应用程序中,许多重要的对象是 爪哇Bean:数据访问模板、数据访问对象、事务管理器、使用数据访问对象的业务服务和事务管理器、Web 视图解析器、使用业务服务的 Web 控制器,等等。

# 5.2.1.资源和事务管理

典型的业务应用程序充斥着重复的资源管理代码。许多项目试图发明自己的解决方案,有时为了编程方便而牺牲了对故障的正确处理。 Spring 提倡用于适当的资源处理的简单解决方案,即在 JDBC 的情况下通过模板化来实现 IOC,并为 ORM 技术应用 AOP 拦截器。

基础架构提供了正确的资源处理,并将特定的 API 异常适当地转换为未经检查的基础架构异常层次结构。 Spring 引入了一种 DAO 异常层次结构,适用于任何数据访问策略。对于直接 JDBC,JdbcTemplate中提到的JdbcTemplate类提供连接处理和SQLExceptionDataAccessException层次结构的正确转换,包括将数据库特定的 SQL 错误代码转换为有意义的异常类。对于 ORM 技术,请参见下一节,了解如何获得相同的异常转换好处。

当涉及到事务管理时,JdbcTemplate类与 Spring 事务支持挂钩,并通过相应的 Spring 事务管理器支持 JTA 和 JDBC 事务。对于所支持的 ORM 技术, Spring 通过 Hibernate 和 JPA 事务管理器以及 JTA 支持提供 Hibernate 和 JPA 支持。有关事务支持的详细信息,请参见事务管理章节。

# 5.2.2.异常转换

在 DAO 中使用 Hibernate 或 JPA 时,必须决定如何处理持久性技术的本机异常类。根据技术的不同,DAO 抛出了HibernateExceptionPersistenceException的子类。这些异常都是运行时异常,不需要声明或捕获。你可能还需要处理IllegalArgumentExceptionIllegalStateException。这意味着调用者只能将异常视为通常是致命的,除非他们希望依赖于持久性技术自身的异常结构。如果不将调用方与实现策略绑定,就不可能捕获特定的原因(例如乐观锁定失败)。这种权衡对于基于强 ORM 或不需要任何特殊异常处理(或两者兼而有之)的应用程序来说可能是可以接受的。然而, Spring 允许通过@Repository注释透明地应用异常翻译。下面的示例(一个用于 爪哇 配置,一个用于 XML 配置)展示了如何这样做:

爪哇

@Repository
public class ProductDaoImpl implements ProductDao {

    // class body here...

}

Kotlin

@Repository
class ProductDaoImpl : ProductDao {

    // class body here...

}
<beans>

    <!-- Exception translation bean post processor -->
    <bean class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor"/>

    <bean id="myProductDao" class="product.ProductDaoImpl"/>

</beans>

后处理器会自动查找所有异常翻译器(PersistenceExceptionTranslator接口的实现),并建议所有标有@Repository注释的 bean,以便发现的翻译器可以拦截并在抛出的异常上应用适当的翻译。

总之,你可以基于纯持久性技术的 API 和注释实现 DAO,同时仍然受益于 Spring 管理的事务、依赖注入和透明的异常转换(如果需要的话)到 Spring 的自定义异常层次结构。

# 5.3. Hibernate

我们从在 Spring 环境中覆盖Hibernate 5 (opens new window)开始,使用它来演示 Spring 用于集成或映射的方法。本节详细介绍了许多问题,并展示了 DAO 实现和事务划分的不同变体。这些模式中的大多数可以直接转换为所有其他受支持的 ORM 工具。本章后面的部分将介绍其他 ORM 技术,并展示简要的示例。

在 Spring Framework5.3 中, Spring 对于 Spring 的HibernateJpaVendorAdapter以及对于本机 Hibernate SessionFactory设置,需要 Hibernate orm5.2+。,对于新启动的应用程序,强烈建议使用 Hibernate orm5.4。,
用于HibernateJpaVendorAdapter, Hibernate 搜索需要升级到 5.1 1.6。

# 5.3.1.SessionFactory Spring 容器中的设置

为了避免将应用程序对象绑定到硬编码的资源查找,可以将资源(例如 JDBC或 Hibernate )定义为 Spring 容器中的 bean。需要访问资源的应用程序对象通过 Bean 引用接收对此类预定义实例的引用,如下一节中的 DAO 定义所示。

以下摘自 XML 应用程序上下文定义的节选显示了如何在其上设置一个 JDBCDataSource和一个 Hibernate SessionFactory:

<beans>

    <bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="org.hsqldb.jdbcDriver"/>
        <property name="url" value="jdbc:hsqldb:hsql://localhost:9001"/>
        <property name="username" value="sa"/>
        <property name="password" value=""/>
    </bean>

    <bean id="mySessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean">
        <property name="dataSource" ref="myDataSource"/>
        <property name="mappingResources">
            <list>
                <value>product.hbm.xml</value>
            </list>
        </property>
        <property name="hibernateProperties">
            <value>
                hibernate.dialect=org.hibernate.dialect.HSQLDialect
            </value>
        </property>
    </bean>

</beans>

从本地的 Jakarta Commons DBCP切换到位于 JNDI 的(通常由应用程序服务器管理)只是一个配置问题,如下例所示:

<beans>
    <jee:jndi-lookup id="myDataSource" jndi-name="java:comp/env/jdbc/myds"/>
</beans>

你还可以访问位于SessionFactory的 JNDI,使用 Spring 的JndiObjectFactoryBean/<jee:jndi-lookup>来检索和公开它。然而,在 EJB 上下文之外,这通常是不常见的。

Spring 还提供了一个LocalSessionFactoryBuilder变体,将
@Bean样式配置和编程设置无缝集成在一起(不涉及FactoryBean)。

LocalSessionFactoryBeanLocalSessionFactoryBuilder支持后台
引导, Hibernate 初始化与给定的引导程序执行器上的应用程序
引导程序线程并行运行(例如SimpleAsyncTaskExecutor)。
LocalSessionFactoryBean上,这可以通过bootstrapExecutor属性获得。在程序化的LocalSessionFactoryBuilder上,有一个重载的buildSessionFactory方法,它接受一个 Bootstrap 执行器参数。

从 Spring Framework5.1 开始,这样的原生 Hibernate 设置还可以公开一个 JPA EntityManagerFactory用于在原生 Hibernate 访问旁边的标准 JPA 交互的方法。
详见Native Hibernate Setup for JPA

# 5.3.2.基于普通 Hibernate API 实现 DAOS

Hibernate 具有称为上下文会话的功能,其中 Hibernate 本身管理一个当前的Session每个事务。这大致相当于 Spring 的每笔交易同步一次 Hibernate Session。基于普通的 Hibernate API,相应的 DAO 实现类似于以下示例:

Java

public class ProductDaoImpl implements ProductDao {

    private SessionFactory sessionFactory;

    public void setSessionFactory(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

    public Collection loadProductsByCategory(String category) {
        return this.sessionFactory.getCurrentSession()
                .createQuery("from test.Product product where product.category=?")
                .setParameter(0, category)
                .list();
    }
}

Kotlin

class ProductDaoImpl(private val sessionFactory: SessionFactory) : ProductDao {

    fun loadProductsByCategory(category: String): Collection<*> {
        return sessionFactory.currentSession
                .createQuery("from test.Product product where product.category=?")
                .setParameter(0, category)
                .list()
    }
}

这种样式类似于 Hibernate 引用文档和示例的样式,只是在实例变量中持有SessionFactory。在 Hibernate 的 CaveaTemptor 示例应用程序中的老派static``HibernateUtil类之上,我们强烈推荐这样一种基于实例的设置。(通常,除非绝对必要,否则不要在static变量中保留任何资源。

前面的 DAO 示例遵循依赖注入模式。它非常适合 Spring IOC 容器,就像它与 Spring 的HibernateTemplate编码一样。你也可以在普通 Java 中设置这样的 DAO(例如,在单元测试中)。要这样做,实例化它,并使用所需的工厂引用调用setSessionFactory(..)。作为 Spring Bean 的定义,DAO 类似于以下内容:

<beans>

    <bean id="myProductDao" class="product.ProductDaoImpl">
        <property name="sessionFactory" ref="mySessionFactory"/>
    </bean>

</beans>

这种 DAO 样式的主要优点是它仅依赖于 Hibernate API。不需要输入任何 Spring 类。从非侵犯性的角度来看,这是有吸引力的,并且对于 Hibernate 开发人员来说可能感觉更自然。

然而,DAO 抛出了普通的HibernateException(它未经检查,因此不必声明或捕获),这意味着调用者只能将异常视为通常是致命的——除非他们希望依赖 Hibernate 自己的异常层次结构。如果不将调用方与实现策略绑定,就不可能捕获特定的原因(例如乐观锁定失败)。这种权衡对于基于强 Hibernate 的应用程序、不需要任何特殊的异常处理,或者两者兼而有之的应用程序来说都是可以接受的。

幸运的是, Spring 的LocalSessionFactoryBean支持 Hibernate 的SessionFactory.getCurrentSession()方法用于任何 Spring 事务策略,返回当前 Spring-管理的事务Session,甚至使用HibernateTransactionManager。该方法的标准行为仍然是返回当前与正在进行的 JTA 事务关联的Session(如果有的话)。无论你是使用 Spring 的JtaTransactionManager、EJB 容器管理事务,还是使用 JTA,都会应用此行为。

总之,你可以基于普通的 Hibernate API 实现 DAO,同时仍然能够参与由 Spring 管理的事务。

# 5.3.3.声明式事务划分

我们建议你使用 Spring 的声明式事务支持,它允许你用 AOP 事务拦截器替换 Java 代码中的显式事务划分 API 调用。你可以使用 Java 注释或 XML 在 Spring 容器中配置此事务拦截器。这种声明性事务功能使你能够使业务服务免于重复的事务划分代码,并专注于添加业务逻辑,这是应用程序的真正价值所在。

在你继续之前,我们强烈建议你阅读声明式事务管理,如果你还没有这样做。

你可以使用@Transactional注释对服务层进行注释,并指示 Spring 容器查找这些注释,并为这些注释的方法提供事务语义。下面的示例展示了如何做到这一点:

Java

public class ProductServiceImpl implements ProductService {

    private ProductDao productDao;

    public void setProductDao(ProductDao productDao) {
        this.productDao = productDao;
    }

    @Transactional
    public void increasePriceOfAllProductsInCategory(final String category) {
        List productsToChange = this.productDao.loadProductsByCategory(category);
        // ...
    }

    @Transactional(readOnly = true)
    public List<Product> findAllProducts() {
        return this.productDao.findAllProducts();
    }
}

Kotlin

class ProductServiceImpl(private val productDao: ProductDao) : ProductService {

    @Transactional
    fun increasePriceOfAllProductsInCategory(category: String) {
        val productsToChange = productDao.loadProductsByCategory(category)
        // ...
    }

    @Transactional(readOnly = true)
    fun findAllProducts() = productDao.findAllProducts()
}

在容器中,你需要设置PlatformTransactionManager实现(作为 Bean)和<tx:annotation-driven/>条目,在运行时选择进入@Transactional处理。下面的示例展示了如何做到这一点:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/tx
        https://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- SessionFactory, DataSource, etc. omitted -->

    <bean id="transactionManager"
            class="org.springframework.orm.hibernate5.HibernateTransactionManager">
        <property name="sessionFactory" ref="sessionFactory"/>
    </bean>

    <tx:annotation-driven/>

    <bean id="myProductService" class="product.SimpleProductService">
        <property name="productDao" ref="myProductDao"/>
    </bean>

</beans>

# 5.3.4.程序化事务划分

你可以在应用程序的较高级别上,在跨越任意数量操作的较低级别的数据访问服务之上,对事务进行划分。对周围业务服务的实现也不存在限制。它只需要一个 Spring PlatformTransactionManager。同样,后者可以来自任何地方,但最好是通过setTransactionManager(..)方法作为 Bean 引用。同样,productDAO应该由setProductDao(..)方法设置。以下一对片段显示了 Spring 应用程序上下文中的事务管理器和业务服务定义,以及业务方法实现的示例:

<beans>

    <bean id="myTxManager" class="org.springframework.orm.hibernate5.HibernateTransactionManager">
        <property name="sessionFactory" ref="mySessionFactory"/>
    </bean>

    <bean id="myProductService" class="product.ProductServiceImpl">
        <property name="transactionManager" ref="myTxManager"/>
        <property name="productDao" ref="myProductDao"/>
    </bean>

</beans>

Java

public class ProductServiceImpl implements ProductService {

    private TransactionTemplate transactionTemplate;
    private ProductDao productDao;

    public void setTransactionManager(PlatformTransactionManager transactionManager) {
        this.transactionTemplate = new TransactionTemplate(transactionManager);
    }

    public void setProductDao(ProductDao productDao) {
        this.productDao = productDao;
    }

    public void increasePriceOfAllProductsInCategory(final String category) {
        this.transactionTemplate.execute(new TransactionCallbackWithoutResult() {
            public void doInTransactionWithoutResult(TransactionStatus status) {
                List productsToChange = this.productDao.loadProductsByCategory(category);
                // do the price increase...
            }
        });
    }
}

Kotlin

class ProductServiceImpl(transactionManager: PlatformTransactionManager,
                        private val productDao: ProductDao) : ProductService {

    private val transactionTemplate = TransactionTemplate(transactionManager)

    fun increasePriceOfAllProductsInCategory(category: String) {
        transactionTemplate.execute {
            val productsToChange = productDao.loadProductsByCategory(category)
            // do the price increase...
        }
    }
}

Spring 的TransactionInterceptor允许使用回调码抛出任何选中的应用程序异常,而TransactionTemplate仅限于回调中未选中的异常。TransactionTemplate在未检查的应用程序异常的情况下,或者如果事务被应用程序标记为只回滚(通过设置TransactionStatus),则触发回滚。默认情况下,TransactionInterceptor的行为方式相同,但每个方法都允许可配置的回滚策略。

# 5.3.5.事务管理策略

TransactionTemplate并且TransactionInterceptor将实际事务处理委托给PlatformTransactionManager实例(对于单个 Hibernate SessionFactory,可以是HibernateTransactionManager(对于单个 Hibernate SessionFactory)),方法是使用ThreadLocal下的遮蔽物)或用于 Hibernate 应用程序的JtaTransactionManager(委托给容器的 JTA 子系统)。你甚至可以使用自定义PlatformTransactionManager实现。从本机 Hibernate 事务管理切换到 JTA(例如,当你的应用程序的某些部署面临分布式事务需求时)只是一个配置问题。你可以用 Spring 的 JTA 事务实现替换 Hibernate 事务管理器。事务划分和数据访问代码都不做任何更改,因为它们使用通用的事务管理 API。

对于跨多个 Hibernate 会话工厂的分布式事务,你可以将JtaTransactionManager合并为具有多个LocalSessionFactoryBean定义的事务策略。然后,每个 DAO 获得一个特定的SessionFactory引用,该引用被传递到其相应的 Bean 属性中。如果所有底层 JDBC 数据源都是事务容器数据源,那么业务服务可以在不需要特别注意的情况下在任意数量的 DAO 和任意数量的会话工厂之间划分事务,只要它使用JtaTransactionManager作为策略。

HibernateTransactionManagerJtaTransactionManager都允许使用 Hibernate 进行适当的 JVM 级缓存处理,而不需要容器特定的事务管理器查找或 JCA 连接器(如果不使用 EJB 发起事务)。

HibernateTransactionManager可以将 Hibernate JDBCConnection导出为特定DataSource的普通 JDBC 访问代码。如果只访问一个数据库,这种能力允许在完全没有 JTA 的情况下使用混合 Hibernate 和 JDBC 数据访问的高级事务划分。HibernateTransactionManager如果你通过LocalSessionFactoryBean类的dataSource属性设置了带DataSource的传入-inSessionFactory事务,则自动将 Hibernate 事务公开为 JDBC 事务。或者,你可以显式地指定DataSource,其中事务应该通过HibernateTransactionManager类的dataSource属性公开。

# 5.3.6.比较容器管理的资源和本地定义的资源

你可以在容器管理的 JNDISessionFactory和本地定义的 JNDI 之间进行切换,而无需更改一行应用程序代码。是将资源定义保留在容器中,还是保留在应用程序的本地中,主要取决于你使用的事务策略。与 Spring 定义的本地SessionFactory相比,手动注册的 JNDISessionFactory不提供任何好处。通过 Hibernate 的 JCA 连接器部署SessionFactory提供了参与 Java EE 服务器的管理基础架构的附加价值,但不会增加更多的实际价值。

Spring 的事务支持不绑定到容器。当使用 JTA 以外的任何策略进行配置时,事务支持也可以在独立或测试环境中工作。特别是在单数据库事务的典型情况下, Spring 的单资源本地事务支持是 JTA 的一种轻量级和强大的替代方案。当使用本地 EJB 无状态会话 bean 来驱动事务时,你既依赖于 EJB 容器,也依赖于 JTA,即使你只访问一个数据库,并且仅使用无状态会话 bean 通过容器管理的事务来提供声明性事务。以编程方式直接使用 JTA 还需要一个 Java EE 环境。JTA 并不只涉及 JTA 本身和 JNDIDataSource实例方面的容器依赖关系。对于非 Spring、JTA 驱动的 Hibernate 事务,你必须使用 Hibernate JCA 连接器或带有TransactionManagerLookup的额外 Hibernate 事务代码,该代码被配置为适当的 JVM 级缓存。

Spring-驱动的事务可以与本地定义的 Hibernate SessionFactory一起工作,就像它们与本地 JDBCDataSource一起工作一样,只要它们访问单个数据库。因此,当你有分布式事务需求时,你只需要使用 Spring 的 JTA 事务策略。JCA 连接器需要特定于容器的部署步骤,并且(显然)首先需要 JCA 支持。这种配置需要更多的工作,而不是部署具有本地资源定义和 Spring 驱动事务的简单 Web 应用程序。此外,如果你使用 WebLogic Express(它不提供 JCA),则通常需要容器的 Enterprise 版本。 Spring 具有跨越单个数据库的本地资源和事务的应用程序在任何 Java EE Web 容器(不包括 JTA、JCA 或 EJB)中工作,例如 Tomcat、Resin,甚至是普通的 Jetty。此外,你可以轻松地在桌面应用程序或测试套件中重用这样的中间层。

综合考虑,如果不使用 EJB,请坚持使用本地SessionFactory设置和 Spring 的HibernateTransactionManagerJtaTransactionManager。你获得了所有的好处,包括适当的事务 JVM 级缓存和分布式事务,而不会带来容器部署的不便。 Hibernate SessionFactory通过 JCA 连接器进行的 JNDI 注册只有在与 EJB 结合使用时才会增加价值。

# 5.3.7.带有 Hibernate 的虚假应用程序服务器警告

在一些具有非常严格的XADataSource实现的 JTA 环境中(目前是一些 WebLogic Server 和 WebSphere 版本),当 Hibernate 在不考虑该环境的 JTA 事务管理器的情况下进行配置时,可能会在应用程序服务器日志中显示虚假的警告或异常。这些警告或异常表示正在访问的连接不再有效或 JDBC 访问不再有效,这可能是因为事务不再活动。举个例子,这里有一个来自 WebLogic 的实际例外:

java.sql.SQLException: The transaction is no longer active - status: 'Committed'. No
further JDBC access is allowed within this transaction.

另一个常见的问题是 JTA 事务之后的连接泄漏, Hibernate 会话(以及潜在的底层 JDBC 连接)无法正确关闭。

你可以通过使 Hibernate 了解 JTA 事务管理器来解决此类问题,JTA 事务管理器与 Spring 同步到该事务管理器。你有两个选择:

  • 将你的 Spring JtaTransactionManager Bean 传递到你的 Hibernate 设置中。最简单的方法是 Bean 引用jtaTransactionManager属性中的LocalSessionFactoryBean Bean(参见Hibernate Transaction Setup)。 Spring 然后使相应的 JTA 策略可用于 Hibernate。

  • 你还可以在LocalSessionFactoryBean上的“HibernateProperties”中显式地配置 Hibernate 的 JTA 相关属性,特别是“ Hibernate.transaction.coordinator_class”、“ Hibernate.connection.handling_mode”和潜在的“ Hibernate.transaction.jta.platform”(有关这些属性的详细信息,请参见 Hibernate 的手册)。

本节的其余部分描述了在 Hibernate 意识到 JTA的情况下和不意识到 JTA的情况下发生的事件的顺序。

Hibernate 未配置任何对 JTA 事务管理器的了解时,当 JTA 事务提交时发生以下事件:

  • 提交 JTA 事务。

  • Spring 的JtaTransactionManager与 JTA 事务同步,因此 JTA 事务管理器通过afterCompletion回调回调它。

  • 在其他活动中,这种同步可以通过 Hibernate 的afterTransactionCompletion回调(用于清除 Hibernate 缓存)触发 Spring 到 Hibernate 的回调,然后在 Hibernate 会话上执行显式的close()调用,这会导致 Hibernate 尝试close()JDBC 连接。

  • 在某些环境中,这个Connection.close()调用会触发警告或错误,因为应用程序服务器不再认为Connection是可用的,因为事务已经提交。

Hibernate 配置了对 JTA 事务管理器的感知时,当 JTA 事务提交时会发生以下事件:

  • JTA 事务已准备好提交。

  • Spring 的JtaTransactionManager与 JTA 事务同步,因此 JTA 事务管理器通过beforeCompletion回调回调事务。

  • Spring 意识到 Hibernate 本身与 JTA 事务同步,并且其行为与在前面的场景中不同。特别是,它与 Hibernate 的事务性资源管理保持一致。

  • 提交 JTA 事务。

  • Hibernate 是同步到 JTA 事务的,因此该事务通过 JTA 事务管理器的afterCompletion回调被调回,并且可以正确地清除其缓存。

# 5.4. JPA

Spring JPA,在包下可用,以类似于与 Hibernate 集成的方式提供对的全面支持,同时意识到底层实现,以便提供额外的特征。

# 5.4.1.在 Spring 环境中用于 JPA 设置的三个选项

Spring JPA 支持提供了设置 JPA 的三种方式,其被应用程序用于获得实体管理器。

# 使用LocalEntityManagerFactoryBean

你只能在简单的部署环境中使用此选项,例如独立应用程序和集成测试。

LocalEntityManagerFactoryBean创建了一个EntityManagerFactory,适用于应用程序仅使用 JPA 进行数据访问的简单部署环境。工厂 Bean 使用 JPA PersistenceProvider自动检测机制(根据 JPA 的 Java SE 引导),并且在大多数情况下,只需要指定持久性单元名称。下面的 XML 示例配置了这样的 Bean:

<beans>
    <bean id="myEmf" class="org.springframework.orm.jpa.LocalEntityManagerFactoryBean">
        <property name="persistenceUnitName" value="myPersistenceUnit"/>
    </bean>
</beans>

JPA 部署的这种形式是最简单和最有限的。你无法引用现有的 JDBCDataSource Bean 定义,并且不存在对全局事务的支持。此外,持久类的编织(字节代码转换)是特定于提供者的,通常需要在启动时指定特定的 JVM 代理。此选项仅适用于 JPA 规范为其设计的独立应用程序和测试环境。

# 从 JNDI 获得 EntityManagerFactory

在部署到 Java EE 服务器时可以使用此选项。检查你的服务器的文档,了解如何将自定义 JPA 提供程序部署到你的服务器中,从而允许使用不同于服务器默认值的提供程序。

从 JNDI(例如在 Java EE 环境中)获得EntityManagerFactory是更改 XML 配置的问题,如下例所示:

<beans>
    <jee:jndi-lookup id="myEmf" jndi-name="persistence/myPersistenceUnit"/>
</beans>

此操作假定了标准的 Java EE 引导。Java EE 服务器自动检测持久化单元(实际上是应用程序 JAR 中的META-INF/persistence.xml文件)和persistence-unit-refJava EE 部署描述符中的条目(例如,web.xml),并为这些持久化单元定义环境命名上下文位置。

在这样的场景中,整个持久性单元部署,包括持久性类的编织(字节代码转换),由 Java EE 服务器决定。JDBCDataSource是通过META-INF/persistence.xml文件中的一个 JNDI 位置定义的。EntityManager事务与服务器的 JTA 子系统集成在一起。 Spring 仅使用获得的EntityManagerFactory,通过依赖注入将其传递给应用程序对象,并为持久性单元管理事务(通常通过JtaTransactionManager)。

如果在同一个应用程序中使用多个持久化单元,则此类 JNDI 检索的持久化单元的 Bean 名称应该与应用程序用于引用它们的持久化单元名称相匹配(例如,在@PersistenceUnit@PersistenceContext注释中)。

# 使用LocalContainerEntityManagerFactoryBean

你可以在基于 Spring 的应用程序环境中使用此选项以获得完整的 JPA 功能。这包括诸如 Tomcat 之类的 Web 容器、独立的应用程序以及具有复杂持久性需求的集成测试。

如果你想专门配置一个 Hibernate 设置,那么一个直接的替代方案
是设置一个本机 Hibernate LocalSessionFactoryBean,而不是一个普通的 JPA LocalContainerEntityManagerFactoryBean,让它与 JPA 访问代码
以及本机 Hibernate 访问代码进行交互。
详情请参见Native Hibernate setup for JPA interaction

LocalContainerEntityManagerFactoryBean提供了对EntityManagerFactory配置的完全控制,并且适合于需要细粒度定制的环境。LocalContainerEntityManagerFactoryBean基于persistence.xml文件、提供的dataSourceLookup策略和指定的loadTimeWeaver创建一个PersistenceUnitInfo实例。因此,可以使用 JNDI 之外的自定义数据源并控制编织过程。下面的示例显示了LocalContainerEntityManagerFactoryBean的典型 Bean 定义:

<beans>
    <bean id="myEmf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="someDataSource"/>
        <property name="loadTimeWeaver">
            <bean class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver"/>
        </property>
    </bean>
</beans>

下面的示例显示了一个典型的persistence.xml文件:

<persistence xmlns="http://java.sun.com/xml/ns/persistence" version="1.0">
    <persistence-unit name="myUnit" transaction-type="RESOURCE_LOCAL">
        <mapping-file>META-INF/orm.xml</mapping-file>
        <exclude-unlisted-classes/>
    </persistence-unit>
</persistence>
<exclude-unlisted-classes/>快捷方式表示不应该对
带注释的实体类进行扫描。显式的’true’值
<exclude-unlisted-classes>true</exclude-unlisted-classes/>)也表示没有扫描。<exclude-unlisted-classes>false</exclude-unlisted-classes/>确实会触发扫描。
但是,如果你希望发生实体类扫描,我们建议省略exclude-unlisted-classes元素

JPA 使用LocalContainerEntityManagerFactoryBean是最强大的设置选项,允许在应用程序中进行灵活的本地配置。它支持到现有 JDBCDataSource的链接,支持本地和全局事务,以此类推。然而,它也对运行时环境提出了要求,例如,如果持久性提供程序要求进行字节码转换,则需要一个具有编织能力的类装入器。

此选项可能与 Java EE 服务器的内置 JPA 功能相冲突。在完整的 Java EE 环境中,考虑从 JNDI 获得EntityManagerFactory。或者,在你的LocalContainerEntityManagerFactoryBean定义(例如,meta-inf/my-persistence.xml)上指定一个自定义的persistenceXmlLocation,并在应用程序 jar 文件中仅包含带有该名称的描述符。因为 Java EE 服务器只查找默认的META-INF/persistence.xml文件,所以它忽略了这些自定义的持久性单元,从而避免了与 Spring 驱动的 JPA 预先设置的冲突。(例如,这适用于树脂 3.1)

什么时候需要进行负载-时间编织?

并非所有 JPA 提供者都需要 JVM 代理。 Hibernate 是一个不这样做的例子。如果你的提供者不需要代理,或者你有其他的选择,例如在构建时通过自定义编译器或 Ant 任务应用增强,那么你不应该使用加载时 Weaver。

LoadTimeWeaver接口是 Spring 提供的类,它允许以特定的方式插入 JPA ClassTransformer实例,这取决于环境是 Web 容器还是应用程序服务器。通过agent (opens new window)来挂钩ClassTransformers通常是无效的。代理针对整个虚拟机工作,并检查加载的每个类,这在生产服务器环境中通常是不希望的。

Spring 为各种环境提供了许多LoadTimeWeaver实现,使得ClassTransformer实例仅适用于每个类装入器,而不适用于每个 VM。

有关LoadTimeWeaver实现及其设置的更多了解,请参见 AOP 章中的Spring configuration,这些实现及其设置可以是通用的,也可以是针对各种平台(例如 Tomcat、JBoss 和 WebSphere)定制的。

Spring configuration中所述,你可以通过使用@EnableLoadTimeWeaving注释或context:load-time-weaverXML 元素来配置上下文范围的LoadTimeWeaver。所有 JPA LocalContainerEntityManagerFactoryBean实例都会自动拾取这样的全局 weaver。下面的示例展示了设置加载时 Weaver、交付平台的自动检测(例如 Tomcat 的可编织的类装入器或 Spring 的 JVM 代理)以及将 Weaver 自动传播到所有可感知 Weaver 的 bean 的首选方法:

<context:load-time-weaver/>
<bean id="emf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
    ...
</bean>

但是,如果需要,你可以通过loadTimeWeaver属性手动指定一个专用的 Weaver,如下例所示:

<bean id="emf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
    <property name="loadTimeWeaver">
        <bean class="org.springframework.instrument.classloading.ReflectiveLoadTimeWeaver"/>
    </property>
</bean>

无论 LTW 是如何配置的,通过使用该技术, JPA 依赖于检测的应用程序可以在目标平台(例如 Tomcat)中运行,而不需要代理。当托管应用程序依赖于不同的实现方式时,这一点尤其重要,因为 JPA 变压器仅在类装入器级别上应用,并且因此彼此隔离。

# 处理多个持久性单元

对于依赖于多个持久性单元位置的应用程序(例如,存储在 Classpath 中的各种 JAR 中), Spring 提供了PersistenceUnitManager作为中心存储库并避免持久性单元发现过程,这可能是昂贵的。默认实现允许指定多个位置。这些位置会被解析,然后通过持久性单元名称检索。(默认情况下,在 Classpath 中搜索META-INF/persistence.xml文件。)以下示例配置了多个位置:

<bean id="pum" class="org.springframework.orm.jpa.persistenceunit.DefaultPersistenceUnitManager">
    <property name="persistenceXmlLocations">
        <list>
            <value>org/springframework/orm/jpa/domain/persistence-multi.xml</value>
            <value>classpath:/my/package/**/custom-persistence.xml</value>
            <value>classpath*:META-INF/persistence.xml</value>
        </list>
    </property>
    <property name="dataSources">
        <map>
            <entry key="localDataSource" value-ref="local-db"/>
            <entry key="remoteDataSource" value-ref="remote-db"/>
        </map>
    </property>
    <!-- if no datasource is specified, use this one -->
    <property name="defaultDataSource" ref="remoteDataSource"/>
</bean>

<bean id="emf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
    <property name="persistenceUnitManager" ref="pum"/>
    <property name="persistenceUnitName" value="myCustomUnit"/>
</bean>

默认实现允许对PersistenceUnitInfo实例(在它们被馈送到 JPA 提供程序之前)进行定制,可以是声明式的(通过其属性,这会影响所有托管单元),也可以是程序化的(通过PersistenceUnitPostProcessor,这允许持久性单元选择)。如果指定了 noPersistenceUnitManager,则由LocalContainerEntityManagerFactoryBean在内部创建和使用一个。

# 背景引导

LocalContainerEntityManagerFactoryBean支持通过bootstrapExecutor属性进行后台引导,如下例所示:

<bean id="emf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
    <property name="bootstrapExecutor">
        <bean class="org.springframework.core.task.SimpleAsyncTaskExecutor"/>
    </property>
</bean>

JPA 提供程序的实际引导将交给指定的执行器,然后并行地运行到应用程序引导线程。公开的EntityManagerFactory代理可以被注入到其他应用程序组件中,甚至能够响应EntityManagerFactoryInfo配置检查。然而,一旦实际的 JPA 提供程序被其他组件访问(例如,调用createEntityManager),这些调用就会阻塞,直到后台引导完成为止。特别是,当你使用 Spring 数据 JPA 时,请确保也为其存储库设置延迟引导。

# 5.4.2.基于 JPA 实现 DAO:EntityManagerFactoryEntityManager

虽然EntityManagerFactory实例是线程安全的,但EntityManager实例不是。注入的 JPA EntityManager的行为类似于从
应用服务器的 JNDI 环境中获取的EntityManager,如 JPA 规范所定义的。它委托
对当前事务EntityManager的所有调用(如果有的话)。否则,它将返回
到每个操作新创建的EntityManager,实际上使其使用线程安全。

通过使用注入的EntityManagerFactoryEntityManager,可以在没有任何 Spring 依赖关系的情况下针对普通 JPA 编写代码。 Spring 如果启用了PersistenceAnnotationBeanPostProcessor@PersistenceContext注释,则可以在字段和方法级别上同时理解@PersistenceUnit注释。下面的示例显示了使用@PersistenceUnit注释的普通 JPA DAO 实现:

Java

public class ProductDaoImpl implements ProductDao {

    private EntityManagerFactory emf;

    @PersistenceUnit
    public void setEntityManagerFactory(EntityManagerFactory emf) {
        this.emf = emf;
    }

    public Collection loadProductsByCategory(String category) {
        EntityManager em = this.emf.createEntityManager();
        try {
            Query query = em.createQuery("from Product as p where p.category = ?1");
            query.setParameter(1, category);
            return query.getResultList();
        }
        finally {
            if (em != null) {
                em.close();
            }
        }
    }
}

Kotlin

class ProductDaoImpl : ProductDao {

    private lateinit var emf: EntityManagerFactory

    @PersistenceUnit
    fun setEntityManagerFactory(emf: EntityManagerFactory) {
        this.emf = emf
    }

    fun loadProductsByCategory(category: String): Collection<*> {
        val em = this.emf.createEntityManager()
        val query = em.createQuery("from Product as p where p.category = ?1");
        query.setParameter(1, category);
        return query.resultList;
    }
}

前面的 DAO 对 Spring 没有依赖性,并且仍然很好地适合 Spring 应用程序上下文。此外,DAO 利用注释的优势要求注入缺省的EntityManagerFactory,如下例 Bean 的定义所示:

<beans>

    <!-- bean post-processor for JPA annotations -->
    <bean class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor"/>

    <bean id="myProductDao" class="product.ProductDaoImpl"/>

</beans>

作为显式定义PersistenceAnnotationBeanPostProcessor的替代方法,可以考虑在应用程序上下文配置中使用 Spring context:annotation-configXML 元素。这样做会自动注册用于基于注释的配置的所有 Spring 标准后处理器,包括CommonAnnotationBeanPostProcessor等。

考虑以下示例:

<beans>

    <!-- post-processors for all standard config annotations -->
    <context:annotation-config/>

    <bean id="myProductDao" class="product.ProductDaoImpl"/>

</beans>

这种 DAO 的主要问题是,它总是通过工厂创建一个新的EntityManager。可以通过请求注入事务EntityManager(也称为“共享 EntityManager”,因为它是实际事务 EntityManager 的共享、线程安全代理)而不是工厂来避免这种情况。下面的示例展示了如何做到这一点:

Java

public class ProductDaoImpl implements ProductDao {

    @PersistenceContext
    private EntityManager em;

    public Collection loadProductsByCategory(String category) {
        Query query = em.createQuery("from Product as p where p.category = :category");
        query.setParameter("category", category);
        return query.getResultList();
    }
}

Kotlin

class ProductDaoImpl : ProductDao {

    @PersistenceContext
    private lateinit var em: EntityManager

    fun loadProductsByCategory(category: String): Collection<*> {
        val query = em.createQuery("from Product as p where p.category = :category")
        query.setParameter("category", category)
        return query.resultList
    }
}

@PersistenceContext注释具有一个名为type的可选属性,该属性缺省为PersistenceContextType.TRANSACTION。你可以使用此默认值来接收共享的EntityManager代理。另一种选择,PersistenceContextType.EXTENDED,则是完全不同的事情。这导致所谓的扩展EntityManager,它不是线程安全的,因此,不能在并发访问的组件中使用,例如 Spring 管理的单例 Bean。扩展的EntityManager实例仅应在有状态组件中使用,例如,这些组件驻留在会话中,而EntityManager的生命周期不与当前事务绑定,而是完全由应用程序决定。

方法和场级注入

你可以在类中的字段或方法上应用指示依赖注入的注释(例如@PersistenceUnit@PersistenceContext),因此产生了表达式“方法级注入”和“字段级注入”。字段级别的注释简洁且易于使用,而方法级别的注释允许对注入的依赖项进行进一步处理。在这两种情况下,成员可见性(公共的、受保护的或私有的)并不重要。

那么类级别的注释呢?

在 Java EE 平台上,它们用于依赖性声明,而不是用于资源注入。

注入的EntityManager是 Spring-管理的(了解正在进行的事务)。尽管新的 DAO 实现使用了方法级注入EntityManager而不是EntityManagerFactory,但由于注释的使用,不需要对应用程序上下文 XML 进行更改。

这种 DAO 风格的主要优点是它仅依赖于 Java 持久性 API。不需要任何 Spring 类的进口。此外,如 JPA 注释所理解的,注入由 Spring 容器自动应用。从非侵犯性的角度来看,这是很有吸引力的,并且对于 JPA 开发人员来说可能会感觉更自然。

# 5.4.3. Spring-驱动的 JPA 交易

我们强烈建议你阅读声明式事务管理,如果你还没有
,以获得 Spring 的声明性事务支持的更详细的覆盖。

JPA 的推荐策略是通过 JPA 的本机事务支持进行本地事务。 Spring 的JpaTransactionManager针对任何常规的 JDBC 连接池(不需要 XA),提供了从本地 JDBC 事务中已知的许多功能(例如特定于事务的隔离级别和资源级别的只读优化)。

Spring JPA 还允许配置的JpaTransactionManager向访问相同DataSource的 JDBC 访问代码公开 JPA 事务,前提是已注册的JpaDialect支持对底层 JDBCConnection的检索。 Spring 为 EclipseLink 和 Hibernate JPA 实现方式提供了方言。有关下一节机制的详细信息,请参见下一节

作为一种直接的替代方案, Spring 的本机HibernateTransactionManager能够与 JPA 访问代码进行交互,适应多个 Hibernate 细节并提供
JDBC 交互。这与LocalSessionFactoryBean设置结合在一起特别有意义。详见Native Hibernate Setup for JPA Interaction

# 5.4.4.理解JpaDialectJpaVendorAdapter

作为一个高级特性,JpaTransactionManagerAbstractEntityManagerFactoryBean的子类允许将自定义JpaDialect传递到jpaDialect Bean 属性中。 Spring 支持的JpaDialect实现通常以特定于供应商的方式实现以下高级特性:

  • 应用特定的事务语义(例如自定义隔离级别或事务超时)

  • 检索事务性 JDBCConnection(用于暴露于基于 JDBC 的 DAO)

  • PersistenceExceptions高级翻译为 Spring DataAccessExceptions

这对于特殊的事务语义和异常的高级转换特别有价值。默认实现(DefaultJpaDialect)不提供任何特殊功能,如果需要前面列出的功能,则必须指定适当的方言。

作为主要针对 Spring 的全功能LocalContainerEntityManagerFactoryBean设置的更广泛的提供者适配工具,JpaVendorAdapter

功能与其他提供者特定的默认值结合在一起。指定HibernateJpaVendorAdapterEclipseLinkJpaVendorAdapter是分别为 Hibernate 或 EclipseLink、
自动配置EntityManagerFactory设置的最方便的
方式。请注意,这些提供程序适配器主要设计用于
Spring 驱动的事务管理(即用于JpaTransactionManager)。

参见[JpaDialect](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/orm/ JPA/jpavendoric.html)和[JpaVendorAdapter](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/ork/orm/[pavendorkframework]/jpavendoradoc),以了解其操作的更多细节,以及如何在[306

# 5.4.5.使用 JTA 事务管理设置 JPA

作为JpaTransactionManager的替代方案, Spring 还允许通过 JTA 进行多资源事务协调,可以是在 Java EE 环境中,也可以是使用独立的事务协调器,例如 Atomikos。除了选择 Spring 的JtaTransactionManager而不是JpaTransactionManager之外,还需要采取一些进一步的步骤:

  • 底层的 JDBC 连接池需要具有 XA 功能,并与事务协调器集成在一起。在 Java EE 环境中,这通常很简单,通过 JNDI 公开不同类型的DataSource。有关详细信息,请参阅你的应用程序服务器文档。类似地,独立的事务协调器通常带有特殊的 XA 集成DataSource变体。再一次,检查它的文档。

  • 需要为 JTA 配置 JPA EntityManagerFactory设置。这是特定于提供者的,通常通过在LocalContainerEntityManagerFactoryBean上指定为jpaProperties的特殊属性。在 Hibernate 的情况下,这些属性甚至是版本特定的。有关详细信息,请参阅你的 Hibernate 文档。

  • Spring 的HibernateJpaVendorAdapter强制执行某些面向 Spring 的默认值,例如连接释放模式,on-close,该模式与 Hibernate 自己在 Hibernate 5.0 中的默认值相匹配,但在 Hibernate 5.1+ 中不再匹配。对于 JTA 设置,请确保将你的持久性单元事务类型声明为“JTA”。或者,将 Hibernate 5.2 的hibernate.connection.handling_mode属性设置为DELAYED_ACQUISITION_AND_RELEASE_AFTER_STATEMENT,以恢复 Hibernate 自己的默认值。相关注解见Spurious Application Server Warnings with Hibernate

  • 或者,考虑从应用程序服务器本身获得EntityManagerFactory(即通过 JNDI 查找,而不是本地声明的LocalContainerEntityManagerFactoryBean)。服务器提供的EntityManagerFactory在你的服务器配置中可能需要特殊的定义(使得部署不那么可移植),但它是为服务器的 JTA 环境设置的。

# 5.4.6.用于 JPA 交互的本机 Hibernate 设置和本机 Hibernate 事务

结合LocalSessionFactoryBean的本机HibernateTransactionManager设置允许与@PersistenceContext和其他 JPA 访问代码进行交互。一个 Hibernate SessionFactory本地实现了 JPA 的EntityManagerFactory接口,而一个 Hibernate Session句柄本地实现了一个 JPA EntityManager。 Spring 的 JPA 支持设施自动检测本机 Hibernate 会话。

因此,这样的本机 Hibernate 设置可以在许多场景中作为标准 JPA LocalContainerEntityManagerFactoryBeanJpaTransactionManager组合的替代,允许在相同的本地事务中与SessionFactory.getCurrentSession()(以及HibernateTemplate)旁边的@PersistenceContext EntityManager进行交互。这样的设置还提供了更强的 Hibernate 集成和更多的配置灵活性,因为它不受 JPA 引导程序契约的约束。

在这样的场景中,你不需要HibernateJpaVendorAdapter配置,因为 Spring 的本机 Hibernate 设置提供了更多的功能(例如,自定义 Hibernate Integrator 设置, Hibernate 5.3 Bean 容器集成,以及针对只读事务的更强优化)。最后但并非最不重要的是,你还可以通过LocalSessionFactoryBuilder表示本机 Hibernate 设置,与@Bean样式配置无缝集成(不涉及FactoryBean)。

LocalSessionFactoryBeanLocalSessionFactoryBuilder支持后台
引导,就像 JPA LocalContainerEntityManagerFactoryBean所做的那样。
参见背景引导介绍。

LocalSessionFactoryBean上,这可以通过bootstrapExecutor属性获得。在程序化的LocalSessionFactoryBuilder上,重载的buildSessionFactory方法接受一个 BootStrap 执行器参数。

# 6. 使用对象-XML 映射器编组 XML

# 6.1.导言

本章介绍 Spring 的对象-XML 映射支持。对象-XML 映射(Object-XML mapping,简称 O-X 映射)是将 XML 文档转换为对象和从对象转换的行为。这种转换过程也称为 XML 编组,或 XML 序列化。本章交替使用这些术语。

在 O-X 映射领域中,编组器负责将对象(图)序列化为 XML。以类似的方式,反编组器将 XML 反序列化为一个对象图。这个 XML 可以采取 DOM 文档、输入或输出流或 SAX 处理程序的形式。

针对你的 O/X 映射需求使用 Spring 的一些好处是:

# 6.1.1.易于配置

Spring 的 Bean 工厂使得很容易配置编组器,而不需要构建 JAXB 上下文、JiBX 绑定工厂等。你可以像在你的应用程序上下文中的任何其他 Bean 那样配置编组器。此外,基于 XML 名称空间的配置可用于许多编组器,这使得配置更加简单。

# 6.1.2.一致的接口

Spring 的 O-X 映射通过两个全球接口进行操作:[Marshaller](https://DOCS. Spring.io/ Spring-Framework/DOCS/5.3.16/javadoc-api/org/SpringFramework/OXM/Marshaller.html)和[Unmarshaller(https://DOCS. Spring.io/ Spring/ Spring-framework/DOCS/5.3.16/javadoc-springframework/OXM/org/unmarshaller.html)。这些抽象允许你相对轻松地切换 O-X 映射框架,只需对进行编组的类进行很少或根本不需要更改。这种方法还有一个额外的好处,那就是可以使用混合匹配的方法(例如,使用 JAXB 和 XStream 执行一些编组)以非侵入性的方式进行 XML 编组,从而使你能够利用每种技术的优势。

# 6.1.3.一致的异常层次结构

Spring 提供了从底层 O-X 映射工具的异常到其自身的异常层次结构的转换,并将XmlMappingException作为根异常。这些运行时异常会对原始异常进行包装,这样就不会丢失任何信息。

# 6.2.MarshallerUnmarshaller

正如导言中所述,编组器将对象序列化为 XML,而解组器将 XML 流反序列化为对象。本节描述了用于此目的的两个 Spring 接口。

# 6.2.1.理解Marshaller

Spring 抽象org.springframework.oxm.Marshaller接口后面的所有编组操作,其主要方法如下:

public interface Marshaller {

    /**
     * Marshal the object graph with the given root into the provided Result.
     */
    void marshal(Object graph, Result result) throws XmlMappingException, IOException;
}

Marshaller接口有一个主要方法,它将给定对象封送到给定的javax.xml.transform.Result。其结果是一个标记接口,它基本上表示一个 XML 输出抽象。具体的实现封装了各种 XML 表示,如下表所示:

Result implementation 包装 XML 表示
DOMResult org.w3c.dom.Node
SAXResult org.xml.sax.ContentHandler
StreamResult java.io.Filejava.io.OutputStream,或java.io.Writer
尽管marshal()方法接受一个普通对象作为其第一个参数,但大多数Marshaller实现不能处理任意对象。相反,对象类
必须映射到映射文件中,用注释标记,用
编组器注册,或者有一个公共的基类。请参阅本章
中后面的部分,以确定你的 O-X 技术如何管理这一点。

# 6.2.2.理解Unmarshaller

Marshaller类似,我们有org.springframework.oxm.Unmarshaller接口,下面的清单显示了该接口:

public interface Unmarshaller {

    /**
     * Unmarshal the given provided Source into an object graph.
     */
    Object unmarshal(Source source) throws XmlMappingException, IOException;
}

这个接口也有一个方法,它从给定的javax.xml.transform.Source(一个 XML 输入抽象)读取并返回读取的对象。与Result一样,Source是一个标记接口,具有三个具体的实现。每一种都包装了不同的 XML 表示形式,如下表所示:

Source implementation 包装 XML 表示
DOMSource org.w3c.dom.Node
SAXSource org.xml.sax.InputSource,和org.xml.sax.XMLReader
StreamSource java.io.Filejava.io.InputStream,或java.io.Reader

尽管有两个独立的编组接口(MarshallerUnmarshaller), Spring-WS 中的所有实现都在一个类中实现这两个接口。这意味着你可以连接一个 Marshaller 类,并在applicationContext.xml中同时将其称为 Marshaller 和 unshaller。

# 6.2.3.理解XmlMappingException

Spring 将来自底层 O-X 映射工具的异常转换为其自身的异常层次结构,并将XmlMappingException作为根异常。这些运行时异常将包装原始异常,这样就不会丢失任何信息。

此外,MarshallingFailureExceptionUnmarshallingFailureException提供了编组和解组操作之间的区别,即使底层的 O-X 映射工具不这样做。

O-X 映射异常层次结构如下图所示:

OXM 例外

# 6.3.使用MarshallerUnmarshaller

你可以在各种各样的情况下使用 Spring 的 OXM。在下面的示例中,我们使用它将 Spring 管理的应用程序的设置编组为 XML 文件。在下面的示例中,我们使用一个简单的 JavaBean 来表示设置:

Java

public class Settings {

    private boolean fooEnabled;

    public boolean isFooEnabled() {
        return fooEnabled;
    }

    public void setFooEnabled(boolean fooEnabled) {
        this.fooEnabled = fooEnabled;
    }
}

Kotlin

class Settings {
    var isFooEnabled: Boolean = false
}

应用程序类使用此 Bean 来存储其设置。除了一个主方法之外,这个类还有两个方法:saveSettings()将设置保存到一个名为settings.xml的文件中,loadSettings()再次加载这些设置。下面的main()方法构造了一个 Spring 应用程序上下文,并调用这两个方法:

Java

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.oxm.Marshaller;
import org.springframework.oxm.Unmarshaller;

public class Application {

    private static final String FILE_NAME = "settings.xml";
    private Settings settings = new Settings();
    private Marshaller marshaller;
    private Unmarshaller unmarshaller;

    public void setMarshaller(Marshaller marshaller) {
        this.marshaller = marshaller;
    }

    public void setUnmarshaller(Unmarshaller unmarshaller) {
        this.unmarshaller = unmarshaller;
    }

    public void saveSettings() throws IOException {
        try (FileOutputStream os = new FileOutputStream(FILE_NAME)) {
            this.marshaller.marshal(settings, new StreamResult(os));
        }
    }

    public void loadSettings() throws IOException {
        try (FileInputStream is = new FileInputStream(FILE_NAME)) {
            this.settings = (Settings) this.unmarshaller.unmarshal(new StreamSource(is));
        }
    }

    public static void main(String[] args) throws IOException {
        ApplicationContext appContext =
                new ClassPathXmlApplicationContext("applicationContext.xml");
        Application application = (Application) appContext.getBean("application");
        application.saveSettings();
        application.loadSettings();
    }
}

Kotlin

class Application {

    lateinit var marshaller: Marshaller

    lateinit var unmarshaller: Unmarshaller

    fun saveSettings() {
        FileOutputStream(FILE_NAME).use { outputStream -> marshaller.marshal(settings, StreamResult(outputStream)) }
    }

    fun loadSettings() {
        FileInputStream(FILE_NAME).use { inputStream -> settings = unmarshaller.unmarshal(StreamSource(inputStream)) as Settings }
    }
}

private const val FILE_NAME = "settings.xml"

fun main(args: Array<String>) {
    val appContext = ClassPathXmlApplicationContext("applicationContext.xml")
    val application = appContext.getBean("application") as Application
    application.saveSettings()
    application.loadSettings()
}

Application需要同时设置marshallerunmarshaller属性。我们可以通过使用以下applicationContext.xml来做到这一点:

<beans>
    <bean id="application" class="Application">
        <property name="marshaller" ref="xstreamMarshaller" />
        <property name="unmarshaller" ref="xstreamMarshaller" />
    </bean>
    <bean id="xstreamMarshaller" class="org.springframework.oxm.xstream.XStreamMarshaller"/>
</beans>

这个应用程序上下文使用 XStream,但我们可以使用本章后面描述的任何其他 Marshaller 实例。请注意,默认情况下,XStream 不需要任何进一步的配置,因此 Bean 的定义相当简单。还要注意,XStreamMarshaller同时实现了MarshallerUnmarshaller,因此我们可以在xstreamMarshallerunmarshaller这两个应用程序的属性中引用xstreamMarshaller Bean。

此示例应用程序生成以下settings.xml文件:

<?xml version="1.0" encoding="UTF-8"?>
<settings foo-enabled="false"/>

# 6.4.XML 配置命名空间

通过使用 OXM 名称空间中的标记,可以更简洁地配置编组器。要使这些标记可用,你必须首先在 XML 配置文件的前导码中引用适当的模式。下面的示例展示了如何做到这一点:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:oxm="http://www.springframework.org/schema/oxm" (1)
xsi:schemaLocation="http://www.springframework.org/schema/beans
  https://www.springframework.org/schema/beans/spring-beans.xsd
  http://www.springframework.org/schema/oxm https://www.springframework.org/schema/oxm/spring-oxm.xsd"> (2)
1 引用oxm模式。
2 指定oxm模式位置。

该模式使以下元素可用:

  • [jaxb2-marshaller](#OXM-JAXB2-XSD)

  • [jibx-marshaller](#OXM-JiBX-XSD)

每个标记在其各自的编组器部分中进行了解释。但是,作为示例,JAXB2 封送编组器的配置可能类似于以下内容:

<oxm:jaxb2-marshaller id="marshaller" contextPath="org.springframework.ws.samples.airline.schema"/>

# 6.5.JAXB

JAXB 绑定编译器将 W3C XML 模式转换为一个或多个 Java 类、jaxb.properties文件,可能还有一些资源文件。JAXB 还提供了一种从带注释的 Java 类生成模式的方法。

Spring 支持 JAXB2.0API 作为 XML 编组策略,遵循[MarshallerUnmarshaller中描述的接口[MarshallerUnmarshaller](#OXM-marshaller-unmarshaller)。相应的集成类驻留在org.springframework.oxm.jaxb包中。

# 6.5.1.使用Jaxb2Marshaller

Jaxb2Marshaller类实现了 Spring 的MarshallerUnmarshaller接口。它需要一个上下文路径来操作。你可以通过设置contextPath属性来设置上下文路径。上下文路径是包含模式派生类的冒号分隔的 Java 包名称的列表。它还提供了classesToBeBound属性,它允许你设置编组器支持的类数组。模式验证是通过向 Bean 指定一个或多个模式资源来执行的,如下例所示:

<beans>
    <bean id="jaxb2Marshaller" class="org.springframework.oxm.jaxb.Jaxb2Marshaller">
        <property name="classesToBeBound">
            <list>
                <value>org.springframework.oxm.jaxb.Flight</value>
                <value>org.springframework.oxm.jaxb.Flights</value>
            </list>
        </property>
        <property name="schema" value="classpath:org/springframework/oxm/schema.xsd"/>
    </bean>

    ...

</beans>
# XML 配置命名空间

jaxb2-marshaller元素配置了org.springframework.oxm.jaxb.Jaxb2Marshaller,如下例所示:

<oxm:jaxb2-marshaller id="marshaller" contextPath="org.springframework.ws.samples.airline.schema"/>

或者,你可以通过使用class-to-be-bound子元素提供要绑定到编组器的类的列表:

<oxm:jaxb2-marshaller id="marshaller">
    <oxm:class-to-be-bound name="org.springframework.ws.samples.airline.schema.Airport"/>
    <oxm:class-to-be-bound name="org.springframework.ws.samples.airline.schema.Flight"/>
    ...
</oxm:jaxb2-marshaller>

下表描述了可用的属性:

Attribute 说明 Required
id 编组员的身份 No
contextPath JAXB 上下文路径 No

# 6.6.JiBX

JiBX 框架提供了类似于 Hibernate 为 ORM 提供的解决方案:绑定定义定义了如何将 Java 对象转换为 XML 或从 XML 转换的规则。在准备好绑定和编译类之后,JiBX 绑定编译器将增强类文件,并添加代码来处理将类的实例从 XML 转换为 XML。

有关 JiBX 的更多信息,请参见JiBX 网站 (opens new window)。 Spring 集成类驻留在org.springframework.oxm.jibx包中。

# 6.6.1.使用JibxMarshaller

JibxMarshaller类实现了MarshallerUnmarshaller接口。要进行操作,它需要封送类的名称,你可以使用targetClass属性对其进行设置。还可以通过设置bindingName属性来设置绑定名称。在下面的示例中,我们绑定Flights类:

<beans>
    <bean id="jibxFlightsMarshaller" class="org.springframework.oxm.jibx.JibxMarshaller">
        <property name="targetClass">org.springframework.oxm.jibx.Flights</property>
    </bean>
    ...
</beans>

为单个类配置了JibxMarshaller。如果要封送多个类,则必须配置具有不同的JibxMarshaller属性值的多个targetClass实例。

# XML 配置命名空间

jibx-marshaller标记配置了org.springframework.oxm.jibx.JibxMarshaller,如下例所示:

<oxm:jibx-marshaller id="marshaller" target-class="org.springframework.ws.samples.airline.schema.Flight"/>

下表描述了可用的属性:

Attribute 说明 Required
id 编组员的身份 No
target-class 这个编组器的目标类 Yes
bindingName 这个编组器使用的绑定名称 No

# 6.7.XStream

XStream 是一个简单的库,用于将对象序列化为 XML 并返回。它不需要任何映射,并生成干净的 XML。

有关 XStream 的更多信息,请参见XStream 网站 (opens new window)。 Spring 集成类驻留在org.springframework.oxm.xstream包中。

# 6.7.1.使用XStreamMarshaller

XStreamMarshaller不需要任何配置,可以直接在应用程序上下文中进行配置。为了进一步定制 XML,你可以设置一个别名映射,它由映射到类的字符串别名组成,如下例所示:

<beans>
    <bean id="xstreamMarshaller" class="org.springframework.oxm.xstream.XStreamMarshaller">
        <property name="aliases">
            <props>
                <prop key="Flight">org.springframework.oxm.xstream.Flight</prop>
            </props>
        </property>
    </bean>
    ...
</beans>
默认情况下,XStream 允许对任意类进行解编,这可能导致
不安全的 Java 序列化效果。因此,我们不建议使用XStreamMarshaller从外部源(即 Web)解编 XML,因为这可能会
导致安全漏洞。

如果你选择使用XStreamMarshaller从外部源解压缩 XML,
supportedClasses上设置supportedClasses属性,如下例所示:

<br/><bean id="xstreamMarshaller" class="org.springframework.oxm.xstream.XStreamMarshaller"><br/> <property name="supportedClasses" value="org.springframework.oxm.xstream.Flight"/><br/> ...<br/></bean><br/>

这样做可以确保只有已注册的类才有资格进行解组。

另外,你可以注册[custom
converters](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/oxm/xstream/XStreamMarshaller.html#setConverters(com.thoughtworks.xstream.converters.ConverterMatcher…​)),以确保只有你支持的类可以进行解组。除了显式支持应该支持的域类的
转换器之外,你可能希望在列表中添加一个CatchAllConverter作为最后一个转换器。作为
结果,不会调用具有较低优先级和可能的安全性
漏洞的默认 XStream 转换器。
请注意,XStream 是一个 XML 序列化库,而不是一个数据绑定库。
因此,它对名称空间的支持有限。因此,它非常不适合在 Web 服务中使用

# 7. 附录

# 7.1.XML 模式

附录的这一部分列出了用于数据访问的 XML 模式,包括以下内容:

  • [thetxschema](#XSD-schemas-tx)

  • [thejdbcschema](#XSD-schemas-jdbc)

# 7.1.1.tx模式

在 Spring 的事务全面支持中,tx标记处理配置所有这些 bean。这些标记在标题为事务管理的章节中进行了介绍。

我们强烈建议你查看带有
Spring 分布的'spring-tx.xsd'文件。该文件包含 Spring 事务
配置的 XML 模式,并涵盖tx命名空间中的所有不同元素,包括
属性默认值和类似信息。这个文件是内联记录的,因此,
这里不重复信息是为了遵守 dry(不要
重复自己)原则。

为了完整起见,要使用tx模式中的元素,你需要在 Spring XML 配置文件的顶部具有以下前导码。以下代码片段中的文本引用了正确的模式,因此tx名称空间中的标记对你是可用的:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:tx="http://www.springframework.org/schema/tx" (1)
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/tx https://www.springframework.org/schema/tx/spring-tx.xsd (2)
        http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- bean definitions here -->

</beans>
1 声明tx名称空间的用法。
2 指定位置(与其他架构位置一起)。
通常,当你使用tx命名空间中的元素时,你还使用来自aop命名空间中的
元素(因为 Spring 中的声明性事务支持是通过使用 AOP 实现的
)。前面的 XML 片段包含引用aop模式所需的相关行,以便aop名称空间中的元素对你可用

# 7.1.2.jdbc模式

jdbc元素允许你快速配置嵌入式数据库或初始化现有数据源。这些元素分别记录在嵌入式数据库支持初始化数据源中。

要使用jdbc模式中的元素,你需要在 Spring XML 配置文件的顶部具有以下前导码。以下代码片段中的文本引用了正确的模式,因此jdbc名称空间中的元素对你是可用的:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:jdbc="http://www.springframework.org/schema/jdbc" (1)
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/jdbc https://www.springframework.org/schema/jdbc/spring-jdbc.xsd"> (2)

    <!-- bean definitions here -->

</beans>