这部分参考文档涉及数据访问以及数据访问层与业务或服务层之间的交互。

详细介绍了 Spring 的综合事务管理支持,然后全面介绍了 Spring Framework 集成的各种数据访问框架和技术。

1.交易管理

全面的事务支持是使用 Spring Framework 的最令人信服的理由之一。Spring 框架为事务管理提供了一致的抽象,具有以下好处:

  • 跨不同事务 API 的一致编程模型,例如 Java Transaction API (JTA)、JDBC、Hibernate 和 Java Persistence API (JPA)。

  • 支持声明式事务管理

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

  • 与 Spring 的数据访问抽象完美集成。

以下部分描述了 Spring Framework 的事务特性和技术:

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

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

传统上,Java EE 开发人员对事务管理有两种选择:全局事务或本地事务,两者都有很大的局限性。接下来的两节将回顾全局和本地事务管理,然后讨论 Spring 框架的事务管理支持如何解决全局和本地事务模型的限制。

1.1.1。全球交易

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

以前,使用全局事务的首选方式是通过 EJB CMT(容器管理事务)。CMT 是一种声明式事务管理(不同于程序化事务管理)。EJB CMT 消除了对与事务相关的 JNDI 查找的需要,尽管使用 EJB 本身就需要使用 JNDI。它消除了大部分(但不是全部)编写 Java 代码来控制事务的需要。显着的缺点是 CMT 与 JTA 和应用程序服务器环境相关联。此外,它仅在选择在 EJB 中实现业务逻辑(或至少在事务性 EJB 外观之后)时才可用。一般来说,EJB 的负面影响是如此之大,以至于这不是一个有吸引力的提议,尤其是在面对声明式事务管理的引人注目的替代方案时。

1.1.2。本地交易

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

1.1.3。Spring Framework 的一致编程模型

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

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

您需要应用服务器来进行事务管理吗?

Spring Framework 的事务管理支持改变了关于企业 Java 应用程序何时需要应用程序服务器的传统规则。

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

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

Spring 框架让您可以选择何时将应用程序扩展到完全加载的应用程序服务器。使用 EJB CMT 或 JTA 的唯一替代方法是使用本地事务(例如 JDBC 连接上的事务)编写代码并且如果您需要该代码在全局、容器管理的事务中运行则面临大量返工的日子已经一去不复返了。使用 Spring Framework,只需更改配置文件中的一些 bean 定义(而不是您的代码)。

1.2. 了解 Spring Framework 事务抽象

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;
}

这主要是一个服务提供者接口 (SPI),尽管您可以 从应用程序代码中以编程方式使用它。因为 PlatformTransactionManager它是一个接口,所以可以根据需要轻松地模拟或存根。它不依赖于查找策略,例如 JNDI。 PlatformTransactionManager实现的定义与 Spring Framework IoC 容器中的任何其他对象(或 bean)一样。仅此一项优势就使 Spring Framework 事务成为有价值的抽象,即使您使用 JTA 也是如此。与直接使用 JTA 相比,您可以更轻松地测试事务代码。

同样,为了与 Spring 的理念保持一致TransactionException,可以由任何PlatformTransactionManager接口的方法抛出的 是未经检查的(即,它扩展了java.lang.RuntimeException类)。交易基础设施故障几乎总是致命的。在应用程序代码实际上可以从事务失败中恢复的极少数情况下,应用程序开发人员仍然可以选择捕获和处理TransactionException. 重点是开发人员并非 被迫这样做。

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

从 Spring Framework 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;
}

反应式事务管理器主要是一个服务提供者接口 (SPI),尽管您可以从应用程序代码中以编程方式使用它。因为ReactiveTransactionManager它是一个接口,所以可以根据需要轻松地模拟或存根。

TransactionDefinition接口指定:

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

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

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

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

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

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();
}

无论您在 Spring 中选择声明式还是编程式事务管理,定义正确的TransactionManager实现都是绝对必要的。您通常通过依赖注入来定义此实现。

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

DataSource您可以通过创建类似于以下内容的 bean来定义 JDBC :

<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>

然后相关的PlatformTransactionManagerbean 定义具有对该 DataSource定义的引用。它应该类似于以下示例:

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

如果您在 Java EE 容器中使用 JTA,那么您可以使用DataSource通过 JNDI 获得的容器与 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或任何其他特定资源),因为它使用容器的全局事务管理基础架构。

dataSourcebean 的前面定义使用命名空间中的<jndi-lookup/>标记jee。有关更多信息,请参阅 JEE 架构
如果您使用 JTA,那么无论您使用何种数据访问技术,无论是 JDBC、Hibernate JPA 还是任何其他受支持的技术,您的事务管理器定义都应该看起来相同。这是因为 JTA 事务是全局事务,它可以征用任何事务资源。

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

1.2.1。休眠事务设置

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

DataSourcebean 定义类似于前面显示的本地 JDBC 示例,因此在以下示例中未显示。

如果DataSource(由任何非 JTA 事务管理器使用)通过 JNDI 查找并由 Java EE 容器管理,则它应该是非事务性的,因为 Spring Framework(而不是 Java 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 和 Java EE 容器管理的 JTA 事务,您应该使用与JtaTransactionManager前面 JTA 示例中相同的 JDBC 事务,如以下示例所示。此外,建议通过其事务协调器以及可能的连接释放模式配置使 Hibernate 了解 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 到 JDBC DataSourceHibernateTransactionManager到 HibernateSessionFactory等)。本节描述应用程序代码(直接或间接地,通过使用 JDBC、Hibernate 或 JPA 等持久性 API)如何确保正确创建、重用和清理这些资源。本节还讨论了如何(可选地)通过相关的TransactionManager.

1.3.1。高级同步方法

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

1.3.2. 低级同步方法

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

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

Connection conn = DataSourceUtils.getConnection(dataSource);

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

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

当然,一旦您使用了 Spring 的 JDBC 支持、JPA 支持或 Hibernate 支持,您通常不喜欢使用DataSourceUtils其他辅助类,因为与直接使用相关 API 相比,通过 Spring 抽象工作要快乐得多。例如,如果您使用 SpringJdbcTemplatejdbc.object包来简化 JDBC 的使用,则正确的连接检索会在幕后发生,您无需编写任何特殊代码。

1.3.3.TransactionAwareDataSourceProxy

在最低级别存在TransactionAwareDataSourceProxy类。这是一个 target 的代理DataSource,它包装了 targetDataSource以增加对 Spring 管理的事务的认识。在这方面,它类似于 DataSource由 Java EE 服务器提供的事务 JNDI。

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

1.4. 声明式事务管理

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

Spring Framework 的声明式事务管理通过 Spring 面向方面编程 (AOP) 成为可能。但是,由于事务方面代码随 Spring Framework 发行版一起提供,并且可以以样板方式使用,因此通常不必理解 AOP 概念即可有效地使用此代码。

Spring Framework 的声明式事务管理类似于 EJB CMT,因为您可以指定事务行为(或缺少它)到单个方法级别。如有必要,您可以setRollbackOnly()在事务上下文中进行调用。两种类型的事务管理之间的区别是:

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

  • 您可以将 Spring Framework 声明式事务管理应用于任何类,而不仅仅是诸如 EJB 之类的特殊类。

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

  • Spring Framework 允许您使用 AOP 自定义事务行为。例如,您可以在事务回滚的情况下插入自定义行为。您还可以添加任意建议以及事务性建议。使用 EJB CMT,您无法影响容器的事务管理,但 setRollbackOnly().

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

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

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

1.4.1。了解 Spring 框架的声明式事务实现

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

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

AOP 部分 介绍了 Spring AOP 。

Spring FrameworkTransactionInterceptor为命令式和反应式编程模型提供事务管理。拦截器通过检查方法返回类型来检测所需的事务管理风格。返回响应式类型的方法,例如PublisherKotlin Flow(或其子类型)有资格进行响应式事务管理。所有其他返回类型,包括void使用代码路径进行命令式事务管理。

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

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

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

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

发送

1.4.2. 声明式事务实现示例

考虑以下接口及其伴随的实现。此示例使用 FooBar类作为占位符,以便您可以专注于事务使用,而无需关注特定的域模型。就本示例而言,类 在每个已实现方法的主体中DefaultFooService抛出实例这一事实很好。UnsupportedOperationException该行为使您可以看到正在创建的事务,然后回滚以响应 UnsupportedOperationException实例。以下清单显示了该FooService 界面:

java
// 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);

}
科特林
// 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)
}

以下示例显示了上述接口的实现:

java
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) {
        // ...
    }
}
科特林
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>

检查前面的配置。它假定您要使服务对象fooServicebean 具有事务性。要应用的事务语义封装在<tx:advice/>定义中。<tx:advice/>定义读作“所有以 开头的方法都get将在只读事务的上下文中运行,而所有其他方法都将以默认事务语义运行”。标记 的 transaction-manager属性设置为将驱动事务的 bean 的名称(在本例中为 bean)。<tx:advice/>TransactionManagertxManager

如果要连接的 bean 名称具有名称,则 可以省略transaction-manager事务通知 () 中的属性。如果要连接的 bean 有任何其他名称,则必须 显式使用该属性,如前面的示例中所示。 <tx:advice/>TransactionManagertransactionManagerTransactionManagertransaction-manager

<aop:config/>定义确保 txAdvicebean 定义的事务建议在程序中的适当点运行。首先,您定义一个切入点,该切入点与FooService接口 ( fooServiceOperation) 中定义的任何操作的执行相匹配。txAdvice然后,您使用顾问将切入点与 相关联。结果表明,在执行 afooServiceOperation时,运行了由定义的建议txAdvice

<aop:pointcut/>在元素中定义的表达式是一个 AspectJ 切入点表达式。有关 Spring 中切入点表达式的更多详细信息,请参阅AOP 部分

一个常见的要求是使整个服务层具有事务性。最好的方法是更改​​切入点表达式以匹配服务层中的任何操作。以下示例显示了如何执行此操作:

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

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

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

java
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());
    }
}
科特林
import org.springframework.beans.factory.getBean

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

运行上述程序的输出应类似于以下内容(为清楚起见,已截断类方法UnsupportedOperationException抛出 的 Log4J 输出和堆栈跟踪):insertFoo(..)DefaultFooService

<!-- 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 Framework 使用ReactiveAdapterRegistry来确定方法返回类型是否是响应式的。

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

java
// 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);

}
科特林
// 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>
}

以下示例显示了上述接口的实现:

java
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) {
        // ...
    }
}
科特林
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 使用事务运算符装饰返回的反应类型以开始和清理事务。因此,调用事务响应式方法将实际事务管理推迟到激活响应式处理的订阅类型。

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

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

反应式事务方法返回一个反应式包装器类型,它表示一个计算序列以及开始和完成计算的承诺。

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

1.4.3. 回滚声明性事务

上一节概述了如何在应用程序中以声明方式为类(通常是服务层类)指定事务设置的基础知识。本节介绍如何在 XML 配置中以简单的声明方式控制事务的回滚。有关使用@Transactional注释以声明方式控制回滚语义的详细信息,请参阅 @Transactional设置

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

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

您可以通过指定回滚规则准确配置哪些Exception类型将事务标记为回滚,包括检查的异常。

回滚规则

回滚规则确定在抛出给定异常时是否应回滚事务,并且规则基于模式。模式可以是完全限定的类名或异常类型的完全限定类名的子字符串(必须是 的子类Throwable),目前不支持通配符。例如, "javax.servlet.ServletException"or的值"ServletException"将匹配 javax.servlet.ServletException及其子类。

回滚规则可以通过rollback-forno-rollback-for 属性在 XML 中配置,允许将模式指定为字符串。使用 时 @Transactional,可以通过rollbackFor/noRollbackForrollbackForClassName/noRollbackForClassName属性配置回滚规则,它们允许将模式分别指定为Class引用或字符串。当异常类型被指定为类引用时,其完全限定名称将用作模式。因此,@Transactional(rollbackFor = example.CustomException.class)等价于@Transactional(rollbackForClassName = "example.CustomException")

您必须仔细考虑模式的具体程度以及是否包含包信息(这不是强制性的)。例如,"Exception"将匹配几乎所有内容,并且可能会隐藏其他规则。"java.lang.Exception"如果 "Exception"旨在为所有检查的异常定义规则,那将是正确的。对于更独特的异常名称,例如"BaseBusinessException"可能不需要为异常模式使用完全限定的类名。

此外,回滚规则可能会导致对类似命名的异常和嵌套类的无意匹配。这是因为如果抛出的异常的名称包含为回滚规则配置的异常模式,则抛出的异常被认为与给定的回滚规则匹配。例如,给定一个配置为匹配 on 的com.example.CustomException规则,该规则将匹配名为com.example.CustomExceptionV2的异常(与同一包中的异常 CustomException但带有附加后缀)或名为 com.example.CustomException$AnotherException的异常(声明为嵌套类的异常CustomException)。

以下 XML 片段演示了如何通过 属性Exception提供异常模式来为已检查的、特定于应用程序的类型配置回滚:rollback-for

<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 Framework 的事务基础设施即使面对未处理的事务也要提交伴随事务InstrumentNotFoundException

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

当 Spring Framework 的事务基础设施捕获到异常并查阅配置的回滚规则以确定是否将事务标记为回滚时,最强匹配规则获胜。因此,在以下配置的情况下,除了 an 之外的任何异常都会InstrumentNotFoundException导致伴随事务的回滚:

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

您还可以以编程方式指示所需的回滚。虽然很简单,但这个过程非常具有侵入性,并且将您的代码与 Spring Framework 的事务基础结构紧密耦合。以下示例显示了如何以编程方式指示所需的回滚:

java
public void resolvePosition() {
    try {
        // some business logic...
    } catch (NoProductInStockException ex) {
        // trigger rollback programmatically
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    }
}
科特林
fun resolvePosition() {
    try {
        // some business logic...
    } catch (ex: NoProductInStockException) {
        // trigger rollback programmatically
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    }
}

如果可能的话,强烈建议您使用声明性方法进行回滚。如果您绝对需要,可以使用程序化回滚,但它的使用与实现干净的基于 POJO 的架构背道而驰。

1.4.4。为不同的 Bean 配置不同的事务语义

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

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

<?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:method/>下表总结了嵌套在标签中的标签的各种属性<tx:advice/><tx:attributes/>标签:

表 1. <tx:method/> 设置
属性 必需的? 默认 描述

name

是的

与事务属性关联的方法名称。通配符 (*) 字符可用于将相同的事务属性设置与多种方法相关联(例如 、、get*等)。handle*on*Event

propagation

REQUIRED

事务传播行为。

isolation

DEFAULT

事务隔离级别。仅适用于REQUIRED或的传播设置REQUIRES_NEW

timeout

-1

事务超时(秒)。仅适用于传播REQUIREDREQUIRES_NEW.

read-only

错误的

读写与只读事务。仅适用于REQUIREDREQUIRES_NEW

rollback-for

Exception触发回滚的实例的逗号分隔列表。例如, com.foo.MyBusinessException,ServletException

no-rollback-for

Exception不触发回滚的实例的逗号分隔列表。例如, com.foo.MyBusinessException,ServletException

1.4.6。使用@Transactional

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

标准javax.transaction.Transactional注解也被支持作为 Spring 自己注解的替代品。有关详细信息,请参阅 JTA 1.2 文档。

使用注释提供的易用性@Transactional最好用一个例子来说明,这将在下面的文本中解释。考虑以下类定义:

java
// 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) {
        // ...
    }
}
科特林
// 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 认为事务性方法的更多详细信息,请参阅方法可见性@Transactional。请注意,类级别的注释不适用于类层次结构中的祖先类;在这种情况下,继承的方法需要在本地重新声明才能参与子类级别的注释。

当上面的 POJO 类被定义为 Spring 上下文中的 bean 时,您可以通过类@EnableTransactionManagement 中的注释使 bean 实例具有事务性@Configuration。有关完整的详细信息,请参阅 javadoc

在 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 实例具有事务性的行。
如果要连接 的 bean 名称具有 name ,则 可以省略标记transaction-manager中的属性。如果要依赖注入的 bean 有任何其他名称,则必须使用属性,如前面的示例中所示。 <tx:annotation-driven/>TransactionManagertransactionManagerTransactionManagertransaction-manager

与命令式编程安排相比,反应式事务方法使用反应式返回类型,如以下清单所示:

java
// 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) {
        // ...
    }
}
科特林
// 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对于 Reactive Streams 取消信号,返回有特殊的考虑。有关更多详细信息,请参阅“使用 TransactionOperator”下的取消信号部分。

方法可见性和@Transactional

当你在 Spring 的标准配置中使用事务代理时,你应该@Transactional只将注解应用于具有public可见性的方法。如果您使用注释对protectedprivate或包可见的方法进行@Transactional 注释,则不会引发错误,但带注释的方法不会显示配置的事务设置。如果您需要注释非公共方法,请考虑下一段中针对基于类的代理的提示,或考虑使用 AspectJ 编译时或加载时编织(稍后描述)。

@EnableTransactionManagement@Configuration类中使用时,protected也可以通过注册自定义transactionAttributeSourcebean 来使基于类的代理的包可见方法具有事务性,如下例所示。但是请注意,基于接口的代理中的事务方法必须始终 public在代理接口中定义。

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

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

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

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

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

表 2. 注释驱动的事务设置
XML 属性 注释属性 默认 描述

transaction-manager

不适用(请参阅TransactionManagementConfigurerjavadoc)

transactionManager

要使用的事务管理器的名称。仅当事务管理器的名称不是 时才需要transactionManager,如前面的示例中所示。

mode

mode

proxy

默认模式proxy(替代模式 ( aspectj) 将受影响的类与 Spring 的 AspectJ 事务方面进行编织,修改目标类字节码以应用于任何类型的方法调用。AspectJ 编织需要 spring-aspects.jar在类路径中以及启用加载时编织(或编译时编织)。(有关 如何设置加载时编织的详细信息,请参阅Spring 配置。)

proxy-target-class

proxyTargetClass

false

仅适用于proxy模式。控制为使用注解注解的类创建什么类型的事务代理@Transactional。如果该 proxy-target-class属性设置为true,则创建基于类的代理。如果proxy-target-classfalse或者如果省略该属性,则创建标准的基于 JDK 接口的代理。(有关不同代理类型的详细检查,请参阅代理机制 。)

order

order

Ordered.LOWEST_PRECEDENCE

定义应用到用 注释的 bean 的事务建议的顺序 @Transactional。(有关 AOP 通知排序规则的更多信息,请参阅Advice Ordering。)没有指定顺序意味着 AOP 子系统决定通知的顺序。

处理@Transactional注解的默认通知模式是proxy,它只允许通过代理拦截调用。同一类中的本地调用不能以这种方式被拦截。对于更高级的拦截模式,请考虑切换到aspectj结合编译时或加载时编织的模式。
proxy-target-class属性控制为使用注释注释的类创建什么类型的事务代理@Transactional。如果 proxy-target-class设置为true,则创建基于类的代理。如果 proxy-target-classfalse或者如果省略该属性,则会创建标准的基于 JDK 接口的代理。(有关不同代理类型的讨论,请参阅代理机制 。)
@EnableTransactionManagement并且仅在定义它们的同一应用程序上下文中<tx:annotation-driven/>查找 bean。@Transactional这意味着,如果您将注释驱动的配置放在 a WebApplicationContext for aDispatcherServlet中,它只会在您的控制器中而不是在您的服务中检查@Transactionalbean。有关详细信息,请参阅MVC

在评估方法的事务设置时,派生最多的位置优先。在以下示例中,DefaultFooService类在类级别使用只读事务的设置进行注释,但同一类中方法的 @Transactional注释updateFoo(Foo)优先于在类级别定义的事务设置。

java
@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) {
        // ...
    }
}
科特林
@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.

  • 事务是读写的。

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

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

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

表 3. @Transactional 设置
财产 类型 描述

价值

String

指定要使用的事务管理器的可选限定符。

transactionManager

String

的别名value

label

String为事务添加表达性描述的标签数组。

事务管理器可以评估标签以将特定于实现的行为与实际事务相关联。

传播

enumPropagation

可选的传播设置。

isolation

enumIsolation

可选的隔离级别。仅适用于REQUIRED或的传播值REQUIRES_NEW

timeout

int(以秒为单位)

可选的事务超时。仅适用于REQUIRED或的传播值REQUIRES_NEW

timeoutString

String(以秒为单位)

timeout将秒数指定为值的替代方法String- 例如,作为占位符。

readOnly

boolean

读写与只读事务。仅适用于REQUIRED或的值REQUIRES_NEW

rollbackFor

对象数组Class,必须派生自Throwable.

必须导致回滚的异常类型的可选数组。

rollbackForClassName

异常名称模式数组。

必须导致回滚的可选异常名称模式数组。

noRollbackFor

对象数组Class,必须派生自Throwable.

不得导致回滚的异常类型的可选数组。

noRollbackForClassName

异常名称模式数组。

不得导致回滚的异常名称模式的可选数组。

有关可能的无意匹配的回滚规则语义、模式和警告的更多详细信息, 请参阅回滚规则。

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

多个事务管理器@Transactional

大多数 Spring 应用程序只需要一个事务管理器,但在某些情况下,您可能希望在单个应用程序中拥有多个独立的事务管理器。您可以使用注释 的valueortransactionManager属性 来选择性地指定要使用的标识。这可以是 bean 名称或事务管理器 bean 的限定符值。例如,使用限定符表示法,您可以将以下 Java 代码与应用程序上下文中的以下事务管理器 bean 声明结合起来:@TransactionalTransactionManager

java
public class TransactionalService {

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

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

    @Transactional("reactive-account")
    public Mono<Void> doSomethingReactive() { ... }
}
科特林
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 限定符区分。如果未找到特别限定的 bean,则仍使用默认<tx:annotation-driven>目标 bean 名称。transactionManagerTransactionManager

自定义组合注释

如果您发现@Transactional在许多不同的方法上重复使用相同的属性,Spring 的元注释支持允许您为特定用例定义自定义组合注释。例如,考虑以下注释定义:

java
@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 {
}
科特林
@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

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

java
public class TransactionalService {

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

    @AccountTx
    public void doSomething() {
        // ...
    }
}
科特林
class TransactionalService {

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

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

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

1.4.7。交易传播

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

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

理解PROPAGATION_REQUIRED
需要 tx 道具

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

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

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

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

理解PROPAGATION_REQUIRES_NEW
tx 道具需要新的

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

理解PROPAGATION_NESTED

PROPAGATION_NESTED使用具有多个可以回滚的保存点的单个物理事务。这种部分回滚让内部事务范围触发其范围的回滚,尽管某些操作已回滚,但外部事务能够继续物理事务。此设置通常映射到 JDBC 保存点,因此它仅适用于 JDBC 资源事务。参见Spring的DataSourceTransactionManager.

1.4.8。为交易操作提供建议

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

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

  • 配置的分析方面启动。

  • 事务性建议运行。

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

  • 事务提交。

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

本章不关心详细解释 AOP(除非它适用于事务)。有关AOP 配置和一般 AOP的详细介绍,请参阅 AOP。

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

java
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;
    }
}
科特林
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界面控制的。有关建议订购的完整详细信息,请参阅 建议订购

以下配置创建了一个fooServicebean,该 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>

上述配置的结果是一个fooServicebean,它按顺序应用了分析和事务方面。如果您希望分析建议在传入的事务建议之后和传出的事务建议之前运行,您可以交换分析方面 bean 的order属性值,使其高于事务建议的订单值。

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

1.4.9。@Transactional与 AspectJ 一起使用

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

在继续之前,您可能需要分别阅读Using@TransactionalAOP

以下示例显示了如何创建事务管理器并配置 AnnotationTransactionAspect使用它:

java
// 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);
科特林
// 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 遵循 Java 的规则,即不继承接口上的注释。

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

类中方法的@Transactional注解覆盖类注解(如果存在)给出的默认事务语义。无论可见性如何,您都可以注释任何方法。

要使用 编织您的应用程序AnnotationTransactionAspect,您必须使用 AspectJ 构建您的应用程序(请参阅 AspectJ 开发指南)或使用加载时编织。有关使用 AspectJ 进行加载时编织的讨论,请参阅 Spring Framework中的使用 AspectJ 进行加载时编织。

1.5。程序化事务管理

Spring 框架提供了两种程序化事务管理方法,通过使用:

  • TransactionTemplateTransactionalOperator。_

  • TransactionManager直接实现。

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

1.5.1。使用TransactionTemplate

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

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

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

java
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();
            }
        });
    }
}
科特林
// 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带有匿名类的便捷类,如下:

java
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
    protected void doInTransactionWithoutResult(TransactionStatus status) {
        updateOperation1();
        updateOperation2();
    }
});
科特林
transactionTemplate.execute(object : TransactionCallbackWithoutResult() {
    override fun doInTransactionWithoutResult(status: TransactionStatus) {
        updateOperation1()
        updateOperation2()
    }
})

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

java
transactionTemplate.execute(new TransactionCallbackWithoutResult() {

    protected void doInTransactionWithoutResult(TransactionStatus status) {
        try {
            updateOperation1();
            updateOperation2();
        } catch (SomeBusinessException ex) {
            status.setRollbackOnly();
        }
    }
});
科特林
transactionTemplate.execute(object : TransactionCallbackWithoutResult() {

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

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

java
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...
    }
}
科特林
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...
    }
}

以下示例TransactionTemplate使用 Spring XML 配置定义了一些自定义事务设置:

<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但是,实例确实维护配置状态。因此,虽然许多类可能共享一个 a 的实例TransactionTemplate,但如果一个类需要使用TransactionTemplate具有不同设置(例如,不同的隔离级别)的 a,您需要创建两个不同的TransactionTemplate实例。

1.5.2. 使用TransactionOperator

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

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

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

java
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);
    }
}
科特林
// 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可以通过两种方式使用:

  • 使用 Project Reactor 类型的操作符样式 ( mono.as(transactionalOperator::transactional))

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

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

java
transactionalOperator.execute(new TransactionCallback<>() {

    public Mono<Object> doInTransaction(ReactiveTransaction status) {
        return updateOperation1().then(updateOperation2)
                    .doOnError(SomeBusinessException.class, e -> status.setRollbackOnly());
        }
    }
});
科特林
transactionalOperator.execute(object : TransactionCallback() {

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

在 Reactive Streams 中,aSubscriber可以取消其Subscription并停止其 Publisher. Project Reactor 以及其他库(例如next()take(long)timeout(Duration)和其他库)中的运算符可以发出取消。无法知道取消的原因,无论是由于错误还是只是缺乏进一步消费的兴趣。由于版本 5.3 取消信号导致回滚。因此,重要的是要考虑在事务下游使用的运算符 Publisher。特别是在 aFlux或其他 multi-value的情况下,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);
    }
}
科特林
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直接使用 a 来管理您的事务。为此,PlatformTransactionManager请通过 bean 引用将您使用的 bean 的实现传递给您的 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);
科特林
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直接使用 a 来管理您的事务。为此,ReactiveTransactionManager请通过 bean 引用将您使用的 bean 的实现传递给您的 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)));
});
科特林
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 Framework 而不是 EJB CMT 时,声明式事务管理的配置成本大大降低。

1.7. 事务绑定事件

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

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

下一个示例显示了这个概念。假设一个组件发布了一个订单创建的事件,并且我们想要定义一个侦听器,该侦听器应该只在发布它的事务成功提交后才处理该事件。以下示例设置了这样一个事件侦听器:

java
@Component
public class MyComponent {

    @TransactionalEventListener
    public void handleOrderCreatedEvent(CreationEvent<Order> creationEvent) {
        // ...
    }
}
科特林
@Component
class MyComponent {

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

@TransactionalEventListener注释公开了一个属性phase,可让您自定义侦听器应绑定到的事务阶段。有效阶段是BEFORE_COMMITAFTER_COMMIT(默认)、AFTER_ROLLBACK,以及 AFTER_COMPLETION聚合事务完成的阶段(无论是提交还是回滚)。

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

@TransactionalEventListener仅适用于由 PlatformTransactionManager. 由 Reactor管理的响应式事务ReactiveTransactionManager 使用 Reactor 上下文而不是线程本地属性,因此从事件侦听器的角度来看,它没有可以参与的兼容的活跃事务。

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

Spring 的事务抽象通常与应用服务器无关。此外,Spring 的JtaTransactionManager类(可以选择对 JTAUserTransactionTransactionManager对象执行 JNDI 查找)自动检测后一个对象的位置,该位置因应用服务器而异。访问 JTA TransactionManager可以增强事务语义——特别是支持事务暂停。有关详细信息,请参阅 JtaTransactionManager javadoc。

SpringJtaTransactionManager是在 Java EE 应用程序服务器上运行的标准选择,并且可以在所有常见服务器上运行。事务暂停等高级功能也可以在许多服务器(包括 GlassFish、JBoss 和 Geronimo)上运行,无需任何特殊配置。但是,对于完全支持的事务暂停和进一步的高级集成,Spring 包括用于 WebLogic Server 和 WebSphere 的特殊适配器。这些适配器将在以下部分中讨论。

对于标准场景,包括 WebLogic Server 和 WebSphere,请考虑使用方便的<tx:jta-transaction-manager/>配置元素。配置后,此元素会自动检测底层服务器并选择可用于平台的最佳事务管理器。这意味着您不需要显式配置特定于服务器的适配器类(如以下部分所述)。相反,它们是自动选择的,标准 JtaTransactionManager是默认的后备。

1.8.1。IBM WebSphere

在 WebSphere 6.1.0.9 及更高版本上,推荐使用的 Spring JTA 事务管理器是 WebSphereUowTransactionManager. 这个特殊的适配器使用 IBM 的UOWManagerAPI,它在 WebSphere Application Server 6.1.0.9 和更高版本中可用。PROPAGATION_REQUIRES_NEW使用此适配器, IBM 正式支持Spring 驱动的事务暂停(由 发起的暂停和恢复 )。

1.8.2. 甲骨文网络逻辑服务器

在 WebLogic Server 9.0 或更高版本上,您通常会使用 WebLogicJtaTransactionManager代替 stockJtaTransactionManager类。这个特殊的 WebLogic 特定的 normal 子类JtaTransactionManager在 WebLogic 管理的事务环境中支持 Spring 事务定义的全部功能,超出了标准的 JTA 语义。功能包括事务名称、每个事务的隔离级别以及在所有情况下正确恢复事务。

1.9。常见问题的解决方案

本节介绍一些常见问题的解决方案。

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

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

1.10。更多资源

有关 Spring Framework 的事务支持的更多信息,请参阅:

  • Spring 中的分布式事务,有和没有 XA是一个 JavaWorld 演示文稿,其中 Spring 的 David Syer 将指导您了解 Spring 应用程序中分布式事务的七种模式,其中三种使用 XA,四种没有。

  • Java Transaction Design StrategiesInfoQ提供的一本书,它对 Java 中的事务进行了有节奏的介绍。它还包含有关如何通过 Spring Framework 和 EJB3 配置和使用事务的并行示例。

2. DAO 支持

Spring 中的数据访问对象 (DAO) 支持旨在以一致的方式轻松使用数据访问技术(例如 JDBC、Hibernate 或 JPA)。这使您可以相当轻松地在上述持久性技术之间切换,并且还可以让您编写代码而不必担心捕获每种技术特定的异常。

2.1。一致的异常层次结构

Spring 提供了从特定于技术的异常 SQLException到其自己的异常类层次结构的便捷转换,该层次具有DataAccessException作为根异常。这些异常包装了原始异常,因此您永远不会丢失任何有关可能出错的信息的风险。

除了 JDBC 异常,Spring 还可以包装 JPA 和 Hibernate 特定的异常,将它们转换为一组集中的运行时异常。这使您可以仅在适当的层中处理大多数不可恢复的持久性异常,而无需在 DAO 中使用烦人的样板捕获和抛出块和异常声明。(您仍然可以在任何需要的地方捕获和处理异常。)如上所述,JDBC 异常(包括特定于数据库的方言)也被转换为相同的层次结构,这意味着您可以在一致的编程模型中使用 JDBC 执行一些操作.

前面的讨论适用于 Spring 对各种 ORM 框架的支持中的各种模板类。如果您使用基于拦截器的类,应用程序必须关心处理HibernateExceptions和自身,最好 分别PersistenceExceptions委托给. 这些方法将异常转换为与异常层次结构中的异常兼容的异常。未经检查,它们也可能被抛出(不过,在异常方面牺牲了通用 DAO 抽象)。convertHibernateAccessException(..)convertJpaAccessException(..)SessionFactoryUtilsorg.springframework.daoPersistenceExceptions

下图显示了 Spring 提供的异常层次结构。(请注意,图中详述的类层次结构仅显示了整个 DataAccessException层次结构的一个子集。)

数据访问异常

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

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

java
@Repository (1)
public class SomeMovieFinder implements MovieFinder {
    // ...
}
1 @Repository注释。
科特林
@Repository (1)
class SomeMovieFinder : MovieFinder {
    // ...
}
1 @Repository注释。

任何 DAO 或存储库实现都需要访问持久性资源,具体取决于所使用的持久性技术。例如,基于 JDBC 的存储库需要访问 JDBC DataSource,而基于 JPA 的存储库需要访问 EntityManager. 完成此操作的最简单方法是使用 、 或 注释之一注入此资源@Autowired依赖@Inject项 。以下示例适用于 JPA 存储库:@Resource@PersistenceContext

java
@Repository
public class JpaMovieFinder implements MovieFinder {

    @PersistenceContext
    private EntityManager entityManager;

    // ...
}
科特林
@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;
    }

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

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

java
@Repository
public class JdbcMovieFinder implements MovieFinder {

    private JdbcTemplate jdbcTemplate;

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

    // ...
}
科特林
@Repository
class JdbcMovieFinder(dataSource: DataSource) : MovieFinder {

    private val jdbcTemplate = JdbcTemplate(dataSource)

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

3. 使用 JDBC 访问数据

Spring Framework JDBC 抽象提供的价值可能最好通过下表中列出的操作序列来显示。该表显示 Spring 负责哪些操作以及您负责哪些操作。

表 4. Spring JDBC - 谁做什么?
行动 Spring

定义连接参数。

X

打开连接。

X

指定 SQL 语句。

X

声明参数并提供参数值

X

准备并运行语句。

X

设置循环以遍历结果(如果有)。

X

完成每次迭代的工作。

X

处理任何异常。

X

处理交易。

X

关闭连接、语句和结果集。

X

Spring 框架负责处理所有使 JDBC 成为如此乏味的 API 的低级细节。

3.1。为 JDBC 数据库访问选择一种方法

您可以从多种方法中进行选择,以形成您的 JDBC 数据库访问的基础。除了三种风格之外JdbcTemplate,还有一种新的SimpleJdbcInsert方法 SimpleJdbcCall优化了数据库元数据,RDBMS 对象风格采用了一种更面向对象的方法,类似于 JDO Query 设计的方法。一旦开始使用其中一种方法,您仍然可以混合搭配以包含来自不同方法的功能。所有方法都需要兼容 JDBC 2.0 的驱动程序,而一些高级功能需要 JDBC 3.0 驱动程序。

  • JdbcTemplate是经典且最流行的 Spring JDBC 方法。这种“最低级别”的方法和所有其他方法都在幕后使用 JdbcTemplate。

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

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

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

3.2. 包层次结构

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

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

  • datasource:该org.springframework.jdbc.datasource包包含一个便于 DataSource访问的实用程序类和各种简单的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你的代码时,你只需要实现回调接口,给他们一个明确定义的契约。给定类Connection提供的 一个,回调接口创建一个准备好的语句,提供 SQL 和任何必要的参数。对于创建可调用语句的接口也是如此 。该 接口从 a 的每一行中提取值。JdbcTemplatePreparedStatementCreatorCallableStatementCreatorRowCallbackHandlerResultSet

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

应始终将其DataSource配置为 Spring IoC 容器中的 bean。在第一种情况下,bean 直接提供给服务;在第二种情况下,它被提供给准备好的模板。

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

以下部分提供了一些JdbcTemplate使用示例。这些示例并不是JdbcTemplate. 请参阅相关的javadoc

查询 ( SELECT)

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

java
int rowCount = this.jdbcTemplate.queryForObject("select count(*) from t_actor", Integer.class);
科特林
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");
科特林
val countOfActorsNamedJoe = jdbcTemplate.queryForObject<Int>(
        "select count(*) from t_actor where first_name = ?", arrayOf("Joe"))!!

以下查询查找 a String

java
String lastName = this.jdbcTemplate.queryForObject(
        "select last_name from t_actor where id = ?",
        String.class, 1212L);
科特林
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);
科特林
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;
        });
科特林
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);
}
科特林
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)
}
更新 ( INSERT, UPDATE, 和DELETE)JdbcTemplate

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

以下示例插入一个新条目:

java
this.jdbcTemplate.update(
        "insert into t_actor (first_name, last_name) values (?, ?)",
        "Leonor", "Watling");
科特林
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);
科特林
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));
科特林
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))");
科特林
jdbcTemplate.execute("create table mytable (id integer, name varchar(100))")

以下示例调用存储过程:

java
this.jdbcTemplate.update(
        "call SUPPORT.REFRESH_ACTORS_SUMMARY(?)",
        Long.valueOf(unionId));
科特林
jdbcTemplate.update(
        "call SUPPORT.REFRESH_ACTORS_SUMMARY(?)",
        unionId.toLong())

稍后将介绍更复杂的存储过程支持。

JdbcTemplate最佳实践

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

使用JdbcTemplate类(和关联 NamedParameterJdbcTemplate类)时的一种常见做法是DataSource在 Spring 配置文件中配置 a,然后将该共享DataSourcebean 依赖注入到 DAO 类中。JdbcTemplate是在 setter 中为DataSource. 这导致 DAO 类似于以下内容:

java
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...
}
科特林
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。以下示例显示了如何执行此操作:

java
@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 用注释DataSourcesetter 方法@Autowired
3 JdbcTemplateDataSource. _
科特林
@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 JdbcTemplateDataSource. _

以下示例显示了相应的 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(..)从该类继承一个方法 JdbcDaoSupport。你可以选择是否继承这个类。提供该类 JdbcDaoSupport只是为了方便。

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

3.3.2. 使用NamedParameterJdbcTemplate

与仅使用经典占位符 ( ) 参数NamedParameterJdbcTemplate对 JDBC 语句进行编程不同,该类增加了对使用命名参数对 JDBC 语句进行编程的支持。'?'该类NamedParameterJdbcTemplate包装了 a JdbcTemplate并委托给被包装的对象JdbcTemplate来完成它的大部分工作。本节仅描述NamedParameterJdbcTemplate类中与JdbcTemplate自身不同的那些区域——即,使用命名参数对 JDBC 语句进行编程。下面的例子展示了如何使用NamedParameterJdbcTemplate

java
// 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);
}
科特林
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)中的相应值中使用命名参数表示法。

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

以下示例显示了Map基于 - 的样式的使用:

java
// 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);
}
科特林
// 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与(并且存在于同一个 Java 包中)相关的一个很好的特性是SqlParameterSource接口。您已经在前面的代码片段之一( MapSqlParameterSource类)中看到了此接口的实现示例。AnSqlParameterSource是 a 的命名参数值的来源NamedParameterJdbcTemplate。该类MapSqlParameterSource是一个简单的实现,它是围绕 a 的适配器java.util.Map,其中键是参数名称,值是参数值。

另一个SqlParameterSource实现是BeanPropertySqlParameterSource 类。此类包装任意 JavaBean(即,遵守JavaBean 约定的类的实例)并使用包装的 JavaBean 的属性作为命名参数值的来源。

以下示例显示了一个典型的 JavaBean:

java
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...

}
科特林
data class Actor(val id: Long, val firstName: String, val lastName: String)

以下示例使用 aNamedParameterJdbcTemplate返回前面示例中显示的类的成员计数:

java
// 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);
}
科特林
// 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()方法JdbcTemplate通过 JdbcOperations接口访问被包装的实例。

有关在应用程序上下文中使用类的指南, 另请参阅JdbcTemplate最佳实践。NamedParameterJdbcTemplate

3.3.3. 使用SQLExceptionTranslator

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

SQLErrorCodeSQLExceptionTranslatorSQLExceptionTranslator 默认使用的实现。此实现使用特定的供应商代码。它比SQLState执行更精确。错误代码转换基于 JavaBean 类型类中保存的代码,称为SQLErrorCodes. 此类由 . SQLErrorCodesFactory_ 该文件由供应商代码填充,并基于 取自. 使用您正在使用的实际数据库的代码。SQLErrorCodessql-error-codes.xmlDatabaseProductNameDatabaseMetaData

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

  1. 由子类实现的任何自定义翻译。通常使用提供的混凝土 SQLErrorCodeSQLExceptionTranslator,因此该规则不适用。仅当您实际提供了子类实现时才适用。

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

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

  4. 应用错误代码匹配。

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

默认情况下SQLErrorCodesFactory用于定义Error代码和自定义异常翻译。它们在从类路径命名的文件中查找sql-error-codes.xml,匹配的SQLErrorCodes实例基于正在使用的数据库的数据库元数据中的数据库名称定位。

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

java
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;
    }
}
科特林
class CustomSQLErrorCodesTranslator : SQLErrorCodeSQLExceptionTranslator() {

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

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

java
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);
}
科特林
// 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 语句只需要很少的代码。您需要 aDataSource和 a JdbcTemplate,包括随 JdbcTemplate. 以下示例显示了创建新表的最小但功能齐全的类需要包含的内容:

java
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))");
    }
}
科特林
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(..). 后者将返回的 JDBC 转换Type为作为参数传入的 Java 类。如果类型转换无效, InvalidDataAccessApiUsageException则抛出 an。以下示例包含两种查询方法,一种查询 an int,另一种查询 a String

java
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);
    }
}
科特林
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包含每个列的一个条目,使用列名作为键。如果您在前面的示例中添加一个方法来检索所有行的列表,它可能如下所示:

java
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");
}
科特林
private val jdbcTemplate = JdbcTemplate(dataSource)

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

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

[{name=鲍勃,id=1},{name=玛丽,id=2}]

3.3.6。更新数据库

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

java
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);
    }
}
科特林
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 语句具有行参数的占位符。您可以将参数值作为可变参数传递,或者作为对象数组传递。因此,您应该在原始包装类中显式包装原始包装,或者您应该使用自动装箱。

3.3.7。检索自动生成的密钥

An update() convenience method supports the retrieval of primary keys generated by the database. This support is part of the JDBC 3.0 standard. See Chapter 13.6 of the specification for details. The method takes a PreparedStatementCreator as its first argument, and this is the way the required insert statement is specified. The other argument is a KeyHolder, which contains the generated key on successful return from the update. There is no standard single way to create an appropriate PreparedStatement (which explains why the method signature is the way it is). The following example works on Oracle but may not work on other platforms:

Java
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. Controlling Database Connections

This section covers:

3.4.1. Using DataSource

Spring obtains a connection to the database through a DataSource. A DataSource is part of the JDBC specification and is a generalized connection factory. It lets a container or a framework hide connection pooling and transaction management issues from the application code. As a developer, you need not know details about how to connect to the database. That is the responsibility of the administrator who sets up the datasource. You most likely fill both roles as you develop and test code, but you do not necessarily have to know how the production data source is configured.

When you use Spring’s JDBC layer, you can obtain a data source from JNDI, or you can configure your own with a connection pool implementation provided by a third party. Traditional choices are Apache Commons DBCP and C3P0 with bean-style DataSource classes; for a modern JDBC connection pool, consider HikariCP with its builder-style API instead.

You should use the DriverManagerDataSource and SimpleDriverDataSource classes (as included in the Spring distribution) only for testing purposes! Those variants do not provide pooling and perform poorly when multiple requests for a connection are made.

The following section uses Spring’s DriverManagerDataSource implementation. Several other DataSource variants are covered later.

To configure a DriverManagerDataSource:

  1. Obtain a connection with DriverManagerDataSource as you typically obtain a JDBC connection.

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

  3. 提供因 JDBC 驱动程序而异的 URL。(有关正确值,请参阅驱动程序的文档。)

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

以下示例显示了如何DriverManagerDataSource在 Java 中配置 a:

java
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("org.hsqldb.jdbcDriver");
dataSource.setUrl("jdbc:hsqldb:hsql://localhost:");
dataSource.setUsername("sa");
dataSource.setPassword("");
科特林
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接口应该由可以提供到关系数据库的连接的类来实现。它扩展了DataSource接口,让使用它的类查询在给定操作后是否应该关闭连接。当您知道需要重用连接时,这种用法很有效。

3.4.4。扩展AbstractDataSource

AbstractDataSourceabstractSpring 实现的基类DataSource 。它实现了所有DataSource实现通用的代码。AbstractDataSource如果您编写自己的DataSource 实现,则应该扩展该类。

3.4.5。使用SingleConnectionDataSource

该类SingleConnectionDataSource是一个SmartDataSource 接口的实现,它包装了一个Connection在每次使用后不关闭的接口。这不是多线程能力。

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

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

3.4.6。使用DriverManagerDataSource

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

此实现对于 Java EE 容器之外的测试和独立环境很有用,可以作为DataSourceSpring IoC 容器中的 bean 或与简单的 JNDI 环境结合使用。池假设Connection.close()调用关闭连接,因此任何DataSource感知持久性代码都应该工作。但是,即使在测试环境中,使用 JavaBean 样式的连接池(例如commons-dbcp)也很容易,因此使用这种连接池几乎总是优于 DriverManagerDataSource.

3.4.7。使用TransactionAwareDataSourceProxy

TransactionAwareDataSourceProxy是目标的代理DataSource。代理包装该目标DataSource以增加对 Spring 管理事务的认识。在这方面,它类似于DataSource由 Java EE 服务器提供的事务 JNDI。

很少需要使用此类,除非必须调用已经存在的代码并传递标准的 JDBCDataSource接口实现。在这种情况下,您仍然可以使用此代码,同时让此代码参与 Spring 托管事务。通常最好使用更高级别的资源管理抽象来编写自己的新代码,例如 JdbcTemplateDataSourceUtils

有关更多详细信息,请参阅TransactionAwareDataSourceProxy javadoc。

3.4.8。使用DataSourceTransactionManager

该类DataSourceTransactionManagerPlatformTransactionManager 单个 JDBC 数据源的实现。它将指定数据源的 JDBC 连接绑定到当前执行的线程,可能允许每个数据源有一个线程连接。

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

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

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

3.5. JDBC 批量操作

如果您批量调用同一个预处理语句,大多数 JDBC 驱动程序会提供改进的性能。通过将更新分组为批次,您可以限制到数据库的往返次数。

3.5.1。基本批处理操作JdbcTemplate

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

java
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
}
科特林
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. 具有对象列表的批处理操作

JdbcTemplate和都NamedParameterJdbcTemplate提供了另​​一种提供批量更新的方法。您无需实现特殊的批处理接口,而是将调用中的所有参数值作为列表提供。框架循环这些值并使用内部准备好的语句设置器。API 会有所不同,具体取决于您是否使用命名参数。对于命名参数,您为 SqlParameterSource批处理的每个成员提供一个数组,一个条目。您可以 SqlParameterSourceUtils.createBatch使用方便的方法来创建这个数组,传入一个 bean 样式的对象数组(带有对应于参数的 getter 方法)、 String-keyedMap实例(包含相应的参数作为值)或两者的混合。

以下示例显示了使用命名参数的批量更新:

java
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
}
科特林
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?占位符:

java
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
}
科特林
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 类型需要从给定的 Java 类型派生。虽然这通常效果很好,但可能会出现问题(例如,使用 Map 包含的 null值)。ParameterMetaData.getParameterType默认情况下,Spring会在这种情况下调用,这对于您的 JDBC 驱动程序来说可能会很昂贵。如果遇到性能问题(如 Oracle 12c、JBoss 和 PostgreSQL 上的报告),您应该使用最新的驱动程序版本并考虑将spring.jdbc.getParameterType.ignore属性设置为true (作为 JVM 系统属性或通过 SpringProperties机制)。

或者,您可以考虑显式指定相应的 JDBC 类型,或者通过 a BatchPreparedStatementSetter(如前所示)、通过提供给List<Object[]>基于调用的显式类型数组、通过registerSqlType对自定义MapSqlParameterSource实例的调用或通过BeanPropertySqlParameterSource 从 Java 派生 SQL 类型的a - 声明的属性类型,即使是空值。

3.5.3. 多批次的批次操作

前面的批处理更新示例处理的批处理太大,以至于您想将它们分成几个较小的批处理。您可以使用前面提到的方法通过多次调用该batchUpdate方法来做到这一点,但现在有一种更方便的方法。除了 SQL 语句之外,此方法还需要 a Collection对象,其中包含参数、为每个批次进行的更新次数,以及ParameterizedPreparedStatementSetter为准备好的语句的参数设置值。框架循环提供的值并将更新调用分成指定大小的批次。

以下示例显示了使用批量大小为 100 的批量更新:

java
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
}
科特林
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 操作

SimpleJdbcInsert和类通过SimpleJdbcCall利用可以通过 JDBC 驱动程序检索的数据库元数据来提供简化的配置。这意味着您无需预先配置,但如果您希望在代码中提供所有详细信息,您可以覆盖或关闭元数据处理。

3.6.1. 通过使用插入数据SimpleJdbcInsert

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

java
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
}
科特林
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方法将plainjava.util.Map作为其唯一参数。这里要注意的重要一点是,用于 的键Map必须与数据库中定义的表的列名匹配。这是因为我们读取元数据来构造实际的插入语句。

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

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

java
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
}
科特林
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
}

使用第二种方法运行插入时的主要区别在于您不将 添加idMap,而是调用该executeAndReturnKey方法。这将返回一个 java.lang.Number对象,您可以使用该对象创建域类中使用的数值类型的实例。您不能依赖所有数据库在此处返回特定的 Java 类。java.lang.Number是您可以依赖的基类。如果您有多个自动生成的列或生成的值是非数字的,则可以使用KeyHolderexecuteAndReturnKeyHolder方法返回的 a。

3.6.3. 指定列SimpleJdbcInsert

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

java
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
}
科特林
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
}

插入的执行与您依赖元数据来确定要使用的列一样。

3.6.4. 用于SqlParameterSource提供参数值

使用 aMap提供参数值可以正常工作,但它不是最方便使用的类。Spring 提供了几个SqlParameterSource 你可以使用的接口实现。第一个是BeanPropertySqlParameterSource,如果您有一个包含您的值的符合 JavaBean 的类,这是一个非常方便的类。它使用相应的 getter 方法来提取参数值。下面的例子展示了如何使用BeanPropertySqlParameterSource

java
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
}
科特林
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类似于 aMap但提供更方便的addValue方法,可以链接。以下示例显示了如何使用它:

java
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
}
科特林
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使用数据库中的元数据来查找名称inout参数,因此您不必显式声明它们。如果您愿意这样做,或者如果您有没有自动映射到 Java 类的参数(例如ARRAY 或),您可以声明参数。STRUCT第一个示例显示了一个简单的过程,它只返回 MySQL 数据库中的标量值VARCHARDATE格式。示例过程读取指定的参与者条目并 以参数的形式返回first_namelast_namebirth_date列。out以下清单显示了第一个示例:

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参数包含id您正在查找的演员的。out 参数返回从表中读取的数据。

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

java
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
}
科特林
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
}

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

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

execute方法的最后一部分创建一个Actor实例,用于返回检索到的数据。同样,重要的是使用out在存储过程中声明的参数名称。此外,out 结果映射中存储的参数名称的大小写out与数据库中参数名称的大小写相匹配,这可能因数据库而异。为了使您的代码更具可移植性,您应该进行不区分大小写的查找或指示 Spring 使用LinkedCaseInsensitiveMap. 要执行后者,您可以创建自己的JdbcTemplate并将setResultsMapCaseInsensitive 属性设置为true. 然后,您可以将此自定义JdbcTemplate实例传递给SimpleJdbcCall. 以下示例显示了此配置:

java
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
}
科特林
class JdbcActorDao(dataSource: DataSource) : ActorDao {

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

    // ... additional methods
}

通过执行此操作,您可以避免用于返回out参数名称的大小写冲突。

3.6.6。显式声明要用于 a 的参数SimpleJdbcCall

在本章前面,我们描述了如何从元数据中推导出参数,但如果您愿意,可以显式声明它们。您可以通过创建和配置方法来做到这一点SimpleJdbcCall,该declareParameters方法将可变数量的SqlParameter对象作为输入。有关如何定义SqlParameter.

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

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

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

java
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
}
科特林
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 操作建模为 Java 对象中介绍)定义一个参数,您可以使用SqlParameter它的子类之一。为此,您通常在构造函数中指定参数名称和 SQL 类型。SQL 类型是使用java.sql.Types常量指定的。在本章前面,我们看到了类似于以下的声明:

java
new SqlParameter("in_id", Types.NUMERIC),
new SqlOutParameter("out_first_name", Types.VARCHAR),
科特林
SqlParameter("in_id", Types.NUMERIC),
SqlOutParameter("out_first_name", Types.VARCHAR),

第一行SqlParameter声明了一个 IN 参数。您可以将 IN 参数用于存储过程调用和查询,方法是使用SqlQuery及其子类(在了解SqlQuery中介绍)。

第二行(带有SqlOutParameter)声明out要在存储过程调用中使用的参数。还有一个SqlInOutParameterforInOut参数(为过程提供 IN 值并返回值的参数)。

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

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

3.6.8。通过使用调用存储函数SimpleJdbcCall

您可以以与调用存储过程几乎相同的方式调用存储函数,不同之处在于您提供的是函数名称而不是过程名称。您使用该 withFunctionName方法作为配置的一部分来指示您要调用函数,并生成函数调用的相应字符串。一个专门的调用 ( executeFunction) 用于运行该函数,它将函数返回值作为指定类型的对象返回,这意味着您不必从结果映射中检索返回值。类似的便捷方法(名为executeObject)也可用于只有一个out 参数的存储过程。以下示例(对于 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,如下例所示:

java
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
}
科特林
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包含来自函数调用的返回值的 a。

3.6.9。从 a返回 aResultSet或 REF 游标SimpleJdbcCall

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

下一个示例(对于 MySQL)使用不带 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. 因为要映射到的类遵循 JavaBean 规则,所以您可以使用通过在方法BeanPropertyRowMapper中传递要映射到的所需类来创建的类newInstance。以下示例显示了如何执行此操作:

java
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
}
科特林
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 操作建模为 Java 对象

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

许多 Spring 开发人员认为,下面描述的各种 RDBMS 操作类(类除外)通常可以直接调用StoredProcedure替换。JdbcTemplate通常,编写直接调用方法的 DAO 方法更简单JdbcTemplate(与将查询封装为成熟的类相反)。

但是,如果您从使用 RDBMS 操作类中获得了可衡量的价值,您应该继续使用这些类。

3.7.1. 理解SqlQuery

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

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;
    }
}
科特林
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。此客户查询的构造函数将 aDataSource作为唯一参数。在此构造函数中,您可以使用DataSource和应该运行的 SQL 调用超类的构造函数来检索此查询的行。此 SQL 用于创建PreparedStatement,因此它可能包含在执行期间要传入的任何参数的占位符。declareParameter 您必须使用传入的方法来声明每个参数SqlParameter。采用SqlParameter名称和 .xml 中定义的 JDBC 类型java.sql.Types。定义完所有参数后,就可以调用 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);
}
科特林
private val actorMappingQuery = ActorMappingQuery(dataSource)

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

前面示例中的方法使用id作为唯一参数传入的客户检索客户。由于我们只希望返回一个对象,因此我们以为参数调用findObject便捷方法。id如果我们有一个返回对象列表并采用附加参数的查询,我们将使用其中一种execute 方法,该方法采用作为可变参数传入的参数值数组。以下示例显示了这种方法:

java
public List<Actor> searchForActors(int age, String namePattern) {
    List<Actor> actors = actorSearchMappingQuery.execute(age, namePattern);
    return actors;
}
科特林
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);
    }
}
科特林
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

该类StoredProcedureabstractRDBMS 存储过程的对象抽象的超类。

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

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

java
new SqlParameter("in_id", Types.NUMERIC),
new SqlOutParameter("out_first_name", Types.VARCHAR),
科特林
SqlParameter("in_id", Types.NUMERIC),
SqlOutParameter("out_first_name", Types.VARCHAR),

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

第一行(带有SqlParameter)声明了一个 IN 参数。您可以将 IN 参数用于存储过程调用和使用SqlQuery及其子类的查询(在理解SqlQuery中介绍)。

第二行(带有SqlOutParameter)声明out要在存储过程调用中使用的参数。还有一个SqlInOutParameterforInOut参数(为in过程提供值并返回值的参数)。

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

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

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;
        }
    }

}
科特林
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
        }
    }
}

以下示例 aStoredProcedure有两个输出参数(在本例中为 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>());
    }
}
科特林
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>())
    }
}

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

该类TitleMapper将 a 映射ResultSetTitle所提供的每一行的域对象ResultSet,如下所示:

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;
    }
}
科特林
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"))
}

该类GenreMapper将 a 映射ResultSetGenre所提供的每一行的域对象ResultSet,如下所示:

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"));
    }
}
科特林
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);
    }
}
科特林
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该数组用于通过使用类中的常量值来指示相应参数的 SQL 类型java.sql.Types。为每个参数提供一个条目。

  • 您可以使用SqlParameterValue该类来包装需要此附加信息的参数值。为此,请为每个值创建一个新实例,并在构造函数中传入 SQL 类型和参数值。您还可以为数值提供可选的比例参数。

  • 对于使用命名参数的方法,您可以使用SqlParameterSourceBeanPropertySqlParameterSourceMapSqlParameterSource. 它们都有为任何命名参数值注册 SQL 类型的方法。

3.8.2. 处理 BLOB 和 CLOB 对象

您可以在数据库中存储图像、其他二进制数据和大块文本。这些大对象被称为二进制数据的 BLOB(二进制大对象)和字符数据的 CLOB(字符大对象)。JdbcTemplate在 Spring 中,您可以直接使用 RDBMS 对象和SimpleJdbc类提供的更高抽象层来处理这些大对象。所有这些方法都使用LobHandler接口的实现来实际管理 LOB(大对象)数据。 LobHandler通过该方法提供对LobCreator类的访问,该getLobCreator方法用于创建要插入的新 LOB 对象。

LobCreatorLobHandler为 LOB 输入和输出提供以下支持:

  • 斑点

    • byte[]:getBlobAsBytessetBlobAsBytes

    • InputStream:getBlobAsBinaryStreamsetBlobAsBinaryStream

  • CLOB

    • String:getClobAsStringsetClobAsString

    • InputStream:getClobAsAsciiStreamsetClobAsAsciiStream

    • Reader:getClobAsCharacterStreamsetClobAsCharacterStream

下一个示例显示如何创建和插入 BLOB。稍后我们将展示如何从数据库中读回它。

此示例使用JdbcTemplate和 的实现 AbstractLobCreatingPreparedStatementCallback。它实现了一种方法, setValues. 此方法提供了一个LobCreator我们用来在 SQL 插入语句中设置 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 的内容。
科特林
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 的内容。

如果对返回的 from 调用setBlobAsBinaryStreamsetClobAsAsciiStreamsetClobAsCharacterStream方法,则可以选择为参数指定负值 。如果指定的内容长度为负数,则 使用不带长度参数的 set-stream 方法的 JDBC 4.0 变体。否则,它将指定的长度传递给驱动程序。LobCreatorDefaultLobHandler.getLobCreator()contentLengthDefaultLobHandler

请参阅您使用的 JDBC 驱动程序的文档,以验证它是否支持流式传输 LOB 而无需提供内容长度。

现在是时候从数据库中读取 LOB 数据了。同样,您将 aJdbcTemplate 与相同的实例变量lobHandler和对 a 的引用一起使用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 的内容。
科特林
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 字符串。NamedParameterJdbcTemplate和中提供的命名参数支持JdbcTemplate采用后一种方法。您可以将值作为java.util.List原始对象传入。此列表用于在语句执行期间插入所需的占位符并传入值。

传入许多值时要小心。JDBC 标准不保证in表达式列表可以使用超过 100 个值。各种数据库都超过了这个数字,但它们通常对允许的值数量有硬性限制。例如,Oracle 的限制是 1000。

除了值列表中的原始值之外,您还可以创建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。以下示例显示返回STRUCT用户声明类型的 Oracle 对象的值ITEM_TYPE

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;
            }));
        // ...
    }
科特林
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;
    }
};
科特林
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)))
    }
}

您现在可以将其添加SqlTypeValueMap包含用于 execute调用存储过程的输入参数的 。

的另一个用途SqlTypeValue是将一组值传递给 Oracle 存储过程。Oracle 有自己的内部ARRAY类,必须在这种情况下使用,您可以使用SqlTypeValue来创建 Oracle 的实例ARRAY并使用 Java 中的值填充它ARRAY,如以下示例所示:

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;
    }
};
科特林
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 数据库引擎的支持。本机提供对HSQLH2Derby的支持。您还可以使用可扩展的 API 来插入新的嵌入式数据库类型和 DataSource实现。

3.9.1。为什么使用嵌入式数据库?

由于其轻量级的特性,嵌入式数据库在项目的开发阶段非常有用。好处包括易于配置、快速启动时间、可测试性以及在开发过程中快速发展 SQL 的能力。

3.9.2. 使用 Spring XML 创建嵌入式数据库

如果要将嵌入式数据库实例公开为 Spring 中的 bean ApplicationContext,则可以使用命名空间embedded-database中的标记spring-jdbc

<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 数据库,该数据库使用来自类路径根目录中的schema.sql和资源的 SQL 进行填充。test-data.sql此外,作为最佳实践,嵌入式数据库被分配一个唯一生成的名称。嵌入式数据库作为 bean 类型提供给 Spring 容器, javax.sql.DataSource然后可以根据需要注入到数据访问对象中。

3.9.3. 以编程方式创建嵌入式数据库

该类EmbeddedDatabaseBuilder提供了一个流畅的 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()
科特林
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 。EmbeddedDatabaseBuilder

您还可以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();
    }
}
科特林
@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 支持的三个嵌入式数据库之一。它包括以下主题:

使用 HSQL

Spring 支持 HSQL 1.8.0 及更高版本。如果没有明确指定类型,HSQL 是默认的嵌入式数据库。要显式指定 HSQL,请将标记的type属性 设置为. 如果您使用构建器 API,请 使用.embedded-databaseHSQLsetType(EmbeddedDatabaseType)EmbeddedDatabaseType.HSQL

使用 H2

Spring 支持 H2 数据库。要启用 H2,请将标签的type属性 设置为。如果您使用构建器 API,请 使用.embedded-databaseH2setType(EmbeddedDatabaseType)EmbeddedDatabaseType.H2

使用德比

Spring 支持 Apache Derby 10.5 及更高版本。要启用 Derby,请将标记的type 属性设置为。如果您使用构建器 API,请使用.embedded-databaseDERBYsetType(EmbeddedDatabaseType)EmbeddedDatabaseType.DERBY

3.9.5。使用嵌入式数据库测试数据访问逻辑

嵌入式数据库提供了一种测试数据访问代码的轻量级方法。下一个示例是使用嵌入式数据库的数据访问集成测试模板。当嵌入式数据库不需要跨测试类重用时,一次性使用这样的模板可能很有用。但是,如果您希望创建在测试套件中共享的嵌入式数据库,请考虑使用Spring TestContext Framework并将嵌入式数据库配置为 Spring 中的 bean,ApplicationContext使用 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();
    }

}
科特林
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。为嵌入式数据库生成唯一名称

如果他们的测试套件无意中尝试重新创建同一数据库的其他实例,开发团队经常会遇到嵌入式数据库错误。@Configuration如果一个 XML 配置文件或类负责创建一个嵌入式数据库,然后在同一个测试套件(即同一个 JVM 进程内)的多个测试场景中重用相应的配置,这很容易发生——例如,集成针对嵌入式数据库的测试,其 ApplicationContext配置仅在哪些 bean 定义配置文件处于活动状态时有所不同。

此类错误的根本原因是 Spring EmbeddedDatabaseFactory(由<jdbc:embedded-database>XML 命名空间元素和 EmbeddedDatabaseBuilderfor Java 配置在内部使用)将嵌入式数据库的名称设置为( testdb如果没有另外指定)。对于 的情况<jdbc:embedded-database>,嵌入式数据库通常被分配一个与 bean 相同的名称id(通常类似于dataSource)。因此,后续创建嵌入式数据库的尝试不会产生新的数据库。相反,相同的 JDBC 连接 URL 被重用,并且尝试创建新的嵌入式数据库实际上指向从相同配置创建的现有嵌入式数据库。

为了解决这个常见问题,Spring Framework 4.2 支持为嵌入式数据库生成唯一名称。要启用生成的名称,请使用以下选项之一。

  • EmbeddedDatabaseFactory.setGenerateUniqueDatabaseName()

  • EmbeddedDatabaseBuilder.generateUniqueName()

  • <jdbc:embedded-database generate-name="true" …​ >

3.9.7。扩展嵌入式数据库支持

您可以通过两种方式扩展 Spring JDBC 嵌入式数据库支持:

  • 实施EmbeddedDatabaseConfigurer以支持新的嵌入式数据库类型。

  • 实施DataSourceFactory以支持新DataSource实施,例如用于管理嵌入式数据库连接的连接池。

我们鼓励您在GitHub 问题上为 Spring 社区贡献扩展 。

3.10。初始化一个DataSource

org.springframework.jdbc.datasource.init包提供对初始化现有DataSource. DataSource嵌入式数据库支持为创建和初始化应用程序提供了一个选项。但是,您有时可能需要初始化在某处服务器上运行的实例。

3.10.1。使用 Spring XML 初始化数据库

如果要初始化数据库并且可以提供对DataSource bean 的引用,则可以使用命名空间initialize-database中的标记spring-jdbc

<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 enabled从名为 的系统属性中获取 的值INITIALIZE_DATABASE

控制现有数据发生的情况的第二个选项是更能容忍故障。为此,您可以控制初始化程序忽略它从脚本运行的 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实例并运行在其初始化回调中提供的脚本(类似于init-methodXML bean 定义中的 an、@PostConstruct组件中的方法或afterPropertiesSet() 实现InitializingBean. 如果其他 bean 依赖于同一个数据源并在初始化回调中使用该数据源,则可能会出现问题,因为数据尚未初始化。一个常见的例子是缓存,它会在应用程序启动时急切地初始化并从数据库中加载数据。

要解决此问题,您有两个选择:将缓存初始化策略更改为稍后阶段或确保首先初始化数据库初始化程序。

如果应用程序在您的控制范围内而不是其他情况下,更改您的缓存初始化策略可能很容易。关于如何实现这一点的一些建议包括:

  • 使缓存在首次使用时延迟初始化,从而提高应用程序启动时间。

  • 让您的缓存或初始化缓存的单独组件实现 LifecycleSmartLifecycle. 当应用程序上下文启动时,您可以SmartLifecycle通过设置其autoStartup标志来自动启动 a,也可以Lifecycle通过调用ConfigurableApplicationContext.start() 封闭上下文手动启动 a。

  • 使用 SpringApplicationEvent或类似的自定义观察者机制来触发缓存初始化。ContextRefreshedEvent总是在准备好使用时由上下文发布(在所有 bean 都已初始化之后),因此这通常是一个有用的钩子(这是SmartLifecycle默认情况下的工作方式)。

确保首先初始化数据库初始化程序也很容易。关于如何实现这一点的一些建议包括:

  • 依赖 Spring 的默认行为,BeanFactory即 bean 按注册顺序进行初始化。您可以通过采用<import/>XML 配置中的一组元素的常见做法来轻松安排这一点,这些元素对您的应用程序模块进行排序,并确保首先列出数据库和数据库初始化。

  • DataSource将和使用它的业务组件分开,并通过将它们放在单独ApplicationContext的实例中来控制它们的启动顺序(例如,父上下文包含DataSource,子上下文包含业务组件)。这种结构在 Spring Web 应用程序中很常见,但可以更普遍地应用。

4. 使用 R2DBC 进行数据访问

R2DBC(“反应式关系数据库连接”)是一个社区驱动的规范工作,旨在使用反应式模式标准化对 SQL 数据库的访问。

4.1。包层次结构

Spring Framework 的 R2DBC 抽象框架由两个不同的包组成:

4.2. 使用 R2DBC 核心类来控制基本的 R2DBC 处理和错误处理

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

4.2.1。使用DatabaseClient

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

  • 运行 SQL 查询

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

  • Result对实例执行迭代

  • 捕获 R2DBC 异常并将它们转换为org.springframework.dao包中定义的通用、信息更丰富的异常层次结构。(请参阅一致的异常层次结构。)

客户端具有功能性、流畅的 API,使用反应类型进行声明性组合。

当您将DatabaseClient用于您的代码时,您只需要实现 java.util.function接口,为它们提供明确定义的合同。给定类Connection提供的a , 回调创建一个. 对于提取结果的映射函数也是如此。DatabaseClientFunctionPublisherRow

您可以DatabaseClient通过引用直接实例化在 DAO 实现中使用ConnectionFactory,也可以在 Spring IoC 容器中配置它并将其作为 bean 引用提供给 DAO。

创建DatabaseClient对象最简单的方法是通过静态工厂方法,如下:

java
DatabaseClient client = DatabaseClient.create(connectionFactory);
科特林
val client = DatabaseClient.create(connectionFactory)
应始终将其ConnectionFactory配置为 Spring IoC 容器中的 bean。

上述方法DatabaseClient使用默认设置创建一个。

您也可以BuilderDatabaseClient.builder(). 您可以通过调用以下方法自定义客户端:

  • ….bindMarkers(…):提供一个特定BindMarkersFactory的配置命名参数到数据库绑定标记翻译。

  • ….executeFunction(…):设置对象ExecuteFunction如何Statement运行。

  • ….namedParameters(false): 禁用命名参数扩展。默认启用。

方言由BindMarkersFactoryResolver from a解决ConnectionFactory,通常通过检查来解决ConnectionFactoryMetadata
您可以通过注册一个实现的类来让 Spring 自动发现 您BindMarkersFactory的. 使用 Spring 的. org.springframework.r2dbc.core.binding.BindMarkersFactoryResolver$BindMarkerFactoryProviderMETA-INF/spring.factoriesBindMarkersFactoryResolverSpringFactoriesLoader

目前支持的数据库有:

  • H2

  • 玛丽亚数据库

  • 微软 SQL 服务器

  • MySQL

  • Postgres

此类发出的所有 SQL 都记录在DEBUG与客户端实例的完全限定类名对应的类别下的级别(通常为 DefaultDatabaseClient)。此外,每次执行都会在反应序列中注册一个检查点以帮助调试。

以下部分提供了一些DatabaseClient使用示例。这些示例并不是DatabaseClient. 请参阅相关的javadoc

执行语句

DatabaseClient提供运行语句的基本功能。以下示例显示了创建新表的最少但功能齐全的代码需要包含的内容:

java
Mono<Void> completion = client.sql("CREATE TABLE person (id VARCHAR(255) PRIMARY KEY, name VARCHAR(255), age INTEGER);")
        .then();
科特林
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();
科特林
val first = client.sql("SELECT id, name FROM person")
        .fetch().awaitSingle()

以下查询使用绑定变量:

java
Mono<Map<String, Object>> first = client.sql("SELECT id, name FROM person WHERE first_name = :fn")
        .bind("fn", "Joe")
        .fetch().first();
科特林
val first = client.sql("SELECT id, name FROM person WHERE WHERE first_name = :fn")
        .bind("fn", "Joe")
        .fetch().awaitSingle()

您可能已经注意到fetch()上面示例中的使用。fetch()是一个延续运算符,可让您指定要使用的数据量。

调用first()从结果中返回第一行并丢弃剩余的行。您可以使用以下运算符使用数据:

  • first()返回整个结果的第一行。它的 Kotlin Coroutine 变体以awaitSingle()不可为空的返回值命名,awaitSingleOrNull() 如果该值是可选的。

  • one()只返回一个结果,如果结果包含更多行则失败。使用 Kotlin 协程,只awaitOne()针对一个值,或者awaitOneOrNull() 如果该值可能是null.

  • all()返回结果的所有行。使用 Kotlin 协程时,请使用flow().

  • rowsUpdated()返回受影响的行数(//INSERT计数 )。它的 Kotlin Coroutine 变体被命名为.UPDATEDELETEawaitRowsUpdated()

在不指定进一步的映射详细信息的情况下,查询返回表格结果,因为Map其键是映射到其列值的不区分大小写的列名。

Function<Row, T>您可以通过提供一个为每个都调用的方法来控制结果映射,Row以便它可以返回任意值(奇异值、集合和映射以及对象)。

以下示例提取name列并发出其值:

java
Flux<String> names = client.sql("SELECT name FROM person")
        .map(row -> row.get("name", String.class))
        .all();
科特林
val names = client.sql("SELECT name FROM person")
        .map{ row: Row -> row.get("name", String.class) }
        .flow()
怎么样null

关系数据库结果可以包含null值。Reactive Streams 规范禁止发射null值。该要求要求正确null处理提取器功能。虽然您可以null从 a 获取值Row,但不能发出null 值。您必须将任何值包装null在对象中(例如,Optional 对于奇异值),以确保null提取器函数永远不会直接返回值。

更新 ( INSERT, UPDATE, 和DELETE)DatabaseClient

修改语句的唯一区别是这些语句通常不返回表格数据,因此您可以使用这些数据rowsUpdated()来使用结果。

以下示例显示了一个UPDATE返回更新行数的语句:

java
Mono<Integer> affectedRows = client.sql("UPDATE person SET first_name = :fn")
        .bind("fn", "Joe")
        .fetch().rowsUpdated();
科特林
val affectedRows = client.sql("UPDATE person SET first_name = :fn")
        .bind("fn", "Joe")
        .fetch().awaitRowsUpdated()
将值绑定到查询

一个典型的应用程序需要参数化的 SQL 语句来根据一些输入选择或更新行。这些通常是SELECT受子句约束的语句WHERE或接受输入参数的语句。如果参数未正确转义,则参数化语句将承担 SQL 注入的风险。利用 R2DBC 的 API 消除查询参数的 SQL 注入风险。您可以提供带有运算符的参数化 SQL 语句并将参数绑定到实际的. 然后,您的 R2DBC 驱动程序通过使用准备好的语句和参数替换来运行该语句。INSERTUPDATEDatabaseClientbindexecute(…)Statement

参数绑定支持两种绑定策略:

  • 按索引,使用从零开始的参数索引。

  • 按名称,使用占位符名称。

以下示例显示了查询的参数绑定:

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))

上述查询可以参数化并按如下方式运行:

java
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);
科特林
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谓词的更简单变体:

java
client.sql("SELECT id, name, state FROM table WHERE age IN (:ages)")
    .bind("ages", Arrays.asList(35, 50));
科特林
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 本身不支持 Collection-like 值。然而,扩展List上面示例中给定的 a 对 Spring 的 R2DBC 支持中的命名参数有效,例如,用于IN如上所示的子句中。但是,插入或更新数组类型的列(例如在 Postgres 中)需要底层 R2DBC 驱动程序支持的数组类型:通常是 Java 数组,例如String[]更新text[]列。不要将Collection<String>之类的东西作为数组参数传递。
语句过滤器

Statement 有时,您需要在实际运行之前对选项进行微调。注册一个Statement过滤器 ( StatementFilterFunction) 通过DatabaseClient来拦截和修改执行中的语句,如以下示例所示:

java
client.sql("INSERT INTO table (name, state) VALUES(:name, :state)")
    .filter((s, next) -> next.execute(s.returnGeneratedValues("id")))
    .bind("name", …)
    .bind("state", …);
科特林
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>

java
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));
科特林
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是线程安全的。这很重要,因为这意味着您可以配置 a 的单个实例,DatabaseClient 然后安全地将这个共享引用注入到多个 DAO(或存储库)中。是有状态的DatabaseClient,因为它维护对 a 的引用ConnectionFactory,但这种状态不是会话状态。

使用DatabaseClient该类时的常见做法是ConnectionFactory 在 Spring 配置文件中配置一个,然后将该共享ConnectionFactorybean 依赖注入到 DAO 类中。DatabaseClient是在 setter 中为ConnectionFactory. 这导致 DAO 类似于以下内容:

java
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...
}
科特林
class R2dbcCorporateEventDao(connectionFactory: ConnectionFactory) : CorporateEventDao {

    private val databaseClient = DatabaseClient.create(connectionFactory)

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

显式配置的替代方法是使用组件扫描和注释支持来进行依赖注入。在这种情况下,您可以使用 注释该类@Component (这使其成为组件扫描的候选对象)并使用注释ConnectionFactorysetter 方法@Autowired。以下示例显示了如何执行此操作:

java
@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 用注释ConnectionFactorysetter 方法@Autowired
3 DatabaseClientConnectionFactory. _
科特林
@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 DatabaseClientConnectionFactory. _

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

4.3. 检索自动生成的密钥

INSERT将行插入到定义自动增量或标识列的表中时,语句可能会生成键。要完全控制要生成的列名,只需注册一个StatementFilterFunction请求生成所需列的键的 a。

java
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
科特林
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. 控制数据库连接

本节涵盖:

4.4.1。使用ConnectionFactory

Spring 通过ConnectionFactory. AConnectionFactory是 R2DBC 规范的一部分,是驱动程序的公共入口点。它允许容器或框架从应用程序代码中隐藏连接池和事务管理问题。作为开发人员,您无需了解有关如何连接到数据库的详细信息。这是设置ConnectionFactory. 在开发和测试代码时,您很可能同时担任这两个角色,但您不必知道生产数据源的配置方式。

当您使用 Spring 的 R2DBC 层时,您可以使用第三方提供的连接池实现来配置自己的。一个流行的实现是 R2DBC Pool ( r2dbc-pool)。Spring 发行版中的实现仅用于测试目的,不提供池。

配置一个ConnectionFactory

  1. 获取与ConnectionFactory您通常获取 R2DBC的连接ConnectionFactory

  2. 提供 R2DBC URL(有关正确值,请参阅驱动程序的文档)。

以下示例显示了如何配置ConnectionFactory

java
ConnectionFactory factory = ConnectionFactories.get("r2dbc:h2:mem:///test?options=DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE");
科特林
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 驱动程序允许这样使用,则可以用于特定要求,例如流水线。与 pooled 相比ConnectionFactory,它始终重用相同的连接,避免过度创建物理连接。

4.4.4. 使用TransactionAwareConnectionFactoryProxy

TransactionAwareConnectionFactoryProxy是目标的代理ConnectionFactory。代理包装该目标ConnectionFactory以增加对 Spring 管理事务的认识。

如果您使用未与 Spring 的 R2DBC 支持集成的 R2DBC 客户端,则需要使用此类。在这种情况下,您仍然可以使用此客户端,同时让此客户端参与 Spring 托管事务。通常最好将具有适当访问权限的 R2DBC 客户端集成到ConnectionFactoryUtils 资源管理中。

有关更多详细信息,请参阅TransactionAwareConnectionFactoryProxy javadoc。

4.4.5。使用R2dbcTransactionManager

该类R2dbcTransactionManagerReactiveTransactionManager单个 R2DBC 数据源的实现。它将一个 R2DBC 连接从指定的连接工厂绑定到订阅者Context,可能允许每个连接工厂有一个订阅者连接。

需要应用程序代码来检索 R2DBC 连接 ConnectionFactoryUtils.getConnection(ConnectionFactory),而不是 R2DBC 的标准 ConnectionFactory.create()

所有框架类(例如DatabaseClient)都隐式使用此策略。如果不与此事务管理器一起使用,则查找策略的行为与常见策略完全相同。因此,它可以在任何情况下使用。

该类R2dbcTransactionManager支持应用于连接的自定义隔离级别。

5.对象关系映射(ORM)数据访问

本节介绍使用对象关系映射 (ORM) 时的数据访问。

5.1。ORM 与 Spring 简介

Spring Framework 支持与 Java Persistence API (JPA) 的集成,并支持本地 Hibernate 进行资源管理、数据访问对象 (DAO) 实现和事务策略。例如,对于 Hibernate,有一流的支持和几个方便的 IoC 特性,可以解决许多典型的 Hibernate 集成问题。您可以通过依赖注入为 OR(对象关系)映射工具配置所有支持的功能。它们可以参与 Spring 的资源和事务管理,并遵守 Spring 的通用事务和 DAO 异常层次结构。推荐的集成方式是针对普通的 Hibernate 或 JPA API 编写 DAO。

当您创建数据访问应用程序时,Spring 为您选择的 ORM 层添加了显着的增强功能。您可以根据需要利用尽可能多的集成支持,并且应该将这种集成工作与在内部构建类似基础架构的成本和风险进行比较。无论技术如何,您都可以像使用库一样使用大部分 ORM 支持,因为一切都被设计为一组可重用的 JavaBean。Spring IoC 容器中的 ORM 有助于配置和部署。因此,本节中的大多数示例都显示了 Spring 容器内的配置。

使用 Spring 框架创建 ORM DAO 的好处包括:

  • 更容易测试。Spring 的 IoC 方法可以轻松交换 HibernateSessionFactory实例、JDBCDataSource 实例、事务管理器和映射对象实现(如果需要)的实现和配置位置。这反过来又使得单独测试每段与持久性相关的代码变得更加容易。

  • 常见的数据访问异常。Spring 可以包装 ORM 工具中的异常,将它们从专有(可能检查的)异常转换为公共运行时 DataAccessException层次结构。此功能使您可以处理大多数不可恢复的持久性异常,仅在适当的层中,而无需烦人的样板捕获、抛出和异常声明。您仍然可以根据需要捕获和处理异常。请记住,JDBC 异常(包括特定于 DB 的方言)也被转换为相同的层次结构,这意味着您可以在一致的编程模型中使用 JDBC 执行一些操作。

  • 通用资源管理。Spring 应用程序上下文可以处理 HibernateSessionFactory实例、JPAEntityManagerFactory 实例、JDBCDataSource实例和其他相关资源的位置和配置。这使得这些值易于管理和更改。Spring 提供了对持久性资源的高效、简单和安全的处理。例如,使用 Hibernate 的相关代码一般需要使用相同的 Hibernate Session,以确保效率和正确的事务处理。Spring通过 Hibernate暴露电流,可以轻松地Session透明地创建和绑定到当前线程。因此,对于任何本地或 JTA 事务环境,Spring 解决了典型 Hibernate 使用的许多长期问题。SessionSessionFactory

  • 综合交易管理。@Transactional您可以通过注释或通过在 XML 配置文件中显式配置事务 AOP 建议,使用声明性、面向方面编程 (AOP) 样式的方法拦截器来包装您的 ORM 代码 。在这两种情况下,都会为您处理事务语义和异常处理(回滚等)。如资源和事务管理中所述,你还可以交换各种事务管理器,而不影响你的 ORM 相关代码。例如,您可以在本地事务和 JTA 之间进行交换,在这两种情况下都可以使用相同的完整服务(例如声明性事务)。此外,与 JDBC 相关的代码可以在事务上与您用于执行 ORM 的代码完全集成。这对于不适合 ORM(例如批处理和 BLOB 流)但仍需要与 ORM 操作共享公共事务的数据访问非常有用。

如需更全面的 ORM 支持,包括对 MongoDB 等替代数据库技术的支持,您可能需要查看 Spring Data项目套件。如果您是 JPA 用户,https: //spring.io 中的Getting Started Accessing Data with JPA指南提供了很好的介绍。

5.2. 通用 ORM 集成注意事项

本节重点介绍适用于所有 ORM 技术的注意事项。Hibernate部分提供了更多详细信息,并在具体上下文中显示了这些功能和配置。

Spring 的 ORM 集成的主要目标是清晰的应用程序分层(使用任何数据访问和事务技术)和应用程序对象的松散耦合——不再依赖于数据访问或事务策略的业务服务,不再需要硬编码的资源查找,更难替换的单例,没有更多的自定义服务注册表。目标是采用一种简单且一致的方法来连接应用程序对象,使它们尽可能地可重用且不受容器依赖性的影响。所有单独的数据访问特性都可以单独使用,但可以很好地与 Spring 的应用程序上下文概念集成,提供基于 XML 的配置和不需要 Spring 感知的普通 JavaBean 实例的交叉引用。在典型的 Spring 应用程序中,许多重要的对象都是 JavaBean:

5.2.1。资源和事务管理

典型的业务应用程序充斥着重复的资源管理代码。许多项目试图发明自己的解决方案,有时为了编程方便而牺牲了对故障的正确处理。Spring 提倡适当的资源处理的简单解决方案,即在 JDBC 的情况下通过模板实现 IoC,并为 ORM 技术应用 AOP 拦截器。

基础设施提供适当的资源处理和将特定 API 异常适当转换为未经检查的基础设施异常层次结构。Spring 引入了 DAO 异常层次结构,适用于任何数据访问策略。对于直接 JDBC,上一节JdbcTemplate中提到的类提供连接处理和对 层次结构 的适当转换,包括将特定于数据库的 SQL 错误代码转换为有意义的异常类。对于 ORM 技术,请参阅 下一节了解如何获得相同的异常转换优势。SQLExceptionDataAccessException

在事务管理方面,JdbcTemplate该类通过各自的 Spring 事务管理器连接到 Spring 事务支持并支持 JTA 和 JDBC 事务。对于支持的 ORM 技术,Spring 通过 Hibernate 和 JPA 事务管理器以及 JTA 支持提供 Hibernate 和 JPA 支持。有关事务支持的详细信息,请参阅事务管理一章。

5.2.2. 异常翻译

当您在 DAO 中使用 Hibernate 或 JPA 时,您必须决定如何处理持久性技术的本机异常类。DAO 抛出一个HibernateException or的子类PersistenceException,具体取决于技术。这些异常都是运行时异常,不必声明或捕获。您可能还需要处理 IllegalArgumentExceptionIllegalStateException. 这意味着调用者只能将异常视为通常是致命的,除非他们希望依赖于持久性技术自己的异常结构。如果不将调用者绑定到实现策略,就不可能捕获特定原因(例如乐观锁定失败)。这种权衡对于基于 ORM 或不需要任何特殊异常处理(或两者)的应用程序可能是可以接受的。然而,Spring 允许通过@Repository注解透明地应用异常翻译。以下示例(一个用于 Java 配置,一个用于 XML 配置)显示了如何执行此操作:

java
@Repository
public class ProductDaoImpl implements ProductDao {

    // class body here...

}
科特林
@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接口的实现)并建议所有标有注释的 bean, @Repository以便发现的翻译器可以拦截并在抛出的异常上应用适当的翻译。

总之,您可以基于普通持久性技术的 API 和注释来实现 DAO,同时仍然受益于 Spring 管理的事务、依赖注入和透明的异常转换(如果需要)到 Spring 的自定义异常层次结构。

5.3. 休眠

我们首先介绍 Spring 环境中的Hibernate 5,使用它来演示 Spring 集成 OR 映射器的方法。本节详细介绍了许多问题,并展示了 DAO 实现和事务划分的不同变体。大多数这些模式都可以直接转换为所有其他受支持的 ORM 工具。本章后面的部分将介绍其他 ORM 技术并展示简短的示例。

从 Spring Framework 5.3 开始,Spring 需要 Hibernate ORM 5.2+ 用于 Spring HibernateJpaVendorAdapter以及原生 HibernateSessionFactory设置。对于新启动的应用程序,强烈建议使用 Hibernate ORM 5.4。要与 一起使用HibernateJpaVendorAdapter,Hibernate Search 需要升级到 5.11.6。

5.3.1。SessionFactory在 Spring 容器中设置

为了避免将应用程序对象绑定到硬编码的资源查找,您可以将资源(例如 JDBCDataSource或 Hibernate SessionFactory)定义为 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 切换BasicDataSource到位于 JNDI 的 DataSource(通常由应用程序服务器管理)只是配置问题,如以下示例所示:

<beans>
    <jee:jndi-lookup id="myDataSource" jndi-name="java:comp/env/jdbc/myds"/>
</beans>

您还可以访问 JNDI-located SessionFactory,使用 Spring 的 JndiObjectFactoryBean/<jee:jndi-lookup>来检索和公开它。但是,这在 EJB 上下文之外通常并不常见。

Spring 还提供了一个变体,与样式配置和编程设置(不涉及)LocalSessionFactoryBuilder无缝集成。@BeanFactoryBean

两者都LocalSessionFactoryBean支持LocalSessionFactoryBuilder后台引导,Hibernate 初始化与给定引导执行程序(例如SimpleAsyncTaskExecutor)上的应用程序引导线程并行运行。在 上LocalSessionFactoryBean,可通过该bootstrapExecutor 属性获得。在 programmaticLocalSessionFactoryBuilder上,有一个 buildSessionFactory采用引导执行程序参数的重载方法。

从 Spring Framework 5.1 开始,这样的原生 Hibernate 设置还可以 EntityManagerFactory在原生 Hibernate 访问旁边公开 JPA 以进行标准 JPA 交互。有关详细信息,请参阅JPA 的本机 Hibernate 设置

5.3.2. 基于普通 Hibernate API 实现 DAO

Hibernate 有一个称为上下文会话的功能,其中 Hibernate 自己管理Session每个事务的一个电流。这大致相当于 Spring 的Session每个事务同步一个 Hibernate。相应的 DAO 实现类似于以下示例,基于普通的 Hibernate API:

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();
    }
}
科特林
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在实例变量中。我们强烈推荐这种基于实例的设置,而不是static HibernateUtil来自 Hibernate 的 CaveatEmptor 示例应用程序的老式类。(一般来说, 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 throws plain HibernateException(这是未检查的,因此不必声明或捕获),这意味着调用者只能将异常视为通常是致命的——除非他们想依赖 Hibernate 自己的异常层次结构。如果不将调用者绑定到实现策略,就不可能捕获特定原因(例如乐观锁定失败)。对于高度基于 Hibernate、不需要任何特殊异常处理或两者兼而有之的应用程序,这种折衷可能是可以接受的。

幸运的是,SpringLocalSessionFactoryBean支持任何 Spring 事务策略的 Hibernate SessionFactory.getCurrentSession()方法,返回当前 Spring 管理的 transactional Session,即使使用 HibernateTransactionManager. 该方法的标准行为仍然是返回Session与正在进行的 JTA 事务相关联的电流(如果有的话)。无论您使用 Spring JtaTransactionManager、EJB 容器管理事务 (CMT) 还是 JTA,此行为都适用。

总之,您可以基于普通的 Hibernate API 实现 DAO,同时仍然能够参与 Spring 管理的事务。

5.3.3. 声明式事务划分

我们建议您使用 Spring 的声明式事务支持,它允许您将 Java 代码中的显式事务划分 API 调用替换为 AOP 事务拦截器。您可以使用 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();
    }
}
科特林
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...
            }
        });
    }
}
科特林
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...
        }
    }
}

SpringTransactionInterceptor允许使用回调代码抛出任何已检查的应用程序异常,而TransactionTemplate仅限于回调中未检查的异常。TransactionTemplate在未经检查的应用程序异常或事务被应用程序标记为仅回滚的情况下触发回滚(通过设置TransactionStatus)。默认情况下,TransactionInterceptor 行为方式相同,但允许每个方法可配置的回滚策略。

5.3.5。交易管理策略

两者都TransactionTemplateTransactionInterceptor实际事务处理委托给 Hibernate 应用程序的PlatformTransactionManager实例(可以是 HibernateTransactionManager(对于单个 Hibernate SessionFactory),通过使用 ThreadLocal Session引擎盖)或JtaTransactionManager(委托给容器的 JTA 子系统)。您甚至可以使用自定义 PlatformTransactionManager实现。从本地 Hibernate 事务管理切换到 JTA(例如,当您的应用程序的某些部署面临分布式事务需求时)只是配置问题。您可以用 Spring 的 JTA 事务实现替换 Hibernate 事务管理器。事务划分和数据访问代码都无需更改即可工作,因为它们使用通用事务管理 API。

对于跨多个 Hibernate 会话工厂的分布式事务,您可以将其组合 JtaTransactionManager为具有多个 LocalSessionFactoryBean定义的事务策略。然后,每个 DAO 都会将一个特定SessionFactory 的引用传递给其相应的 bean 属性。如果所有底层 JDBC 数据源都是事务容器数据源,则业务服务可以在任意数量的 DAO 和任意数量的会话工厂之间划分事务而无需特别注意,只要它JtaTransactionManager用作策略即可。

两者都HibernateTransactionManager允许JtaTransactionManager使用 Hibernate 进行适当的 JVM 级缓存处理,无需容器特定的事务管理器查找或 JCA 连接器(如果您不使用 EJB 来启动事务)。

HibernateTransactionManager可以将 Hibernate JDBC 导出Connection为特定DataSource. 只要您只访问一个数据库,这种能力就可以在完全没有 JTA 的情况下通过混合 Hibernate 和 JDBC 数据访问实现高级事务划分。如果你已经通过类的属性 HibernateTransactionManager设置了传入的传入,则自动将 Hibernate 事务公开为 JDBC SessionFactory事务。或者,您可以通过类的属性明确指定 应该公开哪些事务 。DataSourcedataSourceLocalSessionFactoryBeanDataSourcedataSourceHibernateTransactionManager

5.3.6。比较容器管理的资源和本地定义的资源

您可以在容器管理的 JNDI 和本地定义的 JNDI 之间切换,SessionFactory而无需更改一行应用程序代码。将资源定义保留在容器中还是本地应用程序中主要取决于您使用的事务策略。与 Spring 定义的 local 相比 SessionFactory,手动注册的 JNDISessionFactory没有任何好处。部署一个SessionFactory通过 Hibernate 的 JCA 连接器提供了参与 Java EE 服务器管理基础设施的附加价值,但除此之外并没有增加实际价值。

Spring 的事务支持不绑定到容器。当配置了除 JTA 之外的任何策略时,事务支持也可以在独立或测试环境中工作。尤其是在单库事务的典型案例中,Spring 的单资源本地事务支持是 JTA 的轻量级和强大的替代方案。当您使用本地 EJB 无状态会话 bean 来驱动事务时,您既依赖于 EJB 容器,也依赖于 JTA,即使您只访问一个数据库并且只使用无状态会话 bean 通过容器管理的事务来提供声明性事务。以编程方式直接使用 JTA 还需要 Java EE 环境。JTA 不仅仅涉及 JTA 本身和 JNDI 方面的容器依赖关系DataSource实例。对于非 Spring、JTA 驱动的 Hibernate 事务,您必须使用 Hibernate JCA 连接器或额外的 Hibernate 事务代码,并 TransactionManagerLookup配置为正确的 JVM 级缓存。

Spring 驱动的事务可以与本地定义的 Hibernate 一起工作, SessionFactory就像它们与本地 JDBC 一样DataSource,前提是他们访问单个数据库。因此,当您有分布式事务需求时,您只需要使用 Spring 的 JTA 事务策略。JCA 连接器需要特定于容器的部署步骤,并且(显然)首先需要 JCA 支持。与使用本地资源定义和 Spring 驱动的事务部署简单的 Web 应用程序相比,此配置需要更多的工作。此外,如果您使用例如不提供 JCA 的 WebLogic Express,您通常需要容器的企业版。具有跨单个数据库的本地资源和事务的 Spring 应用程序可以在任何 Java EE Web 容器(没有 JTA、JCA 或 EJB)中运行,例如 Tomcat、Resin 甚至是普通的 Jetty。此外,您可以轻松地在桌面应用程序或测试套件中重用这样的中间层。

考虑到所有因素,如果您不使用 EJB,请坚持使用本地SessionFactory设置和 Spring 的HibernateTransactionManagerJtaTransactionManager. 您可以获得所有好处,包括适当的事务性 JVM 级缓存和分布式事务,而不会带来容器部署的不便。通过 JCA 连接器对 Hibernate 的 JNDI 注册SessionFactory只有在与 EJB 结合使用时才会增加价值。

5.3.7. 带有 Hibernate 的虚假应用程序服务器警告

在一些具有非常严格XADataSource实现的 JTA 环境中(目前是一些 WebLogic Server 和 WebSphere 版本),当配置 Hibernate 而不考虑该环境的 JTA 事务管理器时,虚假警告或异常可能会出现在应用程序服务器日志中。这些警告或异常表明正在访问的连接不再有效或 JDBC 访问不再有效,可能是因为事务不再处于活动状态。例如,下面是 WebLogic 的一个实际例外:

java.sql.SQLException:事务不再处于活动状态 - 状态:“已提交”。不
此事务中允许进一步的 JDBC 访问。

另一个常见问题是 JTA 事务之后的连接泄漏,Hibernate 会话(以及潜在的底层 JDBC 连接)没有正确关闭。

您可以通过让 Hibernate 了解 JTA 事务管理器来解决此类问题,它会同步到该事务管理器(与 Spring 一起)。您有两种选择:

  • 将您的 Spring JtaTransactionManagerbean 传递给您的 Hibernate 设置。最简单的方法是将 bean 引用到jtaTransactionManager您的 bean 的属性中 LocalSessionFactoryBean(请参阅Hibernate Transaction Setup)。然后 Spring 将相应的 JTA 策略提供给 Hibernate。

  • 您还可以在“hibernateProperties”中显式配置 Hibernate 的 JTA 相关属性,特别是“hibernate.transaction.coordinator_class”、“hibernate.connection.handling_mode”和可能的“hibernate.transaction.jta.platform” LocalSessionFactoryBean(参见 Hibernate 的手册这些属性的详细信息)。

本节的其余部分描述了在 Hibernate 是否知道 JTA 的情况下发生的事件序列PlatformTransactionManager

当 Hibernate 没有配置任何对 JTA 事务管理器的感知时,JTA 事务提交时会发生以下事件:

  • JTA 事务提交。

  • SpringJtaTransactionManager与 JTA 事务同步,因此afterCompletion由 JTA 事务管理器通过回调回调。

  • 在其他活动中,这种同步可以触发 Spring 对 Hibernate 的回调,通过 Hibernate 的afterTransactionCompletion回调(用于清除 Hibernate 缓存),然后显式close()调用 Hibernate 会话,这会导致 Hibernate 尝试close()JDBC 连接。

  • 在某些环境中,此Connection.close()调用随后会触发警告或错误,因为应用程序服务器不再认为它Connection是可用的,因为事务已经提交。

当 Hibernate 配置了对 JTA 事务管理器的感知时,JTA 事务提交时会发生以下事件:

  • JTA 事务已准备好提交。

  • SpringJtaTransactionManager与 JTA 事务同步,因此beforeCompletionJTA 事务管理器通过回调回调该事务。

  • Spring 知道 Hibernate 本身与 JTA 事务同步,并且其行为与之前的场景不同。特别是,它与 Hibernate 的事务性资源管理保持一致。

  • JTA 事务提交。

  • Hibernate 是同步到 JTA 事务的,所以事务被afterCompletionJTA 事务管理器通过回调的方式回调,并且可以正确的清除它的缓存。

5.4. JPA

包下提供的 Spring JPA以类似于与 Hibernate 集成的方式为Java Persistence APIorg.springframework.orm.jpa提供全面支持, 同时了解底层实现以提供附加功能。

5.4.1。Spring 环境中 JPA 设置的三个选项

Spring JPA 支持提供了三种设置 JPA的方法EntityManagerFactory ,应用程序使用该 JPA 来获取实体管理器。

使用LocalEntityManagerFactoryBean

您只能在简单的部署环境中使用此选项,例如独立应用程序和集成测试。

LocalEntityManagerFactoryBean创建了一个EntityManagerFactory适用于应用程序仅使用 JPA 进行数据访问的简单部署环境。工厂 bean 使用 JPAPersistenceProvider自动检测机制(根据 JPA 的 Java SE 引导),并且在大多数情况下,只需要您指定持久性单元名称。以下 XML 示例配置了这样一个 bean:

<beans>
    <bean id="myEmf" class="org.springframework.orm.jpa.LocalEntityManagerFactoryBean">
        <property name="persistenceUnitName" value="myPersistenceUnit"/>
    </bean>
</beans>

这种形式的 JPA 部署是最简单和最有限的。您不能引用现有的 JDBC DataSourcebean 定义,并且不存在对全局事务的支持。此外,持久类的编织(字节码转换)是特定于提供者的,通常需要在启动时指定特定的 JVM 代理。此选项仅适用于设计 JPA 规范的独立应用程序和测试环境。

从 JNDI 获取 EntityManagerFactory

您可以在部署到 Java EE 服务器时使用此选项。查看您服务器的文档,了解如何将自定义 JPA 提供程序部署到您的服务器中,允许使用不同于服务器默认提供程序的提供程序。

从 JNDI获取一个EntityManagerFactory(例如在 Java EE 环境中)是一个更改 XML 配置的问题,如以下示例所示:

<beans>
    <jee:jndi-lookup id="myEmf" jndi-name="persistence/myPersistenceUnit"/>
</beans>

此操作假定标准 Java EE 引导。Java EE 服务器自动检测持久性单元(实际上META-INF/persistence.xml是应用程序 jar 中的文件)和 persistence-unit-refJava EE 部署描述符(例如 web.xml)中的条目,并为这些持久性单元定义环境命名上下文位置。

在这种情况下,整个持久性单元部署,包括持久性类的编织(字节码转换),都取决于 Java EE 服务器。JDBC DataSource是通过META-INF/persistence.xml文件中的 JNDI 位置定义的。 EntityManager事务与服务器的 JTA 子系统集成。Spring 仅使用获得的EntityManagerFactory,通过依赖注入将其传递给应用程序对象,并为持久性单元管理事务(通常通过JtaTransactionManager)。

如果您在同一个应用程序中使用多个持久性单元,则此类 JNDI 检索的持久性单元的 bean 名称应该与应用程序用来引用它们的持久性单元名称匹配(例如,in@PersistenceUnit@PersistenceContextannotations)。

使用LocalContainerEntityManagerFactoryBean

您可以在基于 Spring 的应用程序环境中使用此选项来获得完整的 JPA 功能。这包括 Tomcat 等 Web 容器、独立应用程序和具有复杂持久性要求的集成测试。

如果您想专门配置 Hibernate 设置,一个直接的替代方法是设置本机 HibernateLocalSessionFactoryBean而不是普通的 JPA LocalContainerEntityManagerFactoryBean,让它与 JPA 访问代码以及本机 Hibernate 访问代码交互。有关详细信息,请参阅JPA 交互的本机 Hibernate 设置

LocalContainerEntityManagerFactoryBean提供对 配置的完全控制,EntityManagerFactory适用于需要细粒度定制的环境。基于文件、提供的LocalContainerEntityManagerFactoryBean 策略PersistenceUnitInfo和指定的. 因此,可以使用 JNDI 之外的自定义数据源并控制编织过程。以下示例显示了 a 的典型 bean 定义 :persistence.xmldataSourceLookuploadTimeWeaverLocalContainerEntityManagerFactoryBean

<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/>快捷方式表示不应该扫描带注释的实体类。明确的“真”值 ( <exclude-unlisted-classes>true</exclude-unlisted-classes/>) 也意味着不扫描。 <exclude-unlisted-classes>false</exclude-unlisted-classes/>确实会触发扫描。exclude-unlisted-classes但是,如果您希望进行实体类扫描 ,我们建议您省略该元素。

使用LocalContainerEntityManagerFactoryBean是最强大的 JPA 设置选项,允许在应用程序中进行灵活的本地配置。它支持到现有 JDBC 的链接DataSource,支持本地和全局事务,等等。但是,它也对运行时环境提出了要求,例如,如果持久性提供程序需要字节码转换,则具有编织能力的类加载器的可用性。

此选项可能与 Java EE 服务器的内置 JPA 功能冲突。在完整的 Java EE 环境中,考虑EntityManagerFactory从 JNDI 获取。或者,persistenceXmlLocation在您的 定义中指定一个自LocalContainerEntityManagerFactoryBean定义(例如 META-INF/my-persistence.xml),并在您的应用程序 jar 文件中仅包含具有该名称的描述符。因为 Java EE 服务器只查找默认 META-INF/persistence.xml文件,所以它会忽略此类自定义持久性单元,因此可以避免与 Spring 驱动的 JPA 设置发生冲突。(例如,这适用于 Resin 3.1。)

什么时候需要加载时间编织?

并非所有 JPA 提供程序都需要 JVM 代理。Hibernate 就是一个不这样做的例子。如果您的提供者不需要代理或您有其他替代方案,例如在构建时通过自定义编译器或 Ant 任务应用增强功能,则不应使用加载时编织器。

LoadTimeWeaver接口是 Spring 提供的类,它允许 ClassTransformer以特定方式插入 JPA 实例,具体取决于环境是 Web 容器还是应用程序服务器。ClassTransformers 通过 代理挂钩 通常效率不高。代理针对整个虚拟机工作并检查加载的每个类,这在生产服务器环境中通常是不可取的。

SpringLoadTimeWeaver为各种环境提供了许多实现,让ClassTransformer实例仅适用于每个类加载器,而不适用于每个 VM。

有关实现及其设置的更多信息,请参阅AOP 章节中的Spring 配置LoadTimeWeaver,无论是通用的还是针对各种平台(例如 Tomcat、JBoss 和 WebSphere)定制的。

Spring configurationLoadTimeWeaver中所述,您可以使用@EnableLoadTimeWeavingannotation 或 context:load-time-weaverXML 元素配置 context-wide 。LocalContainerEntityManagerFactoryBean所有 JPA实例都会自动选择这样的全局编织器。以下示例显示了设置加载时编织器的首选方式,提供平台的自动检测(例如 Tomcat 的支持编织的类加载器或 Spring 的 JVM 代理)以及将编织器自动传播到所有支持编织器的 bean:

<context:load-time-weaver/>
<bean id="emf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
    ...
</bean>

但是,如果需要,您可以通过 loadTimeWeaver属性手动指定专用编织器,如以下示例所示:

<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 实现时,这一点尤其重要,因为 JPA 转换器仅在类加载器级别应用,因此彼此隔离。

处理多个持久性单元

对于依赖于多个持久性单元位置的应用程序(例如,存储在类路径中的各种 JARS 中),Spring 提供了PersistenceUnitManager充当中央存储库并避免持久性单元发现过程,这可能是昂贵的。默认实现允许指定多个位置。这些位置被解析并稍后通过持久性单元名称检索。(默认情况下,在类路径中搜索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以声明方式(通过影响所有托管单元的属性)或以编程方式(通过 PersistenceUnitPostProcessor允许持久性单元选择)自定义实例(在将它们提供给 JPA 提供程序之前)。如果 PersistenceUnitManager指定 no,则由 内部创建和使用一个 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配置检查。但是,一旦其他组件(例如,调用createEntityManager)访问实际的 JPA 提供程序,这些调用就会阻塞,直到后台引导完成。特别是,当您使用 Spring Data JPA 时,请确保也为其存储库设置延迟引导。

5.4.2. 基于 JPA 实现 DAO:EntityManagerFactoryEntityManager

尽管EntityManagerFactory实例是线程安全的,但EntityManager实例不是。注入的 JPAEntityManager的行为类似于EntityManager从应用程序服务器的 JNDI 环境中提取的数据,如 JPA 规范所定义。它将所有调用委托给当前 transactional EntityManager(如果有)。否则,它会回退到新创建的EntityManager每个操作,实际上使其使用线程安全。

通过使用注入的EntityManagerFactoryEntityManager. 如果启用了 a,Spring 可以在字段和方法级别上理解 @PersistenceUnit和注释。以下示例显示了使用注释的普通 JPA DAO 实现:@PersistenceContextPersistenceAnnotationBeanPostProcessor@PersistenceUnit

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();
            }
        }
    }
}
科特林
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 利用注解来要求注入 default EntityManagerFactory,如以下示例 bean 定义所示:

<beans>

    <!-- bean post-processor for JPA annotations -->
    <bean class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor"/>

    <bean id="myProductDao" class="product.ProductDaoImpl"/>

</beans>

作为显式定义 a 的替代方法,请考虑在应用程序上下文配置中PersistenceAnnotationBeanPostProcessor使用 Spring XML 元素。context:annotation-config这样做会自动为基于注释的配置注册所有 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();
    }
}
科特林
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,是完全不同的事情。这会导致所谓的 extended EntityManager,它不是线程安全的,因此不能在并发访问的组件中使用,例如 Spring 管理的单例 bean。扩展EntityManager实例只应该用在有状态的组件中,例如,驻留在会话中,其生命周期 EntityManager不依赖于当前事务,而是完全取决于应用程序。

方法级和现场级注入

您可以在类中的字段或方法上应用指示依赖注入(例如@PersistenceUnit@PersistenceContext)的注释——因此有“方法级注入”和“字段级注入”的表达。字段级注释简洁且更易于使用,而方法级注释允许进一步处理注入的依赖项。在这两种情况下,成员可见性(公共、受保护或私有)都无关紧要。

类级别的注释呢?

在 Java EE 平台上,它们用于依赖声明而不是资源注入。

注入EntityManager是 Spring 管理的(知道正在进行的事务)。尽管新的 DAO 实现使用方法级注入 anEntityManager而不是 an EntityManagerFactory,但由于注释的使用,应用程序上下文 XML 中不需要更改。

这种 DAO 风格的主要优点是它只依赖于 Java Persistence API。不需要导入任何 Spring 类。此外,由于 JPA 注释被理解,注入由 Spring 容器自动应用。从非侵入性的角度来看,这很有吸引力,并且对 JPA 开发人员来说感觉更自然。

5.4.3. Spring 驱动的 JPA 事务

我们强烈建议您阅读声明式事务管理,如果您还没有这样做的话,以更详细地了解 Spring 的声明式事务支持。

JPA 的推荐策略是通过 JPA 的本机事务支持进行本地事务。SpringJpaTransactionManager针对任何常规 JDBC 连接池(无 XA 要求)提供了许多本地 JDBC 事务已知的功能(例如特定于事务的隔离级别和资源级别的只读优化)。

Spring JPA 还允许已配置JpaTransactionManager的 JPA 事务向访问相同的 JDBC 访问代码公开DataSource,前提是已注册 JpaDialect支持对底层 JDBC 的检索Connection。Spring 为 EclipseLink 和 Hibernate JPA 实现提供方言。有关机制的详细信息,请参阅下一节JpaDialect

作为一个直接的替代方案,Spring 的本机HibernateTransactionManager能够与 JPA 访问代码进行交互,适应几个 Hibernate 细节并提供 JDBC 交互。LocalSessionFactoryBean 这与设置结合起来特别有意义。有关详细信息,请参阅JPA 交互的本机 Hibernate 设置

5.4.4。理解JpaDialectJpaVendorAdapter

作为一项高级功能,JpaTransactionManager它的子类 AbstractEntityManagerFactoryBean允许将自定义JpaDialect传递到 jpaDialectbean 属性中。实现可以启用 Spring 支持的JpaDialect以下高级功能,通常以特定于供应商的方式:

  • 应用特定的事务语义(例如自定义隔离级别或事务超时)

  • 检索事务性 JDBC Connection(用于暴露于基于 JDBC 的 DAO)

  • PersistenceExceptionsSpring的高级翻译DataAccessExceptions

这对于特殊事务语义和异常的高级翻译特别有价值。默认实现 ( DefaultJpaDialect) 不提供任何特殊功能,如果需要前面列出的功能,则必须指定适当的方言。

作为一个更广泛的提供者适配工具,主要用于 Spring 的全功能 LocalContainerEntityManagerFactoryBean设置,JpaVendorAdapter它结合了JpaDialect其他提供者特定默认值的功能。指定 HibernateJpaVendorAdapteror是分别为 Hibernate 或 EclipseLinkEclipseLinkJpaVendorAdapter自动配置设置的最方便的方法。EntityManagerFactory请注意,这些提供者适配器主要设计用于与 Spring 驱动的事务管理一起使用(即与JpaTransactionManager.

有关其操作的更多详细信息以及如何在 Spring 的 JPA 支持中使用它们,请参阅JpaDialect和 javadoc。JpaVendorAdapter

5.4.5。使用 JTA 事务管理设置 JPA

作为 的替代方案JpaTransactionManager,Spring 还允许通过 JTA 进行多资源事务协调,无论是在 Java EE 环境中还是使用独立的事务协调器(例如 Atomikos)。除了选择 Spring's JtaTransactionManager而不是JpaTransactionManager,您还需要采取一些进一步的步骤:

  • 底层 JDBC 连接池需要支持 XA 并与您的事务协调器集成。这在 Java EE 环境中通常很简单,DataSource通过 JNDI 公开了一种不同的类型。有关详细信息,请参阅您的应用程序服务器文档。类似地,独立的事务协调器通常带有特殊的 XA 集成DataSource变体。再次,检查其文档。

  • EntityManagerFactory需要为 JTA 配置JPA设置。这是特定于提供者的,通常通过将特殊属性指定为jpaProperties on LocalContainerEntityManagerFactoryBean。在 Hibernate 的情况下,这些属性甚至是特定于版本的。有关详细信息,请参阅您的 Hibernate 文档。

  • SpringHibernateJpaVendorAdapter强制执行某些面向 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 自己的默认值。有关相关说明,请参阅Hibernate 的虚假应用程序服务器警告。

  • 或者,考虑EntityManagerFactory从您的应用程序服务器本身获取 (即,通过 JNDI 查找而不是本地声明的 LocalContainerEntityManagerFactoryBean)。服务器提供的EntityManagerFactory 可能需要在您的服务器配置中进行特殊定义(使部署的可移植性降低),但它是为服务器的 JTA 环境设置的。

5.4.6。用于 JPA 交互的本机 Hibernate 设置和本机 Hibernate 事务

LocalSessionFactoryBean与 结合的本机设置允许与其他 JPA 访问代码HibernateTransactionManager 进行交互。@PersistenceContextHibernate 现在SessionFactory本机实现 JPA 的EntityManagerFactory接口,而 HibernateSession句柄本机是 JPA EntityManager。Spring 的 JPA 支持工具会自动检测本地 Hibernate 会话。

因此,这种本机 Hibernate 设置可以在许多场景中替代标准 JPA LocalContainerEntityManagerFactoryBeanJpaTransactionManager组合,允许在同一本地事务中与SessionFactory.getCurrentSession() (以及HibernateTemplate)相邻的交互。@PersistenceContext EntityManager这样的设置还提供了更强的 Hibernate 集成和更多的配置灵活性,因为它不受 JPA 引导合同的约束。

在这种情况下您不需要HibernateJpaVendorAdapter配置,因为 Spring 的本机 Hibernate 设置提供了更多功能(例如,自定义 Hibernate Integrator 设置、Hibernate 5.3 bean 容器集成以及对只读事务的更强优化)。最后但同样重要的是,您还可以通过 表达原生 Hibernate 设置,与样式配置LocalSessionFactoryBuilder无缝集成(不涉及)。@BeanFactoryBean

LocalSessionFactoryBeanLocalSessionFactoryBuilder支持后台引导,就像 JPALocalContainerEntityManagerFactoryBean一样。有关介绍,请参阅背景引导

在 上LocalSessionFactoryBean,可通过该bootstrapExecutor 属性获得。在 programmaticLocalSessionFactoryBuilder上,重载 buildSessionFactory方法采用引导执行程序参数。

6. 使用 Object-XML 映射器编组 XML

6.1。介绍

本章描述 Spring 的 Object-XML Mapping 支持。对象-XML 映射(简称 OX 映射)是将 XML 文档与对象相互转换的行为。此转换过程也称为 XML 编组或 XML 序列化。本章交替使用这些术语。

在 OX 映射领域内,编组器负责将对象(图)序列化为 XML。以类似的方式,解组器将 XML 反序列化为对象图。此 XML 可以采用 DOM 文档、输入或输出流或 SAX 处理程序的形式。

使用 Spring 来满足您的 O/X 映射需求的一些好处是:

6.1.1. 易于配置

Spring 的 bean factory 使得配置 marshaller 变得很容易,无需构建 JAXB 上下文、JiBX 绑定工厂等。您可以像配置应用程序上下文中的任何其他 bean 一样配置编组器。此外,许多编组器都可以使用基于 XML 名称空间的配置,从而使配置更加简单。

6.1.2. 一致的接口

Spring 的 OX 映射通过两个全局接口进行操作:MarshallerUnmarshaller. 这些抽象让您可以相对轻松地切换 OX 映射框架,对执行编组的类几乎不需要更改。这种方法还有一个额外的好处,就是可以使用混合匹配方法(例如,一些使用 JAXB 和一些通过 XStream 执行的编组)以非侵入式的方式进行 XML 编组,让您可以利用每种方法的优势技术。

6.1.3. 一致的异常层次结构

Spring 提供了从底层 OX 映射工具的异常到它自己的异常层次结构的转换,并将异常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接口有一个 main 方法,它将给定的对象编组到给定的javax.xml.transform.Result. 结果是一个基本上代表 XML 输出抽象的标记接口。具体实现包装了各种 XML 表示,如下表所示:

结果执行 包装 XML 表示

DOMResult

org.w3c.dom.Node

SAXResult

org.xml.sax.ContentHandler

StreamResult

java.io.File, java.io.OutputStream, 或java.io.Writer

尽管该marshal()方法接受一个普通对象作为其第一个参数,但大多数 Marshaller实现不能处理任意对象。相反,对象类必须在映射文件中进行映射,用注释标记,向编组器注册,或具有公共基类。请参阅本章后面的部分来确定您的 OX 技术如何管理它。

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 输入抽象)中读取并返回读取的对象。与 一样ResultSource是一个标记接口,具有三个具体实现。每个都包装不同的 XML 表示,如下表所示:

源码实现 包装 XML 表示

DOMSource

org.w3c.dom.Node

SAXSource

org.xml.sax.InputSource, 和org.xml.sax.XMLReader

StreamSource

java.io.File, java.io.InputStream, 或java.io.Reader

即使有两个独立的编组接口(MarshallerUnmarshaller),Spring-WS 中的所有实现都在一个类中实现。这意味着您可以连接一个编组器类,并在您的applicationContext.xml.

6.2.3. 理解XmlMappingException

Spring 将来自底层 OX 映射工具的异常转换为它自己的异常层次结构,并以XmlMappingException异常为根异常。这些运行时异常包装了原始异常,因此不会丢失任何信息。

此外,MarshallingFailureExceptionUnmarshallingFailureException 提供了编组和解组操作之间的区别,即使底层的 OX 映射工具不这样做。

OX Mapping异常层次结构如下图所示:

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;
    }
}
科特林
class Settings {
    var isFooEnabled: Boolean = false
}

应用程序类使用这个 bean 来存储它的设置。除了 main 方法之外,该类还有两个方法:saveSettings()将设置 bean 保存到名为 的文件 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();
    }
}
科特林
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需要同时设置 amarshallerunmarshaller属性。我们可以使用以下方法来做到这一点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,但我们也可以使用本章后面描述的任何其他编组器实例。请注意,默认情况下,XStream 不需要任何进一步的配置,因此 bean 定义相当简单。另请注意, XStreamMarshaller实现了Marshallerand ,因此我们可以在应用程序的and属性中Unmarshaller引用 xstreamMarshallerbean 。marshallerunmarshaller

此示例应用程序生成以下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 编组器的配置可能类似于以下内容:

<oxm:jaxb2-marshaller id="marshaller" contextPath="org.springframework.ws.samples.airline.schema"/>

6.5。JAXB

JAXB 绑定编译器将 W3C XML Schema 转换为一个或多个 Java 类、一个 jaxb.properties文件以及可能的一些资源文件。JAXB 还提供了一种从带注释的 Java 类生成模式的方法。

Spring 支持 JAXB 2.0 API 作为 XML 编组策略,遵循 中描述的Marshaller和接口。相应的集成类驻留在 包中。UnmarshallerMarshallerUnmarshallerorg.springframework.oxm.jaxb

6.5.1. 使用Jaxb2Marshaller

该类Jaxb2Marshaller实现了 SpringMarshallerUnmarshaller 接口。它需要上下文路径才能运行。您可以通过设置 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元素配置 a 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>

下表描述了可用的属性:

属性 描述 必需的

id

编组器的 ID

contextPath

JAXB 上下文路径

6.6. 吉宝

JiBX 框架提供的解决方案类似于 Hibernate 为 ORM 提供的解决方案:绑定定义定义了 Java 对象如何转换为 XML 或从 XML 转换的规则。在准备绑定和编译类之后,JiBX 绑定编译器会增强类文件并添加代码来处理将类的实例从 XML 转换为 XML 或从 XML 转换为 XML 的代码。

有关 JiBX 的更多信息,请参阅JiBX 网站。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>

AJibxMarshaller被配置为单个类。如果要编组多个类,则必须配置JibxMarshaller具有不同targetClass 属性值的多个实例。

XML 配置命名空间

jibx-marshaller标签配置 a org.springframework.oxm.jibx.JibxMarshaller,如以下示例所示:

<oxm:jibx-marshaller id="marshaller" target-class="org.springframework.ws.samples.airline.schema.Flight"/>

下表描述了可用的属性:

属性 描述 必需的

id

编组器的 ID

target-class

此编组器的目标类

是的

bindingName

此编组器使用的绑定名称

6.7. XStream

XStream 是一个简单的库,用于将对象序列化为 XML 并再次返回。它不需要任何映射并生成干净的 XML。

有关 XStream 的更多信息,请参阅XStream 网站。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上设置属性XStreamMarshaller,如以下示例所示:

<bean id="xstreamMarshaller" class="org.springframework.oxm.xstream.XStreamMarshaller">
    <property name="supportedClasses" value="org.springframework.oxm.xstream.Flight"/>
    ...
</bean>

这样做可以确保只有注册的类有资格进行解组。

此外,您可以注册 自定义转换器以确保只有您支持的类可以被解组。CatchAllConverter除了明确支持应支持的域类的转换器之外,您可能还希望添加 a作为列表中的最后一个转换器。因此,不会调用具有较低优先级和可能的安全漏洞的默认 XStream 转换器。

请注意,XStream 是一个 XML 序列化库,而不是数据绑定库。因此,它具有有限的命名空间支持。因此,它相当不适合在 Web 服务中使用。

七、附录

7.1。XML 模式

附录的这一部分列出了用于数据访问的 XML 模式,包括以下内容:

7.1.1. tx架构_

这些tx标签处理在 Spring 对事务的全面支持中配置所有这些 bean。这些标签包含在题为“ 事务管理”的章节中。

我们强烈建议您查看'spring-tx.xsd'Spring 发行版附带的文件。此文件包含 Spring 事务配置的 XML Schema,并涵盖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 实现的)。aop前面的 XML 片段包含引用架构所需的相关行,aop以便您可以使用命名空间中的元素。

7.1.2. jdbc架构_

这些jdbc元素使您可以快速配置嵌入式数据库或初始化现有数据源。这些元素分别记录在 Embedded Database SupportInitializing a DataSource中。

要使用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>
1 声明jdbc命名空间的使用。
2 指定位置(与其他模式位置)。

1. see XML Configuration