数据访问

1. 事务管理

全面的事务支持是使用 Spring 框架的最有力理由之一。Spring 框架为事务管理提供了统一的抽象,带来了以下优势:spring-doc.cadn.net.cn

以下各节描述了 Spring 框架的事务功能和技术:spring-doc.cadn.net.cn

本章还讨论了最佳实践, 应用服务器集成, 以及 常见问题的解决方案spring-doc.cadn.net.cn

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

传统上,Java EE 开发人员在事务管理方面只有两种选择: 全局事务或本地事务,这两种方式都有深刻的局限性。全局 和本地事务管理将在接下来的两个部分中进行回顾,然后讨论 Spring 框架的事务管理支持如何解决全局和本地事务模型的局限性。spring-doc.cadn.net.cn

1.1.1. 全局事务

全局事务允许您使用多个事务性资源,通常是关系型数据库和消息队列。应用服务器通过JTA管理全局事务,这是一个复杂的API(部分原因是由于其异常模型)。此外,JTA UserTransaction通常需要从JNDI获取,这意味着您也需要使用JNDI来使用JTA。全局事务的使用限制了应用程序代码的潜在重用,因为JTA通常只在应用服务器环境中可用。spring-doc.cadn.net.cn

以前,使用全局事务的首选方式是通过EJB CMT(容器管理事务)。CMT是一种声明式事务管理形式(与编程式事务管理相对)。EJB CMT消除了对事务相关JNDI查找的需求,尽管使用EJB本身需要使用JNDI。它消除了大部分但不是全部编写Java代码来控制事务的需求。其显著的缺点是CMT与JTA和应用服务器环境绑定。此外,只有在选择将业务逻辑实现为EJB(或至少位于事务性EJB外观之后)时才可用。EJB本身的缺点如此之多,以至于这并不是一个有吸引力的选择,尤其是在存在有说服力的声明式事务管理替代方案的情况下。spring-doc.cadn.net.cn

1.1.2. 本地事务

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

1.1.3. Spring Framework 的一致编程模型

Spring 解决了全局事务和本地事务的缺点。它让应用程序开发者可以在任何环境中使用一致的编程模型。您只需编写一次代码,就可以在不同环境中受益于不同的事务管理策略。Spring 框架提供了声明式和编程式的事务管理。大多数用户更倾向于使用声明式事务管理,我们通常也推荐在大多数情况下使用这种方式。spring-doc.cadn.net.cn

使用编程式事务管理时,开发者使用 Spring 框架的事务抽象,该抽象可以在任何底层事务基础设施上运行。采用首选的声明式模型时,开发者通常编写很少或不编写与事务管理相关的代码,因此不需要依赖 Spring 框架的事务 API 或任何其他事务 API。spring-doc.cadn.net.cn

你是否需要应用服务器来进行事务管理?

Spring框架的事务管理支持改变了传统规则,即企业Java应用程序何时需要应用服务器。spring-doc.cadn.net.cn

特别是,你不需要一个应用服务器仅仅为了通过 EJB 实现声明式事务。事实上,即使你的应用服务器具有强大的 JTA 功能,你可能仍会认为 Spring 框架的声明式事务比 EJB CMT 提供了更大的功能和更高效的编程模型。spring-doc.cadn.net.cn

通常,只有当您的应用程序需要在多个资源上处理事务时,才需要应用服务器的JTA功能,这并不是许多应用程序的要求。许多高端应用程序使用单一的、高度可扩展的数据库(例如Oracle RAC)代替。独立的事务管理器(例如 Atomikos TransactionsJOTM) 是其他选择。当然,您可能还需要其他应用服务器功能,例如Java消息服务(JMS)和Java EE连接器架构(JCA)。spring-doc.cadn.net.cn

Spring框架使您能够在适当的时候将应用程序扩展到完全加载的应用程序服务器。过去那种只能使用EJB CMT或JTA,否则就必须编写带有本地事务(例如JDBC连接上的事务)的代码,并且如果需要让这些代码在全局、容器管理的事务中运行时会面临大量重写的局面已经一去不复返了。借助Spring框架,您的配置文件中只需要部分bean定义发生改变(而不是您的代码)。spring-doc.cadn.net.cn

1.2. 了解 Spring 框架的事务抽象

Spring事务抽象的关键在于事务策略的概念。事务策略由一个TransactionManager定义,具体来说,是用于命令式事务管理的org.springframework.transaction.PlatformTransactionManager接口和用于响应式事务管理的org.springframework.transaction.ReactiveTransactionManager接口。下面的列表显示了PlatformTransactionManager API的定义:spring-doc.cadn.net.cn

Java
public interface PlatformTransactionManager extends TransactionManager {

    TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;

    void commit(TransactionStatus status) throws TransactionException;

    void rollback(TransactionStatus status) throws TransactionException;
}
Kotlin
interface PlatformTransactionManager : TransactionManager {

    @Throws(TransactionException::class)
    fun getTransaction(definition: TransactionDefinition): TransactionStatus

    @Throws(TransactionException::class)
    fun commit(status: TransactionStatus)

    @Throws(TransactionException::class)
    fun rollback(status: TransactionStatus)
}

这主要是服务提供者接口(SPI),尽管您也可以从应用程序代码中 程序化 使用它。因为 PlatformTransactionManager 是一个接口,可以根据需要轻松地进行模拟或存根。 它不依赖于查找策略,例如 JNDI。 PlatformTransactionManager 实现方式与其他对象(或 bean)在 Spring Framework IoC 容器中的定义方式相同。仅此优势就使 Spring Framework 的事务成为一种值得的抽象,即使您使用 JTA 时也是如此。您可以比直接使用 JTA 更容易地测试事务代码。spring-doc.cadn.net.cn

再次遵循Spring的哲学,TransactionException可以由PlatformTransactionManager接口的任何方法抛出,它是未检查的(即,它扩展了java.lang.RuntimeException类)。事务基础设施故障通常是致命的。在极少数情况下,应用程序代码实际上可以从事务失败中恢复时,应用程序开发人员仍然可以选择捕获并处理TransactionException。关键点是,开发者并不被强制这样做。spring-doc.cadn.net.cn

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

从 Spring Framework 5.2 开始,Spring 还为使用响应式类型或 Kotlin 协程的响应式应用程序提供了事务管理抽象。以下列表显示了由 org.springframework.transaction.ReactiveTransactionManager 定义的事务策略:spring-doc.cadn.net.cn

Java
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;
}
Kotlin
interface ReactiveTransactionManager : TransactionManager {

    @Throws(TransactionException::class)
    fun getReactiveTransaction(definition: TransactionDefinition): Mono<ReactiveTransaction>

    @Throws(TransactionException::class)
    fun commit(status: ReactiveTransaction): Mono<Void>

    @Throws(TransactionException::class)
    fun rollback(status: ReactiveTransaction): Mono<Void>
}

响应式事务管理器主要是一个服务提供者接口(SPI), 尽管您可以在您的 应用代码中 编程方式 使用它。由于 ReactiveTransactionManager 是一个接口,可以根据需要轻松地进行模拟或存根。spring-doc.cadn.net.cn

TransactionDefinition 接口指定:spring-doc.cadn.net.cn

  • 传播:通常,事务范围内的所有代码都在该事务中运行。但是,您可以指定当已存在事务上下文时,运行事务方法的行为。例如,代码可以继续在现有事务中运行(常见的情况),或者可以挂起现有事务并创建新事务。Spring 提供了从 EJB CMT 中熟悉的所有的事务传播选项。有关 Spring 中事务传播语义的信息,请参阅 事务传播spring-doc.cadn.net.cn

  • 隔离性:该事务与其他事务的工作之间的隔离程度。例如,该事务能否看到其他事务的未提交写入?spring-doc.cadn.net.cn

  • 超时:事务在超时并由底层事务基础设施自动回滚之前运行的时间。spring-doc.cadn.net.cn

  • 只读状态:当你的代码仅读取而不修改数据时,可以使用只读事务。在某些情况下,例如使用 Hibernate 时,只读事务可以作为一种有用的优化手段。spring-doc.cadn.net.cn

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

TransactionStatus 接口为事务代码提供了一种简单的方法来控制事务执行并查询事务状态。这些概念应该很熟悉,因为它们是所有事务 API 的共同点。以下列表显示了 TransactionStatus 接口:spring-doc.cadn.net.cn

Java
public interface TransactionStatus extends TransactionExecution, SavepointManager, Flushable {

    @Override
    boolean isNewTransaction();

    boolean hasSavepoint();

    @Override
    void setRollbackOnly();

    @Override
    boolean isRollbackOnly();

    void flush();

    @Override
    boolean isCompleted();
}
Kotlin
interface TransactionStatus : TransactionExecution, SavepointManager, Flushable {

    override fun isNewTransaction(): Boolean

    fun hasSavepoint(): Boolean

    override fun setRollbackOnly()

    override fun isRollbackOnly(): Boolean

    fun flush()

    override fun isCompleted(): Boolean
}

无论您选择在Spring中使用声明式还是编程式事务管理,定义正确的TransactionManager实现都是绝对必要的。您通常通过依赖注入来定义这个实现。spring-doc.cadn.net.cn

TransactionManager 实现通常需要了解其工作的环境:JDBC、JTA、Hibernate 等。以下示例显示了如何定义一个本地 PlatformTransactionManager 实现(在这种情况下,使用普通的 JDBC。)spring-doc.cadn.net.cn

您可以通过创建类似以下内容的bean来定义一个JDBC DataSourcespring-doc.cadn.net.cn

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

相关的 PlatformTransactionManager bean 定义随后引用了 DataSource 定义。它应该类似于以下示例:spring-doc.cadn.net.cn

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

如果您在 Java EE 容器中使用 JTA,则通过 JNDI 获取一个容器 DataSource,并将其与 Spring 的 JtaTransactionManager 一起使用。下面的示例显示了 JTA 和 JNDI 查找版本的外观:spring-doc.cadn.net.cn

<?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(或任何其他特定资源),因为它使用容器的全局事务管理基础设施。spring-doc.cadn.net.cn

前面定义的 dataSource bean 使用了 <jndi-lookup/> 标签 来自 jee 命名空间。有关更多信息,请参阅 JEE 模式
如果使用JTA,您的事务管理器定义应该相同,无论您使用的是哪种数据访问技术,无论是JDBC、Hibernate JPA,还是任何其他支持的技术。这是由于JTA事务是全局事务,可以加入任何事务性资源。

在所有Spring事务设置中,应用程序代码不需要更改。您只需通过更改配置即可改变事务的管理方式,即使该更改意味着从本地事务切换到全局事务或反之。spring-doc.cadn.net.cn

1.2.1. Hibernate 事务设置

您也可以轻松使用Hibernate本地事务,如下面的示例所示。 在这种情况下,您需要定义一个Hibernate LocalSessionFactoryBean,您的 应用程序代码可以使用它来获取Hibernate Session 实例。spring-doc.cadn.net.cn

DataSource bean 定义与前面显示的本地 JDBC 示例类似 因此,在下面的示例中未显示。spring-doc.cadn.net.cn

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

这个情况下的 txManager bean 是 HibernateTransactionManager 类型。与 DataSourceTransactionManager 需要对 DataSource 的引用一样,HibernateTransactionManager 也需要对 SessionFactory 的引用。下面的示例声明了 sessionFactorytxManager beans:spring-doc.cadn.net.cn

<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 事务,应该使用与之前 JTA 示例中相同的 JtaTransactionManager,如下例所示。此外,建议通过其事务协调器让 Hibernate 了解 JTA,并可能还应配置其连接释放模式:spring-doc.cadn.net.cn

<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,以强制使用相同的默认值:spring-doc.cadn.net.cn

<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 对应 Hibernate SessionFactory,等等)现在应该已经清楚了。本节描述了应用程序代码(直接或间接地,通过使用如 JDBC、Hibernate 或 JPA 等持久化 API)如何确保这些资源被正确地创建、重用和清理。本节还讨论了如何通过相关的 TransactionManager (可选地)触发事务同步。spring-doc.cadn.net.cn

1.3.1. 高级同步方法

推荐的方法是使用Spring最高级别的基于模板的持久化集成API,或者使用带有事务感知工厂Bean或代理的原生ORM API来管理原生资源工厂。这些事务感知解决方案内部处理资源的创建和重用、清理、可选的资源事务同步以及异常映射。因此,用户的数据访问代码不必处理这些任务,而可以专注于非样板的持久化逻辑。通常,您可以通过使用JdbcTemplate来使用原生ORM API或JDBC访问的模板方法。这些解决方案在本参考文档的后续部分中有详细说明。spring-doc.cadn.net.cn

1.3.2. 低级同步方法

诸如 DataSourceUtils(用于 JDBC)、EntityManagerFactoryUtils(用于 JPA)、 SessionFactoryUtils(用于 Hibernate)等类存在于较低层。当您希望应用程序代码直接处理原生持久化 API 的资源类型时, 您可以使用这些类来确保获得由 Spring 框架管理的正确实例, 事务可(可选地)进行同步,并且在此过程中发生的异常会被正确映射到一致的 API。spring-doc.cadn.net.cn

例如,在JDBC的情况下,而不是传统的JDBC方法,在getConnection()对象上调用DataSource方法,您可以使用Spring的org.springframework.jdbc.datasource.DataSourceUtils类,如下所示:spring-doc.cadn.net.cn

Connection conn = DataSourceUtils.getConnection(dataSource);

如果现有的事务已经有一个连接(链接)到它,那么将返回该实例。否则,该方法调用会触发新连接的创建,并且该连接(可选地)与任何现有事务同步,并在该同一事务中可供后续重用。如前所述,任何SQLException都会被封装在Spring框架的CannotGetJdbcConnectionException中,这是Spring框架的未检查DataAccessException类型层次结构的一部分。这种方法提供的信息比从SQLException中容易获得的信息更多,并确保在不同数据库甚至不同持久化技术之间的可移植性。spring-doc.cadn.net.cn

这种方法在不使用Spring事务管理的情况下也适用(事务同步是可选的),因此无论是否使用Spring进行事务管理,你都可以使用它。spring-doc.cadn.net.cn

当然,一旦你使用了Spring的JDBC支持、JPA支持或Hibernate支持,你通常会更倾向于不使用DataSourceUtils或其他辅助类,因为通过Spring抽象来工作会让你更加愉快,而不是直接使用相关API。例如,如果你使用Spring的JdbcTemplatejdbc.object包来简化JDBC的使用,后台会正确地获取连接,你无需编写任何特殊代码。spring-doc.cadn.net.cn

1.3.3. TransactionAwareDataSourceProxy

在最底层存在 TransactionAwareDataSourceProxy 类。这是对目标 DataSource 的代理,它包装目标 DataSource 以添加对 Spring 管理的事务的感知。在这方面,它类似于由 Java EE 服务器提供的事务性 JNDI DataSourcespring-doc.cadn.net.cn

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

1.4. 声明式事务管理

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

Spring框架的声明式事务管理是通过Spring面向切面编程(AOP)实现的。然而,由于事务方面的代码随Spring框架分发,并且可以以模板化的方式使用,因此通常不需要理解AOP概念就可以有效使用这段代码。spring-doc.cadn.net.cn

Spring框架的声明式事务管理类似于EJB CMT,也就是说,您可以将事务行为(或没有事务的行为)指定到单个方法级别。 如果需要,您可以在事务上下文中调用setRollbackOnly()。这两种事务管理方式之间的差异包括:spring-doc.cadn.net.cn

  • 与EJB CMT不同,后者与JTA绑定,Spring框架的声明式事务管理可以在任何环境中工作。它可以通过调整配置文件,使用JDBC、JPA或Hibernate与JTA事务或本地事务一起工作。spring-doc.cadn.net.cn

  • 你可以将 Spring Framework 的声明式事务管理应用于任何类,而不仅仅是 EJB 等特殊类。spring-doc.cadn.net.cn

  • Spring 框架提供了声明式的 回滚规则,这一特性在 EJB 中没有对应项。提供了对回滚规则的编程式和声明式支持。spring-doc.cadn.net.cn

  • Spring框架通过使用AOP允许您自定义事务行为。 例如,您可以在事务回滚的情况下插入自定义行为。您 还可以添加任意通知,以及事务通知。使用EJB CMT时, 除了使用 setRollbackOnly()之外,您无法影响容器的事务管理。spring-doc.cadn.net.cn

  • Spring 框架不支持跨远程调用传播事务上下文,这一点与高端应用服务器不同。如果您需要此功能,我们建议您使用 EJB。然而,在使用此类功能之前,请仔细考虑,因为通常情况下,人们不希望事务跨越远程调用。spring-doc.cadn.net.cn

回滚规则的概念很重要。它们允许你指定哪些异常(和抛出的事件)应导致自动回滚。你可以通过配置进行声明,而不是在Java代码中进行。因此,尽管你仍然可以调用setRollbackOnly()TransactionStatus对象上回滚当前事务,但通常你可以指定一个规则,即MyApplicationException必须始终导致回滚。这种选项的主要优势是业务对象不依赖于事务基础结构。例如,它们通常不需要导入Spring事务API或其他Spring API。spring-doc.cadn.net.cn

虽然EJB容器的默认行为在发生系统异常(通常是运行时异常)时会自动回滚事务,但EJB CMT不会在发生应用异常(即除<code>0</code>以外的检查型异常)时自动回滚事务。虽然Spring声明式事务管理的默认行为遵循EJB的约定(仅在未检查异常时自动回滚),但通常对这种行为进行自定义会很有用。spring-doc.cadn.net.cn

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

仅仅告诉您使用@Transactional注解标注您的类,将@EnableTransactionManagement添加到您的配置中,并期望您能理解其工作原理是不够的。为了提供更深入的理解,本节将在与事务相关问题的背景下,解释Spring框架声明式事务基础设施的内部工作原理。spring-doc.cadn.net.cn

Spring框架声明式事务支持中最需要掌握的概念是,此支持是通过AOP代理启用的,并且事务通知由元数据(目前为基于XML或注解的)驱动。AOP与事务元数据的结合产生了一个AOP代理,该代理使用TransactionInterceptor并结合适当的TransactionManager实现来在方法调用周围驱动事务。spring-doc.cadn.net.cn

Spring AOP 由 AOP 部分 进行介绍。

Spring Framework 的 TransactionInterceptor 为命令式和响应式编程模型提供事务管理。拦截器通过检查方法返回类型来检测所需的事务管理类型。返回响应式类型(如 Publisher 或 Kotlin Flow(或这些类型的子类))的方法适用于响应式事务管理。所有其他返回类型,包括 void,将使用命令式事务管理的代码路径。spring-doc.cadn.net.cn

事务管理的类型会影响需要哪个事务管理器。命令式事务需要一个 PlatformTransactionManager,而响应式事务使用 ReactiveTransactionManager 实现。spring-doc.cadn.net.cn

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

ReactiveTransactionManager 管理的响应式事务使用 Reactor 上下文而不是线程局部属性。因此,所有参与的数据访问操作都需要在同一响应式管道的相同 Reactor 上下文中执行。spring-doc.cadn.net.cn

下图展示的是在事务代理上调用方法的概念性视图:spring-doc.cadn.net.cn

tx

1.4.2. 声明式事务实现示例

考虑以下接口及其相应的实现。此示例使用FooBar类作为占位符,以便您可以专注于事务用法,而无需关注特定的领域模型。为了本示例的目的,DefaultFooService类在其每个已实现方法的主体中抛出UnsupportedOperationException实例是很好的。这种行为可以让您看到事务被创建,然后在响应UnsupportedOperationException实例时回滚。下列清单显示了FooService接口:spring-doc.cadn.net.cn

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

}
Kotlin
// the service interface that we want to make transactional

package x.y.service

interface FooService {

    fun getFoo(fooName: String): Foo

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

    fun insertFoo(foo: Foo)

    fun updateFoo(foo: Foo)
}

以下示例展示了上述接口的实现:spring-doc.cadn.net.cn

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) {
        // ...
    }
}
Kotlin
package x.y.service

class DefaultFooService : FooService {

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

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

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

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

假设接口 FooService 的前两个方法 getFoo(String)getFoo(String, String) 必须在具有只读语义的事务上下文中运行,而其他方法 insertFoo(Foo)updateFoo(Foo) 必须在具有读写语义的事务上下文中运行。以下配置在接下来的几段中详细解释:spring-doc.cadn.net.cn

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

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

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

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

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

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

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

</beans>

检查前面的配置。它假定您希望使服务对象, fooService bean,具有事务性。要应用的事务语义封装在 <tx:advice/> 定义中。<tx:advice/> 定义表示“所有以 get 开头的方法将在只读事务的上下文中运行,而其他所有方法将使用默认的事务语义”。transaction-manager 属性的 <tx:advice/> 标记设置为将驱动事务的 TransactionManager bean 的名称(在这种情况下,是 txManager bean)。spring-doc.cadn.net.cn

您可以省略事务通知中的 transaction-manager 属性 (<tx:advice/>),如果要注入的 TransactionManager 的 bean 名称是 transactionManager。如果要注入的 TransactionManager bean 有其他名称,则必须显式使用 transaction-manager 属性,如前面的示例所示。

<aop:config/> 定义确保由 txAdvice bean 定义的事务通知在程序的适当位置运行。首先,您定义一个匹配 FooService 接口中任何操作执行的切入点(fooServiceOperation)。然后通过使用通知器将切入点与 txAdvice 关联。结果表明,在执行 fooServiceOperation 时,由 txAdvice 定义的通知将被运行。spring-doc.cadn.net.cn

<aop:pointcut/>元素中定义的表达式是一个AspectJ切入点表达式。有关Spring中切入点表达式的更多细节,请参阅AOP部分spring-doc.cadn.net.cn

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

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

现在我们已经分析了配置,你可能在问, “所有这些配置实际上有什么作用呢?”spring-doc.cadn.net.cn

前面显示的配置用于在从fooService bean定义创建的对象周围创建一个事务代理。该代理配置了事务建议,以便当在代理上调用适当的方法时,根据与该方法相关联的事务配置,会启动、挂起、标记为只读等事务。考虑以下程序,它用于测试前面显示的配置:spring-doc.cadn.net.cn

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());
    }
}
Kotlin
import org.springframework.beans.factory.getBean

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

运行前面程序的输出应该如下所示(为清晰起见,已截断Log4J的输出以及由UnsupportedOperationException引发的insertFoo(..)方法的堆栈跟踪):spring-doc.cadn.net.cn

<!-- 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 [org.apache.commons.dbcp.PoolableConnection@a53de4] 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 [org.apache.commons.dbcp.PoolableConnection@a53de4]
[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-doc.cadn.net.cn

Spring Framework 使用 ReactiveAdapterRegistry 来确定方法返回类型是否为响应式。

以下清单显示了之前使用的 FooService 的修改版本,但这次代码使用了响应式类型:spring-doc.cadn.net.cn

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

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

package x.y.service

interface FooService {

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

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

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

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

以下示例展示了上述接口的实现:spring-doc.cadn.net.cn

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) {
        // ...
    }
}
Kotlin
package x.y.service

class DefaultFooService : FooService {

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

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

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

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

命令式和响应式事务管理在事务边界和事务属性定义方面具有相同的语义。命令式和响应式事务之间的主要区别在于后者具有延迟性。TransactionInterceptor 会使用事务运算符装饰返回的响应式类型,以开始和清理事务。因此,调用事务响应方法会将实际的事务管理延迟到订阅类型,该类型会激活对响应式类型的处理。spring-doc.cadn.net.cn

响应式事务管理的另一个方面涉及数据逃逸,这是编程模型的自然结果。spring-doc.cadn.net.cn

命令式事务的方法返回值在方法成功终止后从事务方法中返回,以便部分计算的结果不会逃逸出方法闭包。spring-doc.cadn.net.cn

响应式事务方法返回一个响应式包装类型,该类型表示一个计算序列以及开始和完成该计算的承诺。spring-doc.cadn.net.cn

一个 Publisher 可以在交易进行中发出数据,但不一定已经完成。 因此,那些依赖整个交易成功完成的方法需要确保交易完成,并在调用代码中缓冲结果。spring-doc.cadn.net.cn

1.4.3. 回滚声明式事务

上一节概述了如何在应用程序中对类(通常是服务层类)声明性地指定事务设置。本节将介绍如何以简单的方式声明性地控制事务的回滚。spring-doc.cadn.net.cn

指示Spring框架的事务基础设施在事务的处理过程中需要回滚的推荐方法是,从当前正在事务上下文中执行的代码中抛出一个Exception。Spring框架的事务基础设施代码会在调用堆栈向上冒泡时捕获任何未处理的Exception,并决定是否将事务标记为回滚。spring-doc.cadn.net.cn

在默认配置中,Spring Framework 的事务基础设施代码仅在发生运行时、未检查异常时标记事务为回滚。也就是说,当抛出的异常是 RuntimeException 的实例或子类时(Error 实例默认情况下也会导致回滚)。从事务方法中抛出的已检查异常在默认配置下不会导致回滚。spring-doc.cadn.net.cn

您可以配置确切的 Exception 类型,这些类型会标记事务进行回滚, 包括已检查异常。下面的XML片段演示了如何为一个已检查的应用程序专用 Exception 类型配置回滚:spring-doc.cadn.net.cn

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

如果您不希望在抛出异常时回滚事务,也可以指定“不回滚规则”。下面的示例告诉Spring框架的事务基础设施,在遇到未处理的InstrumentNotFoundException时仍提交相关事务:spring-doc.cadn.net.cn

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

当Spring框架的事务基础设施捕获到异常并检查配置的回滚规则以确定是否将事务标记为回滚时,最强匹配规则将获胜。因此,在以下配置的情况下,除了InstrumentNotFoundException以外的任何异常都会导致相关事务回滚:spring-doc.cadn.net.cn

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

你也可以通过编程方式指示需要回滚。虽然简单,但此过程较为侵入性,并且会将你的代码与 Spring 框架的事务基础设施紧密耦合。下面的例子展示了如何以编程方式指示需要回滚:spring-doc.cadn.net.cn

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

你被强烈建议在可能的情况下使用声明式回滚方法。如果确实需要,也可以使用编程式回滚,但它的使用与实现干净的 POJO 基础架构相违背。spring-doc.cadn.net.cn

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

考虑这样一个场景,你有一系列服务层对象,并且想要为每个对象应用完全不同的事务配置。你可以通过定义不同的 <aop:advisor/> 元素,并设置不同的 pointcutadvice-ref 属性值来实现。spring-doc.cadn.net.cn

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

<?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:spring-doc.cadn.net.cn

<?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/>设置是:spring-doc.cadn.net.cn

您可以更改这些默认设置。下表总结了嵌套在 <tx:method/> 标签内的 <tx:advice/><tx:attributes/> 标签的各种属性:spring-doc.cadn.net.cn

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

namespring-doc.cadn.net.cn

spring-doc.cadn.net.cn

与事务属性相关联的方法名称。通配符(*)可用于将相同的事务属性设置应用于多个方法(例如,get*handle*on*Event 等)。spring-doc.cadn.net.cn

propagationspring-doc.cadn.net.cn

Nospring-doc.cadn.net.cn

REQUIREDspring-doc.cadn.net.cn

事务传播行为。spring-doc.cadn.net.cn

isolationspring-doc.cadn.net.cn

Nospring-doc.cadn.net.cn

DEFAULTspring-doc.cadn.net.cn

事务隔离级别。仅适用于传播设置为REQUIREDREQUIRES_NEW的情况。spring-doc.cadn.net.cn

timeoutspring-doc.cadn.net.cn

Nospring-doc.cadn.net.cn

-1spring-doc.cadn.net.cn

事务超时(秒)。仅适用于传播 REQUIREDREQUIRES_NEWspring-doc.cadn.net.cn

read-onlyspring-doc.cadn.net.cn

Nospring-doc.cadn.net.cn

falsespring-doc.cadn.net.cn

读写事务与只读事务。仅适用于 REQUIREDREQUIRES_NEWspring-doc.cadn.net.cn

rollback-forspring-doc.cadn.net.cn

Nospring-doc.cadn.net.cn

以逗号分隔的列表,包含触发回滚的 Exception 实例。例如, com.foo.MyBusinessException,ServletExceptionspring-doc.cadn.net.cn

no-rollback-forspring-doc.cadn.net.cn

Nospring-doc.cadn.net.cn

以逗号分隔的列表,包含不会触发回滚的 Exception 实例。例如,com.foo.MyBusinessException,ServletExceptionspring-doc.cadn.net.cn

1.4.6. 使用 @Transactional

除了基于 XML 的声明式事务配置方法外,您还可以使用基于注解的方法。将事务语义直接声明在 Java 源代码中,可以使这些声明更接近受影响的代码。由于旨在以事务方式使用的代码通常无论如何都会这样部署,因此不太可能造成不恰当的耦合。spring-doc.cadn.net.cn

标准的 javax.transaction.Transactional 注解也得到了支持,可作为 Spring 自有注解的直接替代品。有关更多详细信息,请参阅 JTA 1.2 文档。

通过使用@Transactional注解所带来的易用性最好通过一个示例来说明,该示例在下面的文本中进行了解释。 请考虑以下类定义:spring-doc.cadn.net.cn

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) {
        // ...
    }
}
Kotlin
// the service class that we want to make transactional
@Transactional
class DefaultFooService : FooService {

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

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

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

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

如上所述,在类级别使用时,该注解表示声明类(以及其子类)中所有方法的默认值。或者,可以单独对每个方法进行注解。有关Spring认为哪些方法是事务性的更多信息,请参见方法可见性和@Transactional。请注意,类级别的注解不适用于类层次结构中的祖先类;在这种情况下,需要在子类中重新声明继承的方法,以便参与子类级别的注解。spring-doc.cadn.net.cn

当像上面这样的POJO类被定义为Spring上下文中的bean时, 可以通过在@EnableTransactionManagement 注解的@Configuration类中使用@EnableTransactionManagement 注解使bean实例具有事务性。请参阅 javadoc 以获取详细信息。spring-doc.cadn.net.cn

在XML配置中,<tx:annotation-driven/>标签提供了类似的便利:spring-doc.cadn.net.cn

<!-- 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实例具有事务性的那一行。
您可以省略 transaction-manager 属性在 <tx:annotation-driven/> 标签中,如果要注入的 TransactionManager 的 bean 名称是 transactionManager。如果要注入的 TransactionManager bean 有其他名称,您必须使用 transaction-manager 属性,如前面的示例所示。

响应式事务方法使用响应式返回类型,与以下清单所示的命令式编程安排形成对比:spring-doc.cadn.net.cn

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) {
        // ...
    }
}
Kotlin
// the reactive service class that we want to make transactional
@Transactional
class DefaultFooService : FooService {

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

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

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

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

请注意,返回的 Publisher 在与 Reactive Streams 取消信号相关时有特殊考虑。有关更多详细信息,请参阅“使用 TransactionOperator”下的 取消信号 部分。spring-doc.cadn.net.cn

方法可见性和 @Transactional

当您使用 Spring 的标准配置进行事务代理时,应仅将 @Transactional 注解应用于具有 public 可见性的方法。如果您使用 @Transactional 注解对 protectedprivate 或包可见的方法进行注解,则不会引发错误,但被注解的方法不会表现出配置的事务设置。如果需要对非公共方法进行注解,请考虑下一段中关于基于类的代理的提示,或考虑使用 AspectJ 编译时或加载时编织(稍后描述)。spring-doc.cadn.net.cn

@EnableTransactionManagement 类中使用 @Configuration 时,也可以通过注册一个自定义的 transactionAttributeSource bean,使 protected 或包可见的方法对于基于类的代理成为事务性的,如下例所示。 但是请注意,基于接口的代理中的事务性方法必须始终为 public,并定义在被代理的接口中。spring-doc.cadn.net.cn

/**
 * 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 测试方法。有关示例,请参阅测试章节中的 事务管理spring-doc.cadn.net.cn

您可以将 @Transactional 注解应用于接口定义、接口上的方法、类定义或类上的方法。但是,@Transactional 注解的存在本身并不足以激活事务行为。@Transactional 注解只是元数据,可以被某些运行时基础设施使用,这些基础设施是 @Transactional-aware 的,并且可以利用元数据将适当的 bean 配置为具有事务行为。在前面的示例中,<tx:annotation-driven/> 元素会开启事务行为。spring-doc.cadn.net.cn

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

如果希望自调用方法也包含在事务中,请考虑使用 AspectJ 模式(参见下表中的 mode 属性)。在这种情况下,根本不会有代理。相反,目标类会被织入(即,其字节码会被修改),以支持任何类型方法的 @Transactional 运行时行为。spring-doc.cadn.net.cn

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

transaction-managerspring-doc.cadn.net.cn

未提供(参见 TransactionManagementConfigurer 的 javadoc)spring-doc.cadn.net.cn

transactionManagerspring-doc.cadn.net.cn

要使用的事务管理器的名称。仅当事务管理器的名称不是transactionManager时才需要,如前面的示例所示。spring-doc.cadn.net.cn

modespring-doc.cadn.net.cn

modespring-doc.cadn.net.cn

proxyspring-doc.cadn.net.cn

默认模式(proxy)通过使用Spring的AOP框架对带注解的bean进行代理处理(遵循前面讨论的代理语义,仅适用于通过代理传入的方法调用)。替代模式(aspectj)则使用Spring的AspectJ事务切面来织入受影响的类,修改目标类的字节码以适用于任何类型的方法调用。AspectJ织入还需要spring-aspects.jar在类路径中,并且需要启用运行时织入(或编译时织入)。(请参见Spring配置了解如何设置运行时织入的详细信息。)spring-doc.cadn.net.cn

proxy-target-classspring-doc.cadn.net.cn

proxyTargetClassspring-doc.cadn.net.cn

falsespring-doc.cadn.net.cn

仅适用于proxy模式。控制对使用@Transactional注解的类创建哪种类型的事务代理。如果proxy-target-class属性设置为true,则创建基于类的代理。如果proxy-target-classfalse或者该属性被省略,则创建标准的JDK接口代理。(有关不同代理类型的详细分析,请参见代理机制。)spring-doc.cadn.net.cn

orderspring-doc.cadn.net.cn

orderspring-doc.cadn.net.cn

Ordered.LOWEST_PRECEDENCEspring-doc.cadn.net.cn

定义了应用于带有@Transactional注解的Bean的事务建议的顺序。(有关AOP建议排序的相关规则,请参见建议排序。) 未指定排序意味着AOP子系统将确定建议的顺序。spring-doc.cadn.net.cn

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

在评估方法的事务设置时,最具体的定位具有优先权。在下面的例子中,DefaultFooService类在类级别上使用了只读事务的设置,但同一类中的@Transactional注解在updateFoo(Foo)方法上的设置优先于类级别上定义的事务设置。spring-doc.cadn.net.cn

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) {
        // ...
    }
}
Kotlin
@Transactional(readOnly = true)
class DefaultFooService : FooService {

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

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

@Transactional 注解是元数据,用于指定接口、类或方法必须具有事务语义(例如,“当调用此方法时启动一个新的只读事务,暂停任何现有事务”)。 默认的@Transactional设置如下:spring-doc.cadn.net.cn

您可以更改这些默认设置。下表总结了<code>0</code>注释的各种属性:spring-doc.cadn.net.cn

表3. @Transactional 设置
属性 类型 描述

spring-doc.cadn.net.cn

Stringspring-doc.cadn.net.cn

可选的限定符,用于指定要使用的事务管理器。spring-doc.cadn.net.cn

传播spring-doc.cadn.net.cn

enum: Propagationspring-doc.cadn.net.cn

可选的传播设置。spring-doc.cadn.net.cn

isolationspring-doc.cadn.net.cn

enum: Isolationspring-doc.cadn.net.cn

可选的隔离级别。仅适用于传播值为 REQUIREDREQUIRES_NEW 的情况。spring-doc.cadn.net.cn

timeoutspring-doc.cadn.net.cn

int(以秒为单位,精度为)spring-doc.cadn.net.cn

可选的事务超时。仅适用于传播值为 REQUIREDREQUIRES_NEW 的情况。spring-doc.cadn.net.cn

readOnlyspring-doc.cadn.net.cn

booleanspring-doc.cadn.net.cn

读写事务与只读事务。仅适用于值为REQUIREDREQUIRES_NEW的情况。spring-doc.cadn.net.cn

rollbackForspring-doc.cadn.net.cn

包含 Class 个对象的数组,这些对象必须从 Throwable. 继承spring-doc.cadn.net.cn

可选的异常类数组,这些异常必须导致回滚。spring-doc.cadn.net.cn

rollbackForClassNamespring-doc.cadn.net.cn

类名数组。这些类必须从 Throwable. 继承spring-doc.cadn.net.cn

可选的异常类名称数组,这些异常必须导致回滚。spring-doc.cadn.net.cn

noRollbackForspring-doc.cadn.net.cn

包含 Class 个对象的数组,这些对象必须从 Throwable. 继承spring-doc.cadn.net.cn

可选的异常类数组,这些异常不会导致回滚。spring-doc.cadn.net.cn

noRollbackForClassNamespring-doc.cadn.net.cn

包含 String 个类名的数组,这些类名必须从 Throwable. 继承spring-doc.cadn.net.cn

可选的异常类名称数组,这些异常不会导致回滚。spring-doc.cadn.net.cn

目前,您无法对事务的名称进行显式控制,“名称”指的是事务监视器中显示的事务名称(如果适用,例如 WebLogic 的事务监视器)以及日志输出中的名称。对于声明式事务,事务名称始终是完全限定的类名 + . + 事务建议类的方法名。例如,如果 handlePayment(..) 方法启动了事务,那么事务的名称将是: com.example.BusinessService.handlePaymentspring-doc.cadn.net.cn

带有 @Transactional 的多个事务管理器

大多数Spring应用程序只需要一个事务管理器,但有时您可能希望在单个应用程序中使用多个独立的事务管理器。您可以使用valuetransactionManager属性的@Transactional注解来可选地指定要使用的TransactionManager的标识。这可以是事务管理器bean的bean名称或限定符值。例如,使用限定符符号,您可以将以下Java代码与应用程序上下文中的以下事务管理器bean声明结合起来:spring-doc.cadn.net.cn

Java
public class TransactionalService {

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

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

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

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

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

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

以下列表显示了 bean 声明:spring-doc.cadn.net.cn

<tx:annotation-driven/>

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

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

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

在这种情况下,TransactionalService上的各个方法在不同的事务管理器下运行,这些事务管理器通过orderaccountreactive-account限定符进行区分。如果未找到特定的TransactionManager bean,则仍使用默认的<tx:annotation-driven>目标bean名称,transactionManagerspring-doc.cadn.net.cn

自定义组合注解

如果您发现您在许多不同的方法上反复使用相同的属性 @TransactionalSpring 的元注解支持 允许您为您的特定用例定义自定义组合注解。例如,考虑以下注解定义:spring-doc.cadn.net.cn

Java
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Transactional("order")
public @interface OrderTx {
}

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Transactional("account")
public @interface AccountTx {
}
Kotlin
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.TYPE)
@Retention(AnnotationRetention.RUNTIME)
@Transactional("order")
annotation class OrderTx

@Target(AnnotationTarget.FUNCTION, AnnotationTarget.TYPE)
@Retention(AnnotationRetention.RUNTIME)
@Transactional("account")
annotation class AccountTx

前面的注解让我们可以将上一节中的示例写成如下形式:spring-doc.cadn.net.cn

Java
public class TransactionalService {

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

    @AccountTx
    public void doSomething() {
        // ...
    }
}
Kotlin
class TransactionalService {

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

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

在前面的示例中,我们使用了语法来定义事务管理器限定符, 但我们也可以包含传播行为、回滚规则、超时和其他功能。spring-doc.cadn.net.cn

1.4.7. 事务传播

本节描述了Spring中事务传播的一些语义。请注意,本节并不是对事务传播本身的介绍。而是详细说明了与Spring中事务传播相关的某些语义。spring-doc.cadn.net.cn

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

了解 PROPAGATION_REQUIRED
tx prop required

PROPAGATION_REQUIRED 强制进行物理事务,如果当前作用域中尚不存在事务,则在当前作用域内进行,或者参与已定义的“外部”事务(适用于更大作用域)。这在同一个线程内的常见调用堆栈结构中是一个很好的默认选项(例如,一个服务外观,它将多个存储库方法进行委派,其中所有底层资源都必须参与服务级事务)。spring-doc.cadn.net.cn

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

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

然而,在内部事务作用域设置回滚标记的情况下,外部事务尚未自行决定是否回滚,因此由内部事务作用域静默触发的回滚是不可预期的。此时会抛出相应的UnexpectedRollbackException。这是预期的行为,以便事务调用者永远不会被误导,认为实际并未执行提交操作时却以为已经提交。因此,如果内部事务(外部调用者并不知情)静默地将事务标记为回滚-only,外部调用者仍然会调用commit。外部调用者需要收到一个UnexpectedRollbackException,以明确表示执行了回滚操作。spring-doc.cadn.net.cn

了解 PROPAGATION_REQUIRES_NEW
tx prop requires new

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

了解 PROPAGATION_NESTED

PROPAGATION_NESTED 使用一个带有多个保存点的物理事务,它能够回滚到这些保存点。这种部分回滚使得内部事务范围可以为其范围触发回滚,而外部事务可以在某些操作已被回滚的情况下继续进行物理事务。此设置通常映射到 JDBC 保存点,因此仅适用于 JDBC 资源事务。参见 Spring 的 DataSourceTransactionManagerspring-doc.cadn.net.cn

1.4.8. 通知事务操作

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

当您调用 updateFoo(Foo) 方法时,您会看到以下操作:spring-doc.cadn.net.cn

本章不会详细解释AOP(除非涉及事务)。有关AOP配置和AOP的详细内容,请参见 AOP

下面的代码展示了之前讨论的简单性能分析方面:spring-doc.cadn.net.cn

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;
    }
}
Kotlin
class SimpleProfiler : Ordered {

    private var order: Int = 0

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

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

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

通知的顺序通过Ordered接口进行控制。有关通知顺序的详细信息,请参阅 通知顺序spring-doc.cadn.net.cn

以下配置创建了一个 fooService bean,它按照所需的顺序应用了分析和事务方面:spring-doc.cadn.net.cn

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

你可以以类似的方式配置任意数量的其他方面。spring-doc.cadn.net.cn

以下示例创建了与前两个示例相同的设置,但使用了纯粹的 XML 声明方法:spring-doc.cadn.net.cn

<?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 (c.f. the order attribute) -->

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

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

    </aop:config>

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

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

</beans>

前面配置的结果是一个 fooService bean,它按照此顺序应用了分析和事务方面的功能。如果您希望分析建议在进入时在事务建议之后运行,并在退出时在事务建议之前运行,可以将分析方面 bean 的 order 属性的值交换,使其高于事务建议的顺序值。spring-doc.cadn.net.cn

你可以以类似的方式配置其他相关方面。spring-doc.cadn.net.cn

1.4.9. 在 AspectJ 中使用 @Transactional

您也可以通过 AspectJ 切面在 Spring 容器之外使用 Spring 框架的 @Transactional 支持。要做到这一点,首先用 @Transactional 注解您的类(和可选的类的方法),然后将您的应用程序与 org.springframework.transaction.aspectj.AnnotationTransactionAspect 进行链接(编织),该 org.springframework.transaction.aspectj.AnnotationTransactionAspect 定义在 spring-aspects.jar 文件中。您还必须用事务管理器配置该切面。您可以使用 Spring 框架的 IoC 容器来处理该切面的依赖注入。配置事务管理切面的最简单方法是使用 <tx:annotation-driven/> 元素,并如 使用 @Transactional 所述,将 mode 属性指定为 aspectj。由于我们在这里关注的是在 Spring 容器之外运行的应用程序,因此我们将向您展示如何以编程方式实现。spring-doc.cadn.net.cn

在继续之前,您可能想阅读 使用 @TransactionalAOP 分别。

以下示例显示了如何创建事务管理器并配置 AnnotationTransactionAspect 以使用它:spring-doc.cadn.net.cn

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);
Kotlin
// construct an appropriate transaction manager
val txManager = DataSourceTransactionManager(getDataSource())

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

类上的 @Transactional 注解指定了该类中任何公共方法执行时的默认事务语义。spring-doc.cadn.net.cn

方法上的 @Transactional 注解会覆盖类注解提供的默认事务语义(如果存在的话)。您可以对任何方法进行注解,无论其可见性如何。spring-doc.cadn.net.cn

通过使用 AnnotationTransactionAspect 将您的应用程序编织在一起,您必须使用 AspectJ 构建您的应用程序(参见 AspectJ 开发指南)或使用加载时编织。有关使用 AspectJ 的加载时编织的讨论,请参见 Spring 框架中使用 AspectJ 的加载时编织spring-doc.cadn.net.cn

1.5. 编程式事务管理

Spring Framework 提供了两种编程式事务管理的方式,通过使用:spring-doc.cadn.net.cn

Spring团队通常建议在命令式流程中使用TransactionTemplate进行编程事务管理,而在响应式代码中使用TransactionalOperator。第二种方法类似于使用JTA UserTransaction API,尽管异常处理更为简便。spring-doc.cadn.net.cn

1.5.1. 使用 TransactionTemplate

TransactionTemplate 采用与其他 Spring 模板相同的思路,例如 JdbcTemplate。它使用回调方法(使应用程序代码无需执行获取和释放事务资源的样板代码),从而生成以意图为导向的代码,即你的代码只需专注于你想要完成的操作。spring-doc.cadn.net.cn

如下面的示例所示,使用 TransactionTemplate 会将您与 Spring 的事务基础设施和 API 完全耦合。是否适合您的开发需求的编程式事务管理,是您需要自己做出的决定。

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

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();
            }
        });
    }
}
Kotlin
// use constructor-injection to supply the PlatformTransactionManager
class SimpleService(transactionManager: PlatformTransactionManager) : Service {

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

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

如果没有返回值,可以使用方便的 TransactionCallbackWithoutResult 类 与匿名类一起使用,如下所示:spring-doc.cadn.net.cn

Java
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
    protected void doInTransactionWithoutResult(TransactionStatus status) {
        updateOperation1();
        updateOperation2();
    }
});
Kotlin
transactionTemplate.execute(object : TransactionCallbackWithoutResult() {
    override fun doInTransactionWithoutResult(status: TransactionStatus) {
        updateOperation1()
        updateOperation2()
    }
})

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

Java
transactionTemplate.execute(new TransactionCallbackWithoutResult() {

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

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

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

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...
    }
}
Kotlin
class SimpleService(transactionManager: PlatformTransactionManager) : Service {

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

以下示例通过使用Spring XML配置定义了一个带有自定义事务设置的 TransactionTemplatespring-doc.cadn.net.cn

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

然后您可以将 sharedTransactionTemplate 注入到所需数量的服务中。spring-doc.cadn.net.cn

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

1.5.2. 使用 TransactionOperator

TransactionOperator 采用了一种与其他响应式操作符类似的运算符设计。它使用回调方法(以使应用程序代码不必执行获取和释放事务性资源的样板代码),并且生成的代码具有明确的意图,即你的代码只需专注于你想要执行的操作。spring-doc.cadn.net.cn

如下面的示例所示,使用 TransactionOperator 会将您与 Spring 的事务基础设施和 API 完全耦合。是否适合您的开发需求的编程式事务管理,是您需要自己做出的决定。

必须在事务性上下文中运行并显式使用 TransactionOperator 的应用程序代码类似于下面的示例:spring-doc.cadn.net.cn

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);
    }
}
Kotlin
// use constructor-injection to supply the ReactiveTransactionManager
class SimpleService(transactionManager: ReactiveTransactionManager) : Service {

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

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

TransactionalOperator 可以有两种使用方式:spring-doc.cadn.net.cn

  • 使用 Project Reactor 类型的操作符样式(mono.as(transactionalOperator::transactional)spring-doc.cadn.net.cn

  • 其他情况的回调样式(transactionalOperator.execute(TransactionCallback<T>)spring-doc.cadn.net.cn

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

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

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

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

在响应式流中,Subscriber可以取消其Subscription并停止其Publisher。Project Reactor中的操作符,以及其他库中的操作符,如next()take(long)timeout(Duration)等,都可以发出取消操作。无法知道取消的原因,是由于错误还是仅仅因为不再有兴趣继续消费。在版本5.2中,TransactionalOperator默认在取消时提交事务。在版本5.3中,此行为将发生变化,取消时将回滚事务以实现可靠且确定的结果。因此,考虑事务下游使用的操作符非常重要。Publisher。特别是当是Flux或其他多值Publisher时,必须消耗全部输出才能使事务完成。spring-doc.cadn.net.cn

指定事务设置

您可以为<code>0</code>指定事务设置(例如传播模式、隔离级别、超时等)。默认情况下, <code>1</code>实例具有 默认事务设置。以下示例显示了对特定 <code>2</code>的事务设置的自定义spring-doc.cadn.net.cn

Java
public class SimpleService implements Service {

    private final TransactionalOperator transactionalOperator;

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

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

        this.transactionalOperator = TransactionalOperator.create(transactionManager, definition);
    }
}
Kotlin
class SimpleService(transactionManager: ReactiveTransactionManager) : Service {

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

1.5.3. 使用 TransactionManager

以下各节解释了命令式和响应式事务管理器的编程用法。spring-doc.cadn.net.cn

使用 PlatformTransactionManager

对于命令式事务,您可以直接使用 org.springframework.transaction.PlatformTransactionManager 来管理您的 事务。为此,通过 bean 引用将您使用的 PlatformTransactionManager 的实现传递给您的 bean。然后,通过使用 TransactionDefinitionTransactionStatus 对象,您可以启动事务、回滚和提交。下面的示例显示了如何操作:spring-doc.cadn.net.cn

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

TransactionStatus status = txManager.getTransaction(def);
try {
    // put your business logic here
}
catch (MyException ex) {
    txManager.rollback(status);
    throw ex;
}
txManager.commit(status);
Kotlin
val def = DefaultTransactionDefinition()
// explicitly setting the transaction name is something that can be done only programmatically
def.setName("SomeTxName")
def.propagationBehavior = TransactionDefinition.PROPAGATION_REQUIRED

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

txManager.commit(status)
使用 ReactiveTransactionManager

在使用响应式事务时,您可以直接使用 org.springframework.transaction.ReactiveTransactionManager 来管理您的 事务。为此,通过 bean 引用将您使用的 ReactiveTransactionManager 的实现传递给您的 bean。然后,通过使用 TransactionDefinitionReactiveTransaction 对象,您可以启动事务、回滚和提交。下面的示例展示了如何操作:spring-doc.cadn.net.cn

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

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

reactiveTx.flatMap(status -> {

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

    return tx.then(txManager.commit(status))
            .onErrorResume(ex -> txManager.rollback(status).then(Mono.error(ex)));
});
Kotlin
val def = DefaultTransactionDefinition()
// explicitly setting the transaction name is something that can be done only programmatically
def.setName("SomeTxName")
def.propagationBehavior = TransactionDefinition.PROPAGATION_REQUIRED

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

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

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

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

编程式事务管理通常仅在您有少量事务操作时才是一个好主意。例如,如果您有一个只需要对某些更新操作进行事务处理的 Web 应用程序,您可能不想通过 Spring 或其他技术来设置事务代理。在这种情况下,使用 TransactionTemplate 可能是一个好的方法。能够显式设置事务名称也只能通过编程式事务管理方法来实现。spring-doc.cadn.net.cn

另一方面,如果您的应用程序包含许多事务操作,声明式事务管理通常是值得的。它将事务管理从业务逻辑中分离出来,并且配置起来并不困难。在使用 Spring 框架时,相比于 EJB CMT,声明式事务管理的配置成本大大降低。spring-doc.cadn.net.cn

1.7. 事务绑定事件

从 Spring 4.2 开始,事件的监听器可以绑定到事务的一个阶段。 典型的例子是在事务成功完成时处理事件。 这样做可以让事件在当前事务的结果实际上对监听器有意义时,具有更大的灵活性。spring-doc.cadn.net.cn

您可以使用 @EventListener 注解来注册一个常规事件监听器。 如果需要将其绑定到事务,请使用 @TransactionalEventListener。 这样,监听器默认会绑定到事务的提交阶段。spring-doc.cadn.net.cn

下一个示例展示了这个概念。假设一个组件发布了订单创建事件,并且我们想要定义一个监听器,该监听器应在发布该事件的事务成功提交后才处理该事件。以下示例设置了这样的事件监听器:spring-doc.cadn.net.cn

Java
@Component
public class MyComponent {

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

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

@TransactionalEventListener 注解公开了一个 phase 属性,允许你自定义事务的阶段,以便将监听器绑定到该阶段。 有效的阶段包括 BEFORE_COMMITAFTER_COMMIT(默认)、AFTER_ROLLBACK,以及 AFTER_COMPLETION,后者会聚合事务的完成(无论是提交还是回滚)。spring-doc.cadn.net.cn

如果没有运行中的事务,由于无法满足所需的语义,监听器根本不会被调用。但是,您可以通过将注解的 fallbackExecution 属性设置为 true 来覆盖此行为。spring-doc.cadn.net.cn

@TransactionalEventListener 仅适用于由 PlatformTransactionManager 管理的线程绑定事务。由 ReactiveTransactionManager 管理的响应式事务使用 Reactor 上下文而不是线程局部属性,因此从事件监听器的角度来看,没有可以参与的兼容活动事务。spring-doc.cadn.net.cn

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

Spring 的事务抽象通常与应用服务器无关。此外,Spring 的 JtaTransactionManager 类(可以可选地执行 JNDI 查找以获取 JTA UserTransactionTransactionManager 对象)会自动检测后一个对象的位置,该位置因应用服务器而异。访问 JTA TransactionManager 可以实现增强的事务语义——特别是支持事务挂起。有关详细信息,请参阅 JtaTransactionManager javadoc。spring-doc.cadn.net.cn

Spring的JtaTransactionManager是在Java EE应用服务器上运行的标准选择,并且已知可以在所有常见服务器上正常工作。高级功能,例如事务挂起,在许多服务器上也可以正常工作(包括GlassFish、JBoss和Geronimo),而无需任何特殊配置。然而,为了完全支持的事务挂起和进一步的高级集成,Spring为WebLogic Server和WebSphere提供了特殊的适配器。这些适配器将在下面的章节中进行讨论。spring-doc.cadn.net.cn

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

1.8.1. IBM WebSphere

在 WebSphere 6.1.0.9 及以上版本中,推荐使用的 Spring JTA 事务管理器是 WebSphereUowTransactionManager。此特殊适配器使用 IBM 的 UOWManager API, 该 API 在 WebSphere Application Server 6.1.0.9 及更高版本中可用。通过此适配器, 由 PROPAGATION_REQUIRES_NEW 触发的 Spring 驱动的事务挂起(挂起和恢复)已得到 IBM 的正式支持。spring-doc.cadn.net.cn

1.8.2. Oracle WebLogic Server

在 WebLogic Server 9.0 或更高版本中,通常会使用 WebLogicJtaTransactionManager 而不是默认的 JtaTransactionManager 类。这个 WebLogic 特有的 JtaTransactionManager 的子类在 WebLogic 管理的事务环境中支持 Spring 事务定义的全部功能, 超越了标准的 JTA 语义。特性包括事务名称、每事务的隔离级别,以及在所有情况下正确恢复事务。spring-doc.cadn.net.cn

1.9 常见问题的解决方案

本节描述了一些常见问题的解决方案。spring-doc.cadn.net.cn

1.9.1. 为特定 DataSource 使用了错误的事务管理器

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

1.10. 其他资源

有关 Spring 框架事务支持的更多信息,请参见:spring-doc.cadn.net.cn

2. DAO 支持

Spring中的数据访问对象(DAO)支持旨在以一致的方式方便地使用数据访问技术(如JDBC、Hibernate或JPA)。这使您可以在上述提到的持久化技术之间轻松切换,同时也使您能够在编码时不必担心捕获每种技术特有的异常。spring-doc.cadn.net.cn

2.1. 一致的异常层次结构

Spring 提供了从特定于技术的异常(例如 SQLException)到其自己的异常类层次结构的便捷转换,其中 DataAccessException 是根异常。这些异常会包装原始异常,因此您永远不会有任何风险而丢失关于可能出错的任何信息。spring-doc.cadn.net.cn

除了JDBC异常之外,Spring还可以包装JPA和Hibernate特有的异常, 将其转换为一组聚焦的运行时异常。这使你可以在适当的层中处理大多数不可恢复的持久化异常,而无需在你的DAO中使用烦人的冗长的catch-and-throw块和异常声明。 (不过你仍然可以在任何需要的地方捕获和处理异常。)如上所述, JDBC异常(包括特定于数据库的方言)也会被转换为相同的层次结构,这意味着你可以在一致的编程模型中执行一些操作。spring-doc.cadn.net.cn

前面的讨论适用于Spring对各种ORM框架的支持中的各种模板类。如果使用基于拦截器的类,应用程序必须自己处理HibernateExceptionsPersistenceExceptions,最好是分别将它们委托给convertHibernateAccessException(..)convertJpaAccessException(..)方法,这些方法将异常转换为与org.springframework.dao异常层次结构中的异常兼容的异常。由于PersistenceExceptions是未检查的异常,它们也可以被抛出(尽管在异常方面牺牲了通用DAO抽象)。spring-doc.cadn.net.cn

以下图像显示了Spring提供的异常层次结构。 (请注意,图像中详细说明的类层次结构仅显示了整个 DataAccessException 层次结构的一个子集。)spring-doc.cadn.net.cn

DataAccessException

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

保证你的数据访问对象(DAO)或仓库提供异常转换的最佳方法是使用@Repository注解。此注解还允许组件扫描支持找到并配置你的DAO和仓库,而无需为它们提供XML配置条目。下面的示例展示了如何使用@Repository注解:spring-doc.cadn.net.cn

Java
@Repository (1)
public class SomeMovieFinder implements MovieFinder {
    // ...
}
1 这个 @Repository 注解。
Kotlin
@Repository (1)
class SomeMovieFinder : MovieFinder {
    // ...
}
1 这个 @Repository 注解。

任何DAO或仓库实现都需要访问持久化资源,具体取决于使用的持久化技术。例如,基于JDBC的仓库需要访问JDBC DataSource,而基于JPA的仓库需要访问一个 EntityManager。最简单的方法是通过使用其中一个 @Autowired@Inject@Resource@PersistenceContext 注解来注入此资源依赖。以下示例适用于JPA仓库:spring-doc.cadn.net.cn

Java
@Repository
public class JpaMovieFinder implements MovieFinder {

    @PersistenceContext
    private EntityManager entityManager;

    // ...
}
Kotlin
@Repository
class JpaMovieFinder : MovieFinder {

    @PersistenceContext
    private lateinit var entityManager: EntityManager

    // ...
}

如果您使用经典的 Hibernate API,可以注入 SessionFactory,如下例所示:spring-doc.cadn.net.cn

Java
@Repository
public class HibernateMovieFinder implements MovieFinder {

    private SessionFactory sessionFactory;

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

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

我们在这里展示的最后一个例子是关于典型的JDBC支持。您可以将DataSource注入到初始化方法或构造函数中,在那里您将通过使用这个DataSource来创建一个JdbcTemplate和其他数据访问支持类(例如SimpleJdbcCall和其他类)。下面的例子自动连接了一个DataSourcespring-doc.cadn.net.cn

Java
@Repository
public class JdbcMovieFinder implements MovieFinder {

    private JdbcTemplate jdbcTemplate;

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

    // ...
}
Kotlin
@Repository
class JdbcMovieFinder(dataSource: DataSource) : MovieFinder {

    private val jdbcTemplate = JdbcTemplate(dataSource)

    // ...
}
查看每种持久化技术的具体覆盖范围,了解如何配置应用程序上下文以利用这些注解。

3. 使用 JDBC 进行数据访问

Spring框架提供的JDBC抽象价值或许最好通过下面表格中列出的一系列操作来体现。该表格显示了Spring处理的操作以及您需要负责的操作。spring-doc.cadn.net.cn

表4. Spring JDBC - 各自负责什么?
动作 Spring

定义连接参数。spring-doc.cadn.net.cn

Xspring-doc.cadn.net.cn

打开连接。spring-doc.cadn.net.cn

Xspring-doc.cadn.net.cn

指定SQL语句。spring-doc.cadn.net.cn

Xspring-doc.cadn.net.cn

声明参数并提供参数值spring-doc.cadn.net.cn

Xspring-doc.cadn.net.cn

准备并执行语句。spring-doc.cadn.net.cn

Xspring-doc.cadn.net.cn

设置循环以遍历结果(如果有)。spring-doc.cadn.net.cn

Xspring-doc.cadn.net.cn

为每次迭代完成工作。spring-doc.cadn.net.cn

Xspring-doc.cadn.net.cn

处理任何异常。spring-doc.cadn.net.cn

Xspring-doc.cadn.net.cn

处理事务。spring-doc.cadn.net.cn

Xspring-doc.cadn.net.cn

关闭连接、语句和结果集。spring-doc.cadn.net.cn

Xspring-doc.cadn.net.cn

Spring框架处理了使JDBC如此繁琐的所有的底层细节。spring-doc.cadn.net.cn

3.1. 选择JDBC数据库访问方法

您可以选择多种方法作为您的JDBC数据库访问的基础。 除了三种类型的<code>0</code>之外,还有一种新的<code>1</code>和<code>2</code>方法优化了数据库元数据,而RDBMS对象方式则采用更面向对象的方法,类似于JDO查询设计。一旦开始使用其中一种方法,您仍然可以混合使用以包含其他方法的特性。所有方法都需要JDBC 2.0兼容的驱动程序,一些高级功能需要JDBC 3.0驱动程序。spring-doc.cadn.net.cn

  • JdbcTemplate 是经典的且最流行的 Spring JDBC 方法。这种方法是“最低级别”的方法,所有其他方法在底层都使用了 JdbcTemplate。spring-doc.cadn.net.cn

  • NamedParameterJdbcTemplate 通过提供命名参数来包装 JdbcTemplate,而不是传统的 JDBC ? 占位符。当 SQL 语句有多个参数时,这种方法能提供更好的文档说明和更便捷的使用体验。spring-doc.cadn.net.cn

  • SimpleJdbcInsertSimpleJdbcCall 优化数据库元数据以限制所需的配置数量。这种方法简化了编码,您只需提供表或存储过程的名称,并提供一个与列名称匹配的参数映射。只有在数据库提供足够的元数据时才能使用此方法。如果数据库不提供此元数据,您必须提供显式的参数配置。spring-doc.cadn.net.cn

  • 关系型数据库对象 — 包括 MappingSqlQuerySqlUpdateStoredProcedure — 需要您在初始化数据访问层时创建可重用且线程安全的对象。这种方法借鉴了 JDO 查询,其中您定义查询字符串、声明参数并编译查询。一旦完成这些操作, execute(…​)update(…​)findObject(…​) 方法可以多次调用,并使用不同的参数值。spring-doc.cadn.net.cn

3.2. 包层级

Spring Framework 的 JDBC 抽象框架由四个不同的包组成:spring-doc.cadn.net.cn

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

  • datasource: org.springframework.jdbc.datasource 包包含一个用于轻松访问的实用类以及各种简单的 DataSource 实现,您可以将其用于在 Java EE 容器之外测试和运行未修改的 JDBC 代码。一个名为 org.springfamework.jdbc.datasource.embedded 的子包提供了通过使用 Java 数据库引擎(如 HSQL、H2 和 Derby)创建嵌入式数据库的支持。参见 控制数据库连接嵌入式数据库支持spring-doc.cadn.net.cn

  • object: org.springframework.jdbc.object 包包含将关系数据库管理系统(RDBMS)查询、更新和存储过程表示为线程安全、可重用对象的类。请参见 将 JDBC 操作建模为 Java 对象。这种方法受到 JDO 的启发,尽管查询返回的对象自然与数据库断开连接。这种更高层次的 JDBC 抽象依赖于 org.springframework.jdbc.core 包中的低层次抽象。spring-doc.cadn.net.cn

  • support: org.springframework.jdbc.support 包提供了 SQLException 翻译功能和一些实用类。在 JDBC 处理过程中抛出的异常会被翻译为定义在 org.springframework.dao 包中的异常。这意味着使用 Spring JDBC 抽象层的代码不需要实现 JDBC 或 RDBMS 特定的错误处理。所有已翻译的异常都是未检查异常,这使您可以选择捕获可以恢复的异常,而让其他异常传播给调用者。参见 使用 SQLExceptionTranslatorspring-doc.cadn.net.cn

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

本节介绍如何使用JDBC核心类来控制基本的JDBC处理,包括错误处理。它包含以下主题:spring-doc.cadn.net.cn

3.3.1. 使用 JdbcTemplate

JdbcTemplate 是 JDBC 核心包中的核心类。它负责资源的创建和释放,这有助于您避免常见的错误,例如忘记关闭连接。它执行核心 JDBC 工作流的基本任务(如语句创建和执行),让应用程序代码提供 SQL 并提取结果。JdbcTemplate 类:spring-doc.cadn.net.cn

当您在代码中使用 JdbcTemplate 时,只需实现回调接口,并为其提供明确的契约。给定由 Connection 类提供的 JdbcTemplatePreparedStatementCreator 回调接口会创建一个预编译语句,提供 SQL 和任何必要的参数。对于 CallableStatementCreator 接口也是如此,它会创建可调用语句。 RowCallbackHandler 接口从每个 ResultSet 的行中提取值。spring-doc.cadn.net.cn

您可以通过直接实例化一个 JdbcTemplate 引用,在 DAO 实现中使用 JdbcTemplate,或者将其配置在 Spring IoC 容器中,并作为 bean 引用提供给 DAO。spring-doc.cadn.net.cn

DataSource 应始终在 Spring IoC 容器中配置为一个 Bean。在第一种情况下,该 Bean 直接提供给服务;在第二种情况下,它提供给预定义的模板。

此类发出的所有SQL在对应于模板实例的完全限定类名的类别下以DEBUG级别记录。 通常为JdbcTemplate,但如果您使用的是JdbcTemplate类的自定义子类,则可能不同。spring-doc.cadn.net.cn

以下部分提供了一些 JdbcTemplate 用法的示例。这些示例并不是 JdbcTemplate 所有功能的完整列表。 有关详细信息,请参阅相应的 javadocspring-doc.cadn.net.cn

查询(SELECT

以下查询获取关系中的行数:spring-doc.cadn.net.cn

Java
int rowCount = this.jdbcTemplate.queryForObject("select count(*) from t_actor", Integer.class);
Kotlin
val rowCount = jdbcTemplate.queryForObject<Int>("select count(*) from t_actor")!!

以下查询使用了绑定变量:spring-doc.cadn.net.cn

Java
int countOfActorsNamedJoe = this.jdbcTemplate.queryForObject(
        "select count(*) from t_actor where first_name = ?", Integer.class, "Joe");
Kotlin
val countOfActorsNamedJoe = jdbcTemplate.queryForObject<Int>(
        "select count(*) from t_actor where first_name = ?", arrayOf("Joe"))!!

以下查询用于查找 Stringspring-doc.cadn.net.cn

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

以下查询会查找并填充一个领域对象:spring-doc.cadn.net.cn

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

以下查询会查找并填充一个领域对象列表:spring-doc.cadn.net.cn

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

如果前面两个代码片段确实存在于同一个应用程序中,那么删除两个 RowMapper lambda 表达式中的重复内容,并将它们提取到一个单独的字段中,然后根据需要由 DAO 方法引用,这将是合理的。 例如,可能更好的写法是将前面的代码片段改写如下:spring-doc.cadn.net.cn

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

public List<Actor> findAllActors() {
    return this.jdbcTemplate.query( "select first_name, last_name from t_actor", actorRowMapper);
}
Kotlin
val actorMapper = RowMapper<Actor> { rs: ResultSet, rowNum: Int ->
    Actor(rs.getString("first_name"), rs.getString("last_name"))
}

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

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

以下示例插入一条新条目:spring-doc.cadn.net.cn

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

以下示例将更新现有条目:spring-doc.cadn.net.cn

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

以下示例删除一条记录:spring-doc.cadn.net.cn

Java
this.jdbcTemplate.update(
        "delete from t_actor where id = ?",
        Long.valueOf(actorId));
Kotlin
jdbcTemplate.update("delete from t_actor where id = ?", actorId.toLong())
其他 JdbcTemplate 操作

您可以使用 execute(..) 方法来运行任何任意的 SQL。因此,该方法通常用于 DDL 语句。它有大量重载版本,可以接受回调接口、绑定变量数组等。以下示例用于创建表:spring-doc.cadn.net.cn

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

以下示例调用一个存储过程:spring-doc.cadn.net.cn

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

更复杂的存储过程支持将在后面 介绍spring-doc.cadn.net.cn

JdbcTemplate 最佳实践

JdbcTemplate 类的实例在配置后是线程安全的。这一点很重要,因为这意味着您可以配置一个 JdbcTemplate 的实例,然后安全地将此共享引用注入到多个 DAO(或仓库)中。 JdbcTemplate 是有状态的,因为它维护对 DataSource 的引用,但此状态不是会话状态。spring-doc.cadn.net.cn

在使用 JdbcTemplate 类(以及相关的 NamedParameterJdbcTemplate 类)时,一种常见做法是 在你的 Spring 配置文件中配置一个 DataSource,然后将这个共享的 DataSource bean 依赖注入到你的 DAO 类中。JdbcTemplate 是在 DataSource 的 setter 中创建的。这使得 DAO 类看起来如下:spring-doc.cadn.net.cn

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...
}
Kotlin
class JdbcCorporateEventDao(dataSource: DataSource) : CorporateEventDao {

    private val jdbcTemplate = JdbcTemplate(dataSource)

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

下面的示例显示了相应的 XML 配置:spring-doc.cadn.net.cn

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

显式配置的替代方法是使用组件扫描和注解支持的依赖注入。在这种情况下,您可以将类标注为<code>0</code>> (这使其成为组件扫描的候选),并将<code>1</code>设置方法标注为<code>2</code>。下面的示例展示了如何操作:spring-doc.cadn.net.cn

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 使用 @Autowired 注解 DataSource 的设置方法。
3 使用 DataSource 创建一个新的 JdbcTemplate
Kotlin
@Repository (1)
class JdbcCorporateEventDao(dataSource: DataSource) : CorporateEventDao { (2)

    private val jdbcTemplate = JdbcTemplate(dataSource) (3)

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

下面的示例显示了相应的 XML 配置:spring-doc.cadn.net.cn

<?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 类仅作为便利提供。spring-doc.cadn.net.cn

无论您选择以上哪种模板初始化样式(或不使用),通常不需要每次运行SQL时都创建一个新的 JdbcTemplate 类的实例。 配置完成后,JdbcTemplate 实例是线程安全的。 如果您的应用程序访问多个数据库,可能需要多个 JdbcTemplate 实例,这需要多个 DataSources,并且随后需要多个配置不同的 JdbcTemplate 实例。spring-doc.cadn.net.cn

3.3.2. 使用 NamedParameterJdbcTemplate

NamedParameterJdbcTemplate 类通过使用命名参数来支持编程 JDBC 语句,而不是仅使用经典的占位符('?')参数来编程 JDBC 语句。 NamedParameterJdbcTemplate 类包装了一个 JdbcTemplate,并委托包装的 JdbcTemplate 来完成大部分工作。 本节仅描述 NamedParameterJdbcTemplate 类与 JdbcTemplate 本身不同的方面——即通过使用命名参数来编程 JDBC 语句。 下面的示例展示了如何使用 NamedParameterJdbcTemplatespring-doc.cadn.net.cn

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);
}
Kotlin
private val namedParameterJdbcTemplate = NamedParameterJdbcTemplate(dataSource)

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

注意在分配给 sql 变量的值中使用了命名参数符号,以及对应插入到 namedParameters 变量(类型为 MapSqlParameterSource)中的值。spring-doc.cadn.net.cn

或者,您可以通过使用基于Map的样式,将命名参数及其对应的值传递给NamedParameterJdbcTemplate实例。NamedParameterJdbcOperations所暴露的其余方法以及由NamedParameterJdbcTemplate类实现的方法遵循类似的模式,并在此不作详述。spring-doc.cadn.net.cn

以下示例显示了 Map 为基础的样式用法:spring-doc.cadn.net.cn

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);
}
Kotlin
// some JDBC-backed DAO class...
private val namedParameterJdbcTemplate = NamedParameterJdbcTemplate(dataSource)

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

NamedParameterJdbcTemplate(存在于同一Java包中)相关的一个不错的特点是SqlParameterSource接口。您已经在前面的代码片段中的一个示例中看到了该接口的实现(MapSqlParameterSource类)。一个SqlParameterSourceNamedParameterJdbcTemplate的命名参数值的来源。MapSqlParameterSource类是一个简单的实现,它围绕着一个java.util.Map进行适配,其中键是参数名称,值是参数值。spring-doc.cadn.net.cn

另一个 SqlParameterSource 实现是 BeanPropertySqlParameterSource 类。此类包装一个任意的 JavaBean(即,符合 JavaBean 约定 的类的实例),并使用包装的 JavaBean 的属性作为命名参数值的来源。spring-doc.cadn.net.cn

以下示例显示了一个典型的 JavaBean:spring-doc.cadn.net.cn

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

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

以下示例使用 NamedParameterJdbcTemplate 来返回前面示例中类的成员数量:spring-doc.cadn.net.cn

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);
}
Kotlin
// some JDBC-backed DAO class...
private val namedParameterJdbcTemplate = NamedParameterJdbcTemplate(dataSource)

private val namedParameterJdbcTemplate = NamedParameterJdbcTemplate(dataSource)

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

请注意,NamedParameterJdbcTemplate 类包装了一个经典的 JdbcTemplate 模板。如果您需要访问被包装的 JdbcTemplate 实例以使用仅在 JdbcTemplate 类中存在的功能,可以使用 getJdbcOperations() 方法通过 JdbcOperations 接口访问被包装的 JdbcTemplatespring-doc.cadn.net.cn

另请参阅 JdbcTemplate 最佳实践,了解在应用程序上下文中使用 NamedParameterJdbcTemplate 类的指南。spring-doc.cadn.net.cn

3.3.3. 使用 SQLExceptionTranslator

SQLExceptionTranslator 是由可以将 SQLExceptions 与 Spring 自己的 org.springframework.dao.DataAccessException 进行转换的类实现的接口, 这在数据访问策略方面是中立的。实现可以是通用的(例如,使用 JDBC 的 SQLState 代码)或专用的(例如,使用 Oracle 错误代码),以获得更高的精度。spring-doc.cadn.net.cn

SQLErrorCodeSQLExceptionTranslatorSQLExceptionTranslator 的默认实现。此实现使用特定的提供商代码。它比 SQLState 实现更精确。错误代码的翻译基于一个名为 SQLErrorCodes 的 JavaBean 类型类。此类由 SQLErrorCodesFactory 创建并填充,该类(正如其名称所示)是一个工厂,用于根据名为 sql-error-codes.xml 的配置文件内容创建 SQLErrorCodes。此文件包含提供商代码,并基于从 DatabaseMetaData 中获取的 DatabaseProductName。使用的实际数据库的代码将被使用。spring-doc.cadn.net.cn

SQLErrorCodeSQLExceptionTranslator 按照以下顺序应用匹配规则:spring-doc.cadn.net.cn

  1. 子类实现的任何自定义翻译。通常,使用提供的具体 SQLErrorCodeSQLExceptionTranslator,因此此规则不适用。只有在您实际提供了子类实现时才适用。spring-doc.cadn.net.cn

  2. 任何对 SQLExceptionTranslator 接口的自定义实现,作为 customSqlExceptionTranslator 属性提供给 SQLErrorCodes 类。spring-doc.cadn.net.cn

  3. 查找 CustomSQLErrorCodesTranslation 类的实例列表(为 SQLErrorCodes 类的 customTranslations 属性提供)中的匹配项。spring-doc.cadn.net.cn

  4. 错误代码匹配已应用。spring-doc.cadn.net.cn

  5. 使用备用翻译器。SQLExceptionSubclassTranslator 是默认的备用翻译器。如果此翻译不可用,则下一个备用翻译器是SQLStateSQLExceptionTranslatorspring-doc.cadn.net.cn

默认使用 SQLErrorCodesFactory 来定义 Error 代码和自定义异常 转换。它们从类路径中名为 sql-error-codes.xml 的文件中查找,并根据 当前使用的数据库的数据库元数据中的数据库名称定位匹配的 SQLErrorCodes 实例。

您可以扩展 SQLErrorCodeSQLExceptionTranslator,如下面的示例所示:spring-doc.cadn.net.cn

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;
    }
}
Kotlin
class CustomSQLErrorCodesTranslator : SQLErrorCodeSQLExceptionTranslator() {

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

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

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);
}
Kotlin
// create a JdbcTemplate and set data source
private val jdbcTemplate = JdbcTemplate(dataSource).apply {
    // create a custom translator and set the DataSource for the default translation lookup
    exceptionTranslator = CustomSQLErrorCodesTranslator().apply {
        this.dataSource = dataSource
    }
}

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

自定义翻译器会传入数据源,以便在sql-error-codes.xml中查找错误代码。spring-doc.cadn.net.cn

3.3.4. 运行语句

执行SQL语句所需的代码非常少。你需要一个DataSource和一个JdbcTemplate,包括随JdbcTemplate提供的便捷方法。下面的示例显示了创建新表所需包含的内容:spring-doc.cadn.net.cn

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))");
    }
}
Kotlin
import javax.sql.DataSource
import org.springframework.jdbc.core.JdbcTemplate

class ExecuteAStatement(dataSource: DataSource) {

    private val jdbcTemplate = JdbcTemplate(dataSource)

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

3.3.5. 运行查询

一些查询方法返回单个值。要从一行中检索计数或特定值,请使用queryForObject(..)。后者将返回的JDBC Type转换为作为参数传入的Java类。如果类型转换无效,会抛出InvalidDataAccessApiUsageException。下面的例子包含两个查询方法,一个用于int,另一个用于查询Stringspring-doc.cadn.net.cn

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);
    }
}
Kotlin
import javax.sql.DataSource
import org.springframework.jdbc.core.JdbcTemplate

class RunAQuery(dataSource: DataSource) {

    private val jdbcTemplate = JdbcTemplate(dataSource)

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

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

除了单结果查询方法外,还有几种方法会返回一个列表,其中每个条目对应查询返回的一行。最通用的方法是queryForList(..),它返回一个List,其中每个元素是一个Map,每个列对应一个条目,使用列名作为键。如果你在前面的示例中添加一个方法以检索所有行的列表,它可能如下所示:spring-doc.cadn.net.cn

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");
}
Kotlin
private val jdbcTemplate = JdbcTemplate(dataSource)

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

返回的列表将如下所示:spring-doc.cadn.net.cn

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

3.3.6. 更新数据库

以下示例更新某个主键的列:spring-doc.cadn.net.cn

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);
    }
}
Kotlin
import javax.sql.DataSource
import org.springframework.jdbc.core.JdbcTemplate

class ExecuteAnUpdate(dataSource: DataSource) {

    private val jdbcTemplate = JdbcTemplate(dataSource)

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

在前面的例子中, SQL 语句有行参数的占位符。你可以以 varargs 的形式或,作为对象数组传入参数值。因此,你应该显式地将基本类型包装在基本类型包装类中,或者使用自动装箱。spring-doc.cadn.net.cn

3.3.7. 检索自动生成的键

一个 update() 方便方法支持从数据库检索生成的主键。此功能是 JDBC 3.0 标准的一部分。有关详细信息,请参阅规范第 13.6 章。该方法的第一个参数是一个 PreparedStatementCreator,这是指定所需插入语句的方式。另一个参数是一个 KeyHolder,在更新成功返回时包含生成的键。没有一种标准的方法来创建适当的 PreparedStatement(这就是方法签名为何如此的原因)。下面的示例适用于 Oracle,但可能不适用于其他平台:spring-doc.cadn.net.cn

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

本节内容包括:spring-doc.cadn.net.cn

3.4.1. 使用 DataSource

Spring 通过 DataSource 获得对数据库的连接。 DataSource 是 JDBC 规范的一部分,是一个通用的连接工厂。它可以让容器或框架将连接池和事务管理的问题隐藏在应用程序代码之外。作为开发人员,您不需要了解如何连接到数据库的细节。这是设置数据源的管理员的责任。在开发和测试代码时,您很可能会同时担任这两个角色,但您不一定需要了解生产数据源是如何配置的。spring-doc.cadn.net.cn

当您使用 Spring 的 JDBC 层时,可以从 JNDI 获取数据源,或者可以使用第三方提供的连接池实现进行配置。 传统选择是 Apache Commons DBCP 和 C3P0 的 bean 样式 DataSource 类; 对于现代的 JDBC 连接池,建议使用 HikariCP 及其构建器样式 API。spring-doc.cadn.net.cn

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

以下部分使用了 Spring 的 DriverManagerDataSource 实现。 后面会介绍其他几种 DataSource 的变体。spring-doc.cadn.net.cn

要配置一个 DriverManagerDataSourcespring-doc.cadn.net.cn

  1. 以通常获取JDBC连接的方式获取一个连接,其中参数为DriverManagerDataSourcespring-doc.cadn.net.cn

  2. 指定JDBC驱动的全限定类名,以便DriverManager可以加载驱动类。spring-doc.cadn.net.cn

  3. 提供一个在不同JDBC驱动之间变化的URL。(请参阅您的驱动程序的文档以获取正确的值。)spring-doc.cadn.net.cn

  4. 提供一个用户名和密码以连接到数据库。spring-doc.cadn.net.cn

以下示例显示了如何在 Java 中配置一个 DriverManagerDataSourcespring-doc.cadn.net.cn

Java
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("org.hsqldb.jdbcDriver");
dataSource.setUrl("jdbc:hsqldb:hsql://localhost:");
dataSource.setUsername("sa");
dataSource.setPassword("");
Kotlin
val dataSource = DriverManagerDataSource().apply {
    setDriverClassName("org.hsqldb.jdbcDriver")
    url = "jdbc:hsqldb:hsql://localhost:"
    username = "sa"
    password = ""
}

下面的示例显示了相应的 XML 配置:spring-doc.cadn.net.cn

<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 的基本连接和配置。 要了解有助于控制池化功能的更多选项,请参阅相应连接池实现的产品文档。spring-doc.cadn.net.cn

以下示例显示了 DBCP 配置:spring-doc.cadn.net.cn

<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 的配置:spring-doc.cadn.net.cn

<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获取连接并在必要时关闭连接。它支持线程绑定的连接,例如DataSourceTransactionManagerspring-doc.cadn.net.cn

3.4.3. 实现 SmartDataSource

SmartDataSource 接口应由可以提供到关系型数据库连接的类实现。它扩展了 DataSource 接口,以让使用它的类查询在给定操作后是否应关闭连接。当你知道需要重用连接时,这种用法是高效的。spring-doc.cadn.net.cn

3.4.4. 扩展 AbstractDataSource

AbstractDataSource 是 Spring 的 abstract 实现的基类。它实现了所有 DataSource 实现共用的代码。 如果您编写自己的 DataSource 实现,请应继承 AbstractDataSource 类。spring-doc.cadn.net.cn

3.4.5. 使用 SingleConnectionDataSource

SingleConnectionDataSource 类是 SmartDataSource 接口的一个实现,它包装了一个单个的 Connection,在每次使用后不会关闭。 这不支持多线程。spring-doc.cadn.net.cn

如果任何客户端代码在假设使用连接池连接的情况下调用 close(例如使用持久化工具时),您应将 suppressClose 属性设置为 true。此设置会返回一个关闭抑制代理,该代理包装了物理连接。请注意,您不能再将其强制转换为原生的 Oracle Connection 或类似对象。spring-doc.cadn.net.cn

SingleConnectionDataSource 主要是一个测试类。它通常允许在应用服务器外部,结合简单的 JNDI 环境,方便地对代码进行测试。与 DriverManagerDataSource 相比,它始终重复使用同一连接,避免过多地创建物理连接。spring-doc.cadn.net.cn

3.4.6. 使用 DriverManagerDataSource

DriverManagerDataSource 类是标准 DataSource 接口的实现,它通过 bean 属性配置一个普通的 JDBC 驱动程序,并且每次返回一个新 Connectionspring-doc.cadn.net.cn

此实现适用于Java EE容器之外的测试和独立环境,可以作为Spring IoC容器中的DataSource bean,或者与简单的JNDI环境结合使用。假设使用连接池的Connection.close()调用会关闭连接,因此任何DataSource感知的持久化代码都应该有效。然而,在测试环境中,使用JavaBean风格的连接池(如commons-dbcp)非常容易,因此通常更倾向于使用这种连接池而不是DriverManagerDataSourcespring-doc.cadn.net.cn

3.4.7. 使用 TransactionAwareDataSourceProxy

TransactionAwareDataSourceProxy 是对目标 DataSource 的代理。该代理包装该目标 DataSource 以增加对 Spring 管理的事务的了解。在这方面,它类似于由 Java EE 服务器提供的事务性 JNDI DataSourcespring-doc.cadn.net.cn

通常不建议使用此类,除非现有的代码必须调用并传递一个标准的JDBC DataSource 接口实现。在这种情况下,您仍然可以使这段代码可用,并且同时让这段代码参与Spring管理的事务。通常更倾向于使用资源管理的高级抽象来编写您的新代码,例如 JdbcTemplateDataSourceUtils

查看 TransactionAwareDataSourceProxy javadoc 以了解更多信息。spring-doc.cadn.net.cn

3.4.8. 使用 DataSourceTransactionManager

The DataSourceTransactionManager class is a PlatformTransactionManager implementation for single JDBC datasources. It binds a JDBC connection from the specified data source to the currently executing thread, potentially allowing for one thread connection per data source.spring-doc.cadn.net.cn

应用程序代码需要通过DataSourceUtils.getConnection(DataSource)来获取JDBC连接,而不是使用Java EE的标准DataSource.getConnection。它抛出未检查的org.springframework.dao异常,而不是检查的SQLExceptions。所有框架类(如JdbcTemplate)都隐式地使用此策略。如果不与这个事务管理器一起使用,查找策略的行为将与普通的一样。因此,可以在任何情况下使用它。spring-doc.cadn.net.cn

DataSourceTransactionManager 类支持自定义隔离级别和超时时间,这些会在适当的 JDBC 语句查询超时情况下应用。为了支持后者,应用程序代码必须使用 JdbcTemplate 或为每个创建的语句调用 DataSourceUtils.applyTransactionTimeout(..) 方法。spring-doc.cadn.net.cn

您可以在此单资源情况下使用此实现代替JtaTransactionManager,因为它不需要容器支持JTA。在两者之间切换只需进行配置,前提是您遵循所需的连接查找模式。JTA不支持自定义隔离级别。spring-doc.cadn.net.cn

3.5. JDBC 批处理操作

大多数 JDBC 驱动程序在您对同一预编译语句进行多次调用时会提供更好的性能。通过将更新分组为批次,您可以减少与数据库之间的往返次数。spring-doc.cadn.net.cn

3.5.1. 使用 JdbcTemplate 的基本批处理操作

您通过实现特殊接口JdbcTemplate的两个方法,并在您的batchUpdate方法调用中将该实现作为第二个参数传递,来完成BatchPreparedStatementSetter批处理。您可以使用getBatchSize方法来提供当前批处理的大小。您可以使用setValues方法来设置预编译语句的参数值。此方法的调用次数与您在getBatchSize调用中指定的次数相同。下面的示例根据列表中的条目更新t_actor表,并且整个列表用作批处理:spring-doc.cadn.net.cn

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
}
Kotlin
class JdbcActorDao(dataSource: DataSource) : ActorDao {

    private val jdbcTemplate = JdbcTemplate(dataSource)

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

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

    // ... additional methods
}

如果您处理的是更新流或从文件中读取,可能会有首选的批处理大小,但最后一批可能没有那么多条目。在这种情况下,您可以使用 InterruptibleBatchPreparedStatementSetter 接口,它允许在输入源用尽时中断一批处理。isBatchExhausted 方法允许您标记一批处理的结束。spring-doc.cadn.net.cn

3.5.2. 使用对象列表进行批量操作

JdbcTemplateNamedParameterJdbcTemplate 都提供了一种替代方法来提供批处理更新。不需要实现特殊的批处理接口,您可以在调用中作为列表提供所有参数值。框架会遍历这些值,并使用内部的预编译语句设置器。API 会根据您是否使用命名参数而有所不同。对于命名参数,您需要提供一个 SqlParameterSource 的数组,每个批次成员对应一个条目。您可以使用 SqlParameterSourceUtils.createBatch 的便捷方法来创建此数组,传入一个 bean 样式的对象数组(具有与参数对应的 getter 方法)、String 键的 Map 实例(包含相应的参数作为值),或两者的组合。spring-doc.cadn.net.cn

以下示例显示了使用命名参数的批量更新:spring-doc.cadn.net.cn

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
}
Kotlin
class JdbcActorDao(dataSource: DataSource) : ActorDao {

    private val namedParameterJdbcTemplate = NamedParameterJdbcTemplate(dataSource)

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

        // ... additional methods
}

对于使用经典 ? 占位符的 SQL 语句,您需要传入一个包含更新值的对象数组的列表。此对象数组必须为 SQL 语句中的每个占位符提供一个条目,并且它们的顺序必须与 SQL 语句中定义的顺序相同。spring-doc.cadn.net.cn

以下示例与前面的示例相同,只是它使用了经典的 JDBC ? 占位符:spring-doc.cadn.net.cn

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
}
Kotlin
class JdbcActorDao(dataSource: DataSource) : ActorDao {

    private val jdbcTemplate = JdbcTemplate(dataSource)

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

    // ... additional methods
}

我们之前描述的所有批处理更新方法返回一个包含每个批处理条目影响行数的int数组。此计数由JDBC驱动程序报告。如果无法获得计数,JDBC驱动程序将返回-2的值。spring-doc.cadn.net.cn

在这样的情况下,当对底层 PreparedStatement 自动设置值时, 每个值的相应 JDBC 类型需要从给定的 Java 类型推断出来。 虽然这通常可以正常工作,但可能会出现一些问题(例如,包含在 Map 中的 null 值)。 Spring 默认会在这种情况下调用 ParameterMetaData.getParameterType,这可能在你的 JDBC 驱动程序中会很耗性能。 如果你遇到性能问题(如在 Oracle 12c、JBoss 和 PostgreSQL 上报告的),应使用较新的驱动程序版本,并考虑将 spring.jdbc.getParameterType.ignore 属性设置为 true (作为 JVM 系统属性或在类路径根目录的 spring.properties 文件中)。spring-doc.cadn.net.cn

或者,你也可以考虑通过显式指定对应的JDBC类型,例如通过“BatchPreparedStatementSetter”(如前所示),通过给“List<Object[]>”的调用提供显式的类型数组,通过在自定义的“MapSqlParameterSource”实例上调用“registerSqlType”,或者通过“BeanPropertySqlParameterSource”来实现,即使对于null值,它也能从Java声明的属性类型推断出SQL类型。spring-doc.cadn.net.cn

3.5.3. 使用多个批处理的批量操作

前面的批处理更新示例涉及非常大的批次,您希望将它们拆分为几个较小的批次。您可以使用前面提到的方法通过多次调用batchUpdate方法来实现,但现在有一个更方便的方法。该方法除了SQL语句外,还接受一个包含参数的Collection,每个批次要进行的更新次数,以及一个ParameterizedPreparedStatementSetter来设置预编译语句的参数值。框架会遍历提供的值,并将更新调用拆分为指定大小的批次。spring-doc.cadn.net.cn

以下示例显示了一个使用批处理大小为100的批量更新:spring-doc.cadn.net.cn

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
}
Kotlin
class JdbcActorDao(dataSource: DataSource) : ActorDao {

    private val jdbcTemplate = JdbcTemplate(dataSource)

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

    // ... additional methods
}

此调用的批量更新方法返回一个包含数组的数组,每个数组对应一个批次,并包含每个更新的受影响行数数组。 最外层数组的长度表示运行的批次数量,第二层数组的长度表示该批次中的更新数量。每个批次中的更新数量应为所有批次提供的批次大小(最后一个批次可能较少),具体取决于提供的更新对象总数。每个更新语句的更新计数是JDBC驱动程序报告的。如果无法获得计数,JDBC驱动程序将返回值-2spring-doc.cadn.net.cn

3.6. 使用 SimpleJdbc 类简化 JDBC 操作

SimpleJdbcInsertSimpleJdbcCall 类通过利用可以通过 JDBC 驱动程序检索的数据库元数据,提供了简化的配置。 这意味着您一开始需要配置的内容更少,尽管如果您希望在代码中提供所有细节,可以覆盖或关闭元数据处理。spring-doc.cadn.net.cn

3.6.1. 使用 SimpleJdbcInsert 插入数据

我们首先查看具有最少配置选项的 SimpleJdbcInsert 类。您应该在数据访问层的初始化方法中实例化 SimpleJdbcInsert。对于此示例,初始化方法是 setDataSource 方法。您不需要继承 SimpleJdbcInsert 类。相反,您可以创建一个新实例并通过使用 withTableName 方法设置表名。此类的配置方法遵循返回 SimpleJdbcInsert 实例的 fluid 风格,这使您可以链接所有配置方法。下面的示例仅使用了一个配置方法(我们稍后会展示多个方法的示例):spring-doc.cadn.net.cn

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
}
Kotlin
class JdbcActorDao(dataSource: DataSource) : ActorDao {

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

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

    // ... additional methods
}

此处使用的 execute 方法仅以一个普通的 java.util.Map 作为参数。需要注意的重要一点是,用于 Map 的键必须与数据库中定义的表的列名匹配。这是因为我们读取元数据来构造实际的插入语句。spring-doc.cadn.net.cn

3.6.2. 使用 SimpleJdbcInsert 检索自动生成的键

下一个示例使用与前一个示例相同的插入操作,但不是传入id,而是检索自动生成的键并将其设置在新的Actor对象上。当它创建SimpleJdbcInsert时,除了指定表名外,还通过usingGeneratedKeyColumns方法指定了生成的键列的名称。下面的列表显示了其工作方式:spring-doc.cadn.net.cn

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
}
Kotlin
class JdbcActorDao(dataSource: DataSource) : ActorDao {

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

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

    // ... additional methods
}

使用此第二种方法执行插入操作时的主要区别在于,您不会将 id 添加到 Map,而是调用 executeAndReturnKey 方法。这将返回一个 java.lang.Number 对象,您可以使用它来创建您的领域类中使用的数值类型的实例。您不能依赖所有数据库在此处返回特定的 Java 类。java.lang.Number 是您可以依赖的基类。如果您有多个自动生成的列或生成的值是非数值的,可以使用从 executeAndReturnKeyHolder 方法返回的 KeyHolderspring-doc.cadn.net.cn

3.6.3. 为 SimpleJdbcInsert 指定列

您可以使用usingColumns方法通过指定列名列表来限制插入的列,如下例所示:spring-doc.cadn.net.cn

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
}
Kotlin
class JdbcActorDao(dataSource: DataSource) : ActorDao {

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

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

    // ... additional methods
}

插入的执行方式与你依赖元数据来确定使用哪些列的方式相同。spring-doc.cadn.net.cn

3.6.4. 使用 SqlParameterSource 提供参数值

使用 Map 来提供参数值可以正常工作,但并不是最方便的类。Spring 提供了 SqlParameterSource 接口的一些实现类,您可以改用这些类。第一个是 BeanPropertySqlParameterSource,如果您有一个符合 JavaBean 规范的类来存储您的值,这个类非常方便。它使用相应的 getter 方法来提取参数值。下面的示例展示了如何使用 BeanPropertySqlParameterSourcespring-doc.cadn.net.cn

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
}
Kotlin
class JdbcActorDao(dataSource: DataSource) : ActorDao {

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

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

    // ... additional methods
}

另一个选项是MapSqlParameterSource,它类似于Map,但提供了一个更方便的addValue方法,可以进行链式调用。下面的例子展示了如何使用它:spring-doc.cadn.net.cn

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
}
Kotlin
class JdbcActorDao(dataSource: DataSource) : ActorDao {

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

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

    // ... additional methods
}

正如你所看到的,配置是相同的。只有执行的代码需要更改以使用这些替代输入类。spring-doc.cadn.net.cn

3.6.5. 使用 SimpleJdbcCall 调用存储过程

SimpleJdbcCall 类使用数据库中的元数据来查找 inout 参数的名称,这样您就不必显式声明它们。如果您更喜欢这样做,或者如果您有参数(例如 ARRAYSTRUCT)无法自动映射到 Java 类,您可以声明参数。第一个示例显示了一个从 MySQL 数据库中仅以 VARCHARDATE 格式返回标量值的简单存储过程。该示例存储过程读取指定的演员条目,并以 out 参数的形式返回 first_namelast_namebirth_date 列。 以下列表显示了第一个示例:spring-doc.cadn.net.cn

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 参数包含您要查找的参与者idout 参数返回从表中读取的数据。spring-doc.cadn.net.cn

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

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
}
Kotlin
class JdbcActorDao(dataSource: DataSource) : ActorDao {

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


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

        // ... additional methods
}

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

execute 方法接受 IN 参数,并返回一个包含任何 Mapout,该参数通过名称进行键控,如存储过程所指定的。在这种情况下,它们是 out_first_nameout_last_nameout_birth_datespring-doc.cadn.net.cn

execute 方法的最后部分创建了一个 Actor 实例,用于返回检索到的数据。同样,使用存储过程中的 out 参数名称非常重要。此外,结果映射中存储的 out 参数名称的大小写与数据库中的 out 参数名称的大小写匹配,这在不同数据库之间可能会有所不同。为了使您的代码更具可移植性,您应该进行不区分大小写的查找,或者指示 Spring 使用 LinkedCaseInsensitiveMap。要实现后者,您可以创建自己的 JdbcTemplate 并将 setResultsMapCaseInsensitive 属性设置为 true。然后可以将此自定义的 JdbcTemplate 实例传递给您的 SimpleJdbcCall 的构造函数。下面的示例显示了此配置:spring-doc.cadn.net.cn

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
}
Kotlin
class JdbcActorDao(dataSource: DataSource) : ActorDao {

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

    // ... additional methods
}

通过执行此操作,您可以避免在使用返回的out参数名称时出现冲突。spring-doc.cadn.net.cn

3.6.6. 为 SimpleJdbcCall 显式声明参数

在本章前面,我们描述了如何从元数据中推断参数,但如果您希望,可以显式声明它们。您可以通过创建并配置SimpleJdbcCall,并使用declareParameters方法,该方法接受可变数量的SqlParameter对象作为输入。有关如何定义SqlParameter的详细信息,请参阅< a t="C4">下一节。spring-doc.cadn.net.cn

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

您可以选择显式声明一个、一些或所有参数。在您没有显式声明参数的地方,仍会使用参数元数据。要绕过对潜在参数的元数据查找处理,并仅使用已声明的参数,可以在声明过程中调用方法 withoutProcedureColumnMetaDataAccess。假设您为数据库函数声明了两个或多个不同的调用签名。在这种情况下,您调用 useInParameterNames 来指定给定签名中要包含的 IN 参数名称列表。spring-doc.cadn.net.cn

以下示例显示了一个完全声明的存储过程调用,并使用了前面示例中的信息:spring-doc.cadn.net.cn

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
}
Kotlin
class JdbcActorDao(dataSource: DataSource) : ActorDao {

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

        // ... additional methods
}

这两个示例的执行和最终结果是相同的。第二个示例明确指定了所有细节,而不是依赖元数据。spring-doc.cadn.net.cn

3.6.7. 如何定义 SqlParameters

要为 SimpleJdbc 类和 RDBMS 操作类(在 将 JDBC 操作建模为 Java 对象 中介绍)定义参数,可以使用 SqlParameter 或其某个子类。 要做到这一点,通常需要在构造函数中指定参数名称和 SQL 类型。SQL 类型通过使用 java.sql.Types 常量来指定。在本章之前,我们看到过类似以下的声明:spring-doc.cadn.net.cn

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

第一行使用 SqlParameter 声明了一个 IN 参数。您可以使用 IN 参数来调用存储过程和查询,方法是使用 SqlQuery 及其子类(在 了解 SqlQuery 中有介绍)。spring-doc.cadn.net.cn

第二行(带有SqlOutParameter)声明了一个out参数,用于存储过程调用。还有一个SqlInOutParameter用于InOut参数(为过程提供IN值并返回值的参数)。spring-doc.cadn.net.cn

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

对于 IN 参数,除了名称和 SQL 类型外,还可以为数值数据指定一个精度,或为自定义数据库类型指定一个类型名称。对于 out 参数,您可以提供一个 RowMapper 来处理从 REF 光标返回的行映射。另一个选项是指定一个 SqlReturnType,这提供了定义返回值自定义处理方式的机会。spring-doc.cadn.net.cn

3.6.8. 通过使用 SimpleJdbcCall 调用存储的函数

您可以几乎以与调用存储过程相同的方式调用存储函数,只是您提供的是函数名称而不是过程名称。您使用withFunctionName方法作为配置的一部分,以表明您希望调用一个函数,并生成相应的函数调用字符串。专用调用(executeFunction)用于运行函数,并将函数返回值作为指定类型的对象返回,这意味着您不必从结果映射中检索返回值。对于只有一个out参数的存储过程,还提供了类似的便捷方法(命名为executeObject)。以下示例(针对MySQL)基于名为get_actor_name的存储函数,该函数返回演员的全名:spring-doc.cadn.net.cn

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, 如下例所示:spring-doc.cadn.net.cn

Java
public class JdbcActorDao implements ActorDao {

    private JdbcTemplate jdbcTemplate;
    private SimpleJdbcCall funcGetActorName;

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

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

    // ... additional methods
}
Kotlin
class JdbcActorDao(dataSource: DataSource) : ActorDao {

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

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

    // ... additional methods
}

executeFunction 方法返回一个 String,其中包含函数调用的返回值。spring-doc.cadn.net.cn

3.6.9. 从 SimpleJdbcCall 返回 ResultSet 或 REF 游标

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

下一个示例(针对MySQL)使用了一个不带IN参数的存储过程,并从t_actor表中返回所有行:spring-doc.cadn.net.cn

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 规则,您可以使用通过在 newInstance 方法中传入所需的映射类而创建的 BeanPropertyRowMapper。 以下示例显示了如何操作:spring-doc.cadn.net.cn

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
}
Kotlin
class JdbcActorDao(dataSource: DataSource) : ActorDao {

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

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

    // ... additional methods
}

execute 调用传入一个空的 Map,因为此调用不接受任何参数。 然后从结果映射中检索演员列表并返回给调用者。spring-doc.cadn.net.cn

3.7. 将JDBC操作建模为Java对象

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

许多Spring开发人员认为,下面描述的各种RDBMS操作类(除< a t="C3">StoredProcedure类外)通常可以用直接的JdbcTemplate调用来替代。通常,编写一个调用JdbcTemplate上方法的DAO方法会更简单(而不是将查询封装成一个完整的类)。spring-doc.cadn.net.cn

但是,如果你在使用RDBMS操作类时获得了可衡量的价值,你应该继续使用这些类。spring-doc.cadn.net.cn

3.7.1. 理解 SqlQuery

SqlQuery 是一个可重用的、线程安全的类,用于封装 SQL 查询。子类必须实现 newRowMapper(..) 方法,以提供一个可以创建一个对象的 RowMapper 实例,该对象来自查询执行期间创建的 ResultSet 的迭代结果。 SqlQuery 类很少被直接使用,因为 MappingSqlQuery 子类为将行映射到 Java 类提供了更方便的实现。其他继承 SqlQuery 的实现包括 MappingSqlQueryWithParametersUpdatableSqlQueryspring-doc.cadn.net.cn

3.7.2. 使用 MappingSqlQuery

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

Java
public class ActorMappingQuery extends MappingSqlQuery<Actor> {

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

    @Override
    protected Actor mapRow(ResultSet rs, int rowNumber) throws SQLException {
        Actor actor = new Actor();
        actor.setId(rs.getLong("id"));
        actor.setFirstName(rs.getString("first_name"));
        actor.setLastName(rs.getString("last_name"));
        return actor;
    }
}
Kotlin
class ActorMappingQuery(ds: DataSource) : MappingSqlQuery<Actor>(ds, "select id, first_name, last_name from t_actor where id = ?") {

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

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

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

Java
private ActorMappingQuery actorMappingQuery;

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

public Customer getCustomer(Long id) {
    return actorMappingQuery.findObject(id);
}
Kotlin
private val actorMappingQuery = ActorMappingQuery(dataSource)

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

前面示例中的方法检索传递为唯一参数的id的客户。由于我们只希望返回一个对象,因此调用带有findObject作为参数的id便捷方法。如果我们有一个返回对象列表并接受其他参数的查询,我们会使用其中一个execute方法,该方法以传递的可变参数数组作为参数。下面的示例显示了这样的方法:spring-doc.cadn.net.cn

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

3.7.3. 使用 SqlUpdate

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

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

public class UpdateCreditRating extends SqlUpdate {

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

    /**
     * @param id for the Customer to be updated
     * @param rating the new value for credit rating
     * @return number of rows updated
     */
    public int execute(int id, int rating) {
        return update(rating, id);
    }
}
Kotlin
import java.sql.Types
import javax.sql.DataSource
import org.springframework.jdbc.core.SqlParameter
import org.springframework.jdbc.object.SqlUpdate

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

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

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

3.7.4. 使用 StoredProcedure

StoredProcedure 类是 abstract 的超类,用于 RDBMS 存储过程的对象抽象。spring-doc.cadn.net.cn

继承的 sql 属性是在RDBMS中的存储过程的名称。spring-doc.cadn.net.cn

要为 StoredProcedure 类定义一个参数,可以使用 SqlParameter 或其子类之一。您必须在构造函数中指定参数名称和 SQL 类型,如下代码片段所示:spring-doc.cadn.net.cn

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

使用 java.sql.Types 常量指定 SQL 类型。spring-doc.cadn.net.cn

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

第二行(带有SqlOutParameter)声明了一个out参数,用于存储过程调用。还有一个SqlInOutParameter用于InOut参数(提供in值到过程并返回一个值的参数)。spring-doc.cadn.net.cn

对于 in 个参数,除了名称和 SQL 类型外,还可以为数值数据指定一个精度,或为自定义数据库类型指定一个类型名称。对于 out 个参数,您可以提供一个 RowMapper 来处理从 REF 光标返回的行映射。另一个选项是指定一个 SqlReturnType,它允许您定义返回值的自定义处理方式。spring-doc.cadn.net.cn

下一个简单的DAO示例使用StoredProcedure来调用一个函数(sysdate()),该函数随任何Oracle数据库一起提供。要使用存储过程功能,您必须创建一个继承StoredProcedure的类。在此示例中,StoredProcedure类是一个内部类。但是,如果您需要重用StoredProcedure,可以将其声明为顶级类。此示例没有输入参数,但通过使用SqlOutParameter类将输出参数声明为日期类型。execute()方法运行该过程并从结果Map中提取返回的日期。结果Map中每个声明的输出参数(在这种情况下只有一个)都有一项条目,使用参数名称作为键。 以下列表显示了我们的自定义StoredProcedure类:spring-doc.cadn.net.cn

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

public class StoredProcedureDao {

    private GetSysdateProcedure getSysdate;

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

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

    private class GetSysdateProcedure extends StoredProcedure {

        private static final String SQL = "sysdate";

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

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

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

class StoredProcedureDao(dataSource: DataSource) {

    private val SQL = "sysdate"

    private val getSysdate = GetSysdateProcedure(dataSource)

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

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

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

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

以下是一个 StoredProcedure 的示例,包含两个输出参数(在这种情况下,是 Oracle REF 游标):spring-doc.cadn.net.cn

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

public class TitlesAndGenresStoredProcedure extends StoredProcedure {

    private static final String SPROC_NAME = "AllTitlesAndGenres";

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

    public Map<String, Object> execute() {
        // again, this sproc has no input parameters, so an empty Map is supplied
        return super.execute(new HashMap<String, Object>());
    }
}
Kotlin
import java.util.HashMap
import javax.sql.DataSource
import oracle.jdbc.OracleTypes
import org.springframework.jdbc.core.SqlOutParameter
import org.springframework.jdbc.object.StoredProcedure

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

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

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

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

注意如何将已在 TitlesAndGenresStoredProcedure 构造函数中使用的 declareParameter(..) 方法的重载版本传递给 RowMapper 实现实例。这是一种非常方便且强大的方式,可以复用现有功能。接下来的两个示例提供了两个 RowMapper 实现的代码。spring-doc.cadn.net.cn

The TitleMapper class maps a ResultSet to a Title domain object for each row in the supplied ResultSet, as follows:spring-doc.cadn.net.cn

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

public final class TitleMapper implements RowMapper<Title> {

    public Title mapRow(ResultSet rs, int rowNum) throws SQLException {
        Title title = new Title();
        title.setId(rs.getLong("id"));
        title.setName(rs.getString("name"));
        return title;
    }
}
Kotlin
import java.sql.ResultSet
import com.foo.domain.Title
import org.springframework.jdbc.core.RowMapper

class TitleMapper : RowMapper<Title> {

    override fun mapRow(rs: ResultSet, rowNum: Int) =
            Title(rs.getLong("id"), rs.getString("name"))
}

The GenreMapper class maps a ResultSet to a Genre domain object for each row in the supplied ResultSet, as follows:spring-doc.cadn.net.cn

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

public final class GenreMapper implements RowMapper<Genre> {

    public Genre mapRow(ResultSet rs, int rowNum) throws SQLException {
        return new Genre(rs.getString("name"));
    }
}
Kotlin
import java.sql.ResultSet
import com.foo.domain.Genre
import org.springframework.jdbc.core.RowMapper

class GenreMapper : RowMapper<Genre> {

    override fun mapRow(rs: ResultSet, rowNum: Int): Genre {
        return Genre(rs.getString("name"))
    }
}

要向在关系数据库管理系统(RDBMS)中定义了一个或多个输入参数的存储过程传递参数,可以编写一个强类型 execute(..) 方法,该方法会将调用委托给父类中的非类型化 execute(Map) 方法,如下例所示:spring-doc.cadn.net.cn

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

public class TitlesAfterDateStoredProcedure extends StoredProcedure {

    private static final String SPROC_NAME = "TitlesAfterDate";
    private static final String CUTOFF_DATE_PARAM = "cutoffDate";

    public TitlesAfterDateStoredProcedure(DataSource dataSource) {
        super(dataSource, SPROC_NAME);
        declareParameter(new SqlParameter(CUTOFF_DATE_PARAM, Types.DATE);
        declareParameter(new SqlOutParameter("titles", OracleTypes.CURSOR, new TitleMapper()));
        compile();
    }

    public Map<String, Object> execute(Date cutoffDate) {
        Map<String, Object> inputs = new HashMap<String, Object>();
        inputs.put(CUTOFF_DATE_PARAM, cutoffDate);
        return super.execute(inputs);
    }
}
Kotlin
import java.sql.Types
import java.util.Date
import javax.sql.DataSource
import oracle.jdbc.OracleTypes
import org.springframework.jdbc.core.SqlOutParameter
import org.springframework.jdbc.core.SqlParameter
import org.springframework.jdbc.object.StoredProcedure

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

    companion object {
        private const val SPROC_NAME = "TitlesAfterDate"
        private const val CUTOFF_DATE_PARAM = "cutoffDate"
    }

    init {
        declareParameter(SqlParameter(CUTOFF_DATE_PARAM, Types.DATE))
        declareParameter(SqlOutParameter("titles", OracleTypes.CURSOR, TitleMapper()))
        compile()
    }

    fun execute(cutoffDate: Date) = super.execute(
            mapOf<String, Any>(CUTOFF_DATE_PARAM to cutoffDate))
}

3.8. 参数和数据值处理的常见问题

Spring框架的JDBC支持提供的不同方法中,参数和数据值的常见问题存在。本节将介绍如何解决这些问题。spring-doc.cadn.net.cn

3.8.1. 为参数提供SQL类型信息

通常,Spring 会根据传入的参数类型来确定参数的 SQL 类型。可以在设置参数值时显式提供要使用的 SQL 类型。在正确设置 NULL 值时,这有时是必要的。spring-doc.cadn.net.cn

你可以通过几种方式提供SQL类型信息:spring-doc.cadn.net.cn

  • Many update and query methods of the JdbcTemplate take an additional parameter in the form of an int array. This array is used to indicate the SQL type of the corresponding parameter by using constant values from the java.sql.Types class. Provide one entry for each parameter.spring-doc.cadn.net.cn

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

  • 对于使用命名参数的方法,可以使用 SqlParameterSource 类, BeanPropertySqlParameterSourceMapSqlParameterSource。它们都有方法 用于为任何命名参数值注册 SQL 类型。spring-doc.cadn.net.cn

3.8.2. 处理 BLOB 和 CLOB 对象

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

LobCreatorLobHandler 为 LOB 输入和输出提供以下支持:spring-doc.cadn.net.cn

下一个示例展示了如何创建并插入一个 BLOB。稍后我们将展示如何从数据库中读取它。spring-doc.cadn.net.cn

此示例使用 JdbcTemplateAbstractLobCreatingPreparedStatementCallback 的实现。它实现了一个方法,setValues。该方法提供一个 LobCreator,我们用它来设置 SQL 插入语句中的 LOB 列的值。spring-doc.cadn.net.cn

在此示例中,我们假设有一个变量 lobHandler,它已经被设置为 DefaultLobHandler 的实例。您通常通过依赖注入来设置此值。spring-doc.cadn.net.cn

以下示例展示了如何创建并插入 BLOB:spring-doc.cadn.net.cn

Java
final File blobIn = new File("spring2004.jpg");
final InputStream blobIs = new FileInputStream(blobIn);
final File clobIn = new File("large.txt");
final InputStream clobIs = new FileInputStream(clobIn);
final InputStreamReader clobReader = new InputStreamReader(clobIs);

jdbcTemplate.execute(
    "INSERT INTO lob_table (id, a_clob, a_blob) VALUES (?, ?, ?)",
    new AbstractLobCreatingPreparedStatementCallback(lobHandler) {  (1)
        protected void setValues(PreparedStatement ps, LobCreator lobCreator) throws SQLException {
            ps.setLong(1, 1L);
            lobCreator.setClobAsCharacterStream(ps, 2, clobReader, (int)clobIn.length());  (2)
            lobCreator.setBlobAsBinaryStream(ps, 3, blobIs, (int)blobIn.length());  (3)
        }
    }
);

blobIs.close();
clobReader.close();
1 传入 lobHandler(在本例中)是一个普通的 DefaultLobHandler
2 使用方法 setClobAsCharacterStream 来传入 CLOB 的内容。
3 使用方法 setBlobAsBinaryStream 来传入 BLOB 的内容。
Kotlin
val blobIn = File("spring2004.jpg")
val blobIs = FileInputStream(blobIn)
val clobIn = File("large.txt")
val clobIs = FileInputStream(clobIn)
val clobReader = InputStreamReader(clobIs)

jdbcTemplate.execute(
        "INSERT INTO lob_table (id, a_clob, a_blob) VALUES (?, ?, ?)",
        object: AbstractLobCreatingPreparedStatementCallback(lobHandler) {  (1)
            override fun setValues(ps: PreparedStatement, lobCreator: LobCreator) {
                ps.setLong(1, 1L)
                lobCreator.setClobAsCharacterStream(ps, 2, clobReader, clobIn.length().toInt())  (2)
                lobCreator.setBlobAsBinaryStream(ps, 3, blobIs, blobIn.length().toInt())  (3)
            }
        }
)
blobIs.close()
clobReader.close()
1 传入 lobHandler(在本例中)是一个普通的 DefaultLobHandler
2 使用方法 setClobAsCharacterStream 来传入 CLOB 的内容。
3 使用方法 setBlobAsBinaryStream 来传入 BLOB 的内容。

如果您在setBlobAsBinaryStreamsetClobAsAsciiStreamsetClobAsCharacterStream方法上调用,该方法返回的LobCreator来自DefaultLobHandler.getLobCreator(),您可以选择为contentLength参数指定一个负值。如果指定的内容长度为负数,DefaultLobHandler将使用没有长度参数的JDBC 4.0变体的set-stream方法。否则,它会将指定的长度传递给驱动程序。spring-doc.cadn.net.cn

查看你使用的JDBC驱动程序的文档,以确认它是否支持在不提供内容长度的情况下流式传输LOB。spring-doc.cadn.net.cn

现在是时候从数据库中读取LOB数据了。同样,你使用一个JdbcTemplate,相同的实例变量lobHandler以及对DefaultLobHandler的引用。 下面的例子展示了如何操作:spring-doc.cadn.net.cn

Java
List<Map<String, Object>> l = jdbcTemplate.query("select id, a_clob, a_blob from lob_table",
    new RowMapper<Map<String, Object>>() {
        public Map<String, Object> mapRow(ResultSet rs, int i) throws SQLException {
            Map<String, Object> results = new HashMap<String, Object>();
            String clobText = lobHandler.getClobAsString(rs, "a_clob");  (1)
            results.put("CLOB", clobText);
            byte[] blobBytes = lobHandler.getBlobAsBytes(rs, "a_blob");  (2)
            results.put("BLOB", blobBytes);
            return results;
        }
    });
1 使用方法 getClobAsString 来检索 CLOB 的内容。
2 使用方法 getBlobAsBytes 来检索 BLOB 的内容。
Kotlin
val l = jdbcTemplate.query("select id, a_clob, a_blob from lob_table") { rs, _ ->
    val clobText = lobHandler.getClobAsString(rs, "a_clob")  (1)
    val blobBytes = lobHandler.getBlobAsBytes(rs, "a_blob")  (2)
    mapOf("CLOB" to clobText, "BLOB" to blobBytes)
}
1 使用方法 getClobAsString 来检索 CLOB 的内容。
2 使用方法 getBlobAsBytes 来检索 BLOB 的内容。

3.8.3. 为 IN 子句传递值列表

SQL 标准允许根据包含可变数量值的表达式选择行。一个典型示例是 select * from T_ACTOR where id in (1, 2, 3)。JDBC 标准不直接支持为预编译语句提供可变数量的占位符。您不能声明可变数量的占位符。您需要准备具有所需占位符数量的不同变体,或者在知道需要多少占位符后动态生成 SQL 字符串。NamedParameterJdbcTemplateJdbcTemplate 中提供的命名参数支持采用后一种方法。您可以将值作为 java.util.List 个原始对象的列表传入。此列表用于插入所需的占位符,并在语句执行时传入这些值。spring-doc.cadn.net.cn

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

除了值列表中的基本值外,您还可以创建一个 java.util.List 对象数组。此列表可以支持为 in 子句定义多个表达式,例如 select * from T_ACTOR where (id, last_name) in ((1, 'Johnson'), (2, 'Harrop'\))。当然,这要求您的数据库支持此语法。spring-doc.cadn.net.cn

3.8.4. 处理存储过程调用的复杂类型

当调用存储过程时,有时可以使用数据库特有的复杂类型。为了处理这些类型,Spring 提供了 SqlReturnType 用于处理从存储过程调用返回的类型,以及 SqlTypeValue 用于作为参数传递给存储过程的类型。spring-doc.cadn.net.cn

SqlReturnType 接口有一个必须实现的方法(名为 getTypeValue)。此接口用于声明 SqlOutParameter 的一部分。以下示例显示了如何返回用户声明类型 ITEM_TYPE 的 Oracle STRUCT 对象的值:spring-doc.cadn.net.cn

Java
public class TestItemStoredProcedure extends StoredProcedure {

    public TestItemStoredProcedure(DataSource dataSource) {
        // ...
        declareParameter(new SqlOutParameter("item", OracleTypes.STRUCT, "ITEM_TYPE",
            (CallableStatement cs, int colIndx, int sqlType, String typeName) -> {
                STRUCT struct = (STRUCT) cs.getObject(colIndx);
                Object[] attr = struct.getAttributes();
                TestItem item = new TestItem();
                item.setId(((Number) attr[0]).longValue());
                item.setDescription((String) attr[1]);
                item.setExpirationDate((java.util.Date) attr[2]);
                return item;
            }));
        // ...
    }
Kotlin
class TestItemStoredProcedure(dataSource: DataSource) : StoredProcedure() {

    init {
        // ...
        declareParameter(SqlOutParameter("item", OracleTypes.STRUCT, "ITEM_TYPE") { cs, colIndx, sqlType, typeName ->
            val struct = cs.getObject(colIndx) as STRUCT
            val attr = struct.getAttributes()
            TestItem((attr[0] as Long, attr[1] as String, attr[2] as Date)
        })
        // ...
    }
}

您可以使用 SqlTypeValue 将 Java 对象(例如 TestItem)的值传递给存储过程。 SqlTypeValue 接口有一个方法(名为 createTypeValue),您必须实现该方法。 活动连接被传入,您可以使用它来创建特定于数据库的对象,例如 StructDescriptor 实例或 ArrayDescriptor 实例。 下面的示例创建了一个 StructDescriptor 实例:spring-doc.cadn.net.cn

Java
final TestItem testItem = new TestItem(123L, "A test item",
        new SimpleDateFormat("yyyy-M-d").parse("2010-12-31"));

SqlTypeValue value = new AbstractSqlTypeValue() {
    protected Object createTypeValue(Connection conn, int sqlType, String typeName) throws SQLException {
        StructDescriptor itemDescriptor = new StructDescriptor(typeName, conn);
        Struct item = new STRUCT(itemDescriptor, conn,
        new Object[] {
            testItem.getId(),
            testItem.getDescription(),
            new java.sql.Date(testItem.getExpirationDate().getTime())
        });
        return item;
    }
};
Kotlin
val (id, description, expirationDate) = TestItem(123L, "A test item",
        SimpleDateFormat("yyyy-M-d").parse("2010-12-31"))

val value = object : AbstractSqlTypeValue() {
    override fun createTypeValue(conn: Connection, sqlType: Int, typeName: String?): Any {
        val itemDescriptor = StructDescriptor(typeName, conn)
        return STRUCT(itemDescriptor, conn,
                arrayOf(id, description, java.sql.Date(expirationDate.time)))
    }
}

现在可以将这个 SqlTypeValue 添加到包含存储过程的 Map 调用输入参数的 execute 中。spring-doc.cadn.net.cn

SqlTypeValue 的另一种用途是将值数组传递给 Oracle 存储过程。Oracle 有其自己的内部 ARRAY 类,在这种情况下必须使用该类,你可以使用 SqlTypeValue 创建 Oracle ARRAY 的实例,并用 Java ARRAY 中的值填充它,如下例所示:spring-doc.cadn.net.cn

Java
final Long[] ids = new Long[] {1L, 2L};

SqlTypeValue value = new AbstractSqlTypeValue() {
    protected Object createTypeValue(Connection conn, int sqlType, String typeName) throws SQLException {
        ArrayDescriptor arrayDescriptor = new ArrayDescriptor(typeName, conn);
        ARRAY idArray = new ARRAY(arrayDescriptor, conn, ids);
        return idArray;
    }
};
Kotlin
class TestItemStoredProcedure(dataSource: DataSource) : StoredProcedure() {

    init {
        val ids = arrayOf(1L, 2L)
        val value = object : AbstractSqlTypeValue() {
            override fun createTypeValue(conn: Connection, sqlType: Int, typeName: String?): Any {
                val arrayDescriptor = ArrayDescriptor(typeName, conn)
                return ARRAY(arrayDescriptor, conn, ids)
            }
        }
    }
}

3.9. 嵌入式数据库支持

The org.springframework.jdbc.datasource.embedded package provides support for embedded Java database engines. Support for HSQL, H2, and Derby is provided natively. You can also use an extensible API to plug in new embedded database types and DataSource implementations.spring-doc.cadn.net.cn

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

嵌入式数据库在项目开发阶段很有用,因为它具有轻量级的特点。其优点包括易于配置、启动速度快、易于测试,以及在开发过程中能够快速演变你的 SQL 语句。spring-doc.cadn.net.cn

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

如果您希望将嵌入式数据库实例作为Spring中的bean公开, ApplicationContext,可以使用embedded-database标签中的spring-jdbc命名空间:spring-doc.cadn.net.cn

<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.sqltest-data.sql资源中填充SQL。此外,作为最佳实践,嵌入式数据库被赋予了一个唯一生成的名称。嵌入式数据库作为类型为javax.sql.DataSource的bean提供给Spring容器,然后可以根据需要注入到数据访问对象中。spring-doc.cadn.net.cn

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

EmbeddedDatabaseBuilder 类提供了一个用于以编程方式构建嵌入式数据库的流畅 API。当您需要在独立环境或独立集成测试中创建嵌入式数据库时,可以使用此功能,如下例所示:spring-doc.cadn.net.cn

Java
EmbeddedDatabase db = new EmbeddedDatabaseBuilder()
        .generateUniqueName(true)
        .setType(H2)
        .setScriptEncoding("UTF-8")
        .ignoreFailedDrops(true)
        .addScript("schema.sql")
        .addScripts("user_data.sql", "country_data.sql")
        .build();

// perform actions against the db (EmbeddedDatabase extends javax.sql.DataSource)

db.shutdown()
Kotlin
val db = EmbeddedDatabaseBuilder()
        .generateUniqueName(true)
        .setType(H2)
        .setScriptEncoding("UTF-8")
        .ignoreFailedDrops(true)
        .addScript("schema.sql")
        .addScripts("user_data.sql", "country_data.sql")
        .build()

// perform actions against the db (EmbeddedDatabase extends javax.sql.DataSource)

db.shutdown()

查看 javadoc for EmbeddedDatabaseBuilder 以获取所有支持选项的更多详细信息。spring-doc.cadn.net.cn

您也可以使用 EmbeddedDatabaseBuilder 通过使用Java配置创建嵌入式数据库,如下例所示:spring-doc.cadn.net.cn

Java
@Configuration
public class DataSourceConfig {

    @Bean
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
                .generateUniqueName(true)
                .setType(H2)
                .setScriptEncoding("UTF-8")
                .ignoreFailedDrops(true)
                .addScript("schema.sql")
                .addScripts("user_data.sql", "country_data.sql")
                .build();
    }
}
Kotlin
@Configuration
class DataSourceConfig {

    @Bean
    fun dataSource(): DataSource {
        return EmbeddedDatabaseBuilder()
                .generateUniqueName(true)
                .setType(H2)
                .setScriptEncoding("UTF-8")
                .ignoreFailedDrops(true)
                .addScript("schema.sql")
                .addScripts("user_data.sql", "country_data.sql")
                .build()
    }
}

3.9.4. 选择嵌入式数据库类型

本节内容介绍如何选择Spring支持的三种嵌入式数据库之一。它包括以下主题:spring-doc.cadn.net.cn

使用HSQL

Spring 支持 HSQL 1.8.0 及以上版本。如果未明确指定类型,HSQL 是默认的嵌入式数据库。要显式指定 HSQL,请将 type 标签的 embedded-database 属性设置为 HSQL。如果您使用构建器 API,请使用 setType(EmbeddedDatabaseType) 方法并传入 EmbeddedDatabaseType.HSQLspring-doc.cadn.net.cn

使用 H2

Spring 支持 H2 数据库。要启用 H2,请将 type 标签的 embedded-database 属性设置为 H2。如果您使用构建器 API,请使用 setType(EmbeddedDatabaseType) 方法并传入 EmbeddedDatabaseType.H2spring-doc.cadn.net.cn

使用 Derby

Spring 支持 Apache Derby 10.5 及以上版本。要启用 Derby,请将 type 标签的 embedded-database 属性设置为 DERBY。如果您使用构建器 API, 请调用带有 EmbeddedDatabaseType.DERBYsetType(EmbeddedDatabaseType) 方法。spring-doc.cadn.net.cn

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

嵌入式数据库为测试数据访问代码提供了一种轻量级的方法。下一个示例是一个使用嵌入式数据库的数据访问集成测试模板。在不需要跨测试类重用嵌入式数据库的情况下,使用这样的模板对于一次性任务可能很有用。然而,如果您希望在测试套件内创建一个共享的嵌入式数据库,请考虑使用Spring TestContext Framework并按照使用Spring XML创建嵌入式数据库以编程方式创建嵌入式数据库中所述,在Spring ApplicationContext中将嵌入式数据库配置为一个bean。下面的列表显示了测试模板:spring-doc.cadn.net.cn

Java
public class DataAccessIntegrationTestTemplate {

    private EmbeddedDatabase db;

    @BeforeEach
    public void setUp() {
        // creates an HSQL in-memory database populated from default scripts
        // classpath:schema.sql and classpath:data.sql
        db = new EmbeddedDatabaseBuilder()
                .generateUniqueName(true)
                .addDefaultScripts()
                .build();
    }

    @Test
    public void testDataAccess() {
        JdbcTemplate template = new JdbcTemplate(db);
        template.query( /* ... */ );
    }

    @AfterEach
    public void tearDown() {
        db.shutdown();
    }

}
Kotlin
class DataAccessIntegrationTestTemplate {

    private lateinit var db: EmbeddedDatabase

    @BeforeEach
    fun setUp() {
        // creates an HSQL in-memory database populated from default scripts
        // classpath:schema.sql and classpath:data.sql
        db = EmbeddedDatabaseBuilder()
                .generateUniqueName(true)
                .addDefaultScripts()
                .build()
    }

    @Test
    fun testDataAccess() {
        val template = JdbcTemplate(db)
        template.query( /* ... */)
    }

    @AfterEach
    fun tearDown() {
        db.shutdown()
    }
}

3.9.6. 为嵌入式数据库生成唯一名称

开发团队在测试套件意外尝试重新创建同一数据库的额外实例时,经常会遇到嵌入式数据库的错误。如果 XML 配置文件或 @Configuration 类负责创建嵌入式数据库,并且相应的配置在同一个测试套件(即同一 JVM 进程)内的多个测试场景中被重复使用,就很容易发生这种情况 — 例如,针对嵌入式数据库的集成测试,其 ApplicationContext 配置仅在激活的 bean 定义配置文件上有所不同。spring-doc.cadn.net.cn

此类错误的根本原因是,Spring 的 EmbeddedDatabaseFactory(由 <jdbc:embedded-database> XML 命名空间元素和 EmbeddedDatabaseBuilder Java 配置共同内部使用)如果未另外指定,会将嵌入式数据库的名称设置为 testdb。对于 <jdbc:embedded-database> 的情况,嵌入式数据库通常会被赋予一个等于该 bean 的 id 的名称(通常是类似 dataSource 的名称)。因此,后续尝试创建嵌入式数据库不会生成新的数据库。相反,会重复使用相同的 JDBC 连接 URL,而尝试创建新的嵌入式数据库实际上会指向从相同配置创建的现有嵌入式数据库。spring-doc.cadn.net.cn

为了解决这个常见问题,Spring Framework 4.2 提供了对生成嵌入式数据库唯一名称的支持。要启用生成名称的使用,请使用以下任一选项。spring-doc.cadn.net.cn

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

你可以通过两种方式扩展Spring JDBC嵌入式数据库支持:spring-doc.cadn.net.cn

  • EmbeddedDatabaseConfigurer 实现为支持新的嵌入式数据库类型。spring-doc.cadn.net.cn

  • 实现 DataSourceFactory 以支持新的 DataSource 实现,例如用于管理嵌入式数据库连接的连接池。spring-doc.cadn.net.cn

我们鼓励您在GitHub Issues上为Spring社区贡献扩展。spring-doc.cadn.net.cn

3.10. 初始化 DataSource

The org.springframework.jdbc.datasource.init package provides support for initializing an existing DataSource. The embedded database support provides one option for creating and initializing a DataSource for an application. However, you may sometimes need to initialize an instance that runs on a server somewhere.spring-doc.cadn.net.cn

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

如果您想初始化一个数据库并且可以提供对DataSource bean的引用,可以在initialize-database命名空间中使用spring-jdbc标签:spring-doc.cadn.net.cn

<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中的资源(例如, classpath*:/com/foo/**/sql/*-data.sql)。如果您使用模式,脚本将按照其URL或文件名的字典顺序执行。spring-doc.cadn.net.cn

数据库初始化器的默认行为是无条件运行提供的脚本。这并不总是你想要的——例如,如果你将脚本运行到已经包含测试数据的数据库上时。通过遵循前面展示的常见模式(先创建表,然后插入数据),可以降低意外删除数据的可能性。如果表已经存在,第一步将会失败。spring-doc.cadn.net.cn

然而,为了更灵活地控制现有数据的创建和删除,XML 命名空间提供了一些额外的选项。第一个是用于开启和关闭初始化的标志。您可以根据环境设置此标志(例如从系统属性或环境 Bean 中获取一个布尔值)。以下示例从系统属性中获取一个值:spring-doc.cadn.net.cn

<jdbc:initialize-database data-source="dataSource"
    enabled="#{systemProperties.INITIALIZE_DATABASE}"> (1)
    <jdbc:script location="..."/>
</jdbc:initialize-database>
1 从名为 enabled 的系统属性中获取值 INITIALIZE_DATABASE

控制现有数据如何处理的第二种方法是更宽容地处理失败。为此,你可以控制初始化程序在运行脚本中的SQL时忽略某些错误的能力,如下例所示:spring-doc.cadn.net.cn

<jdbc:initialize-database data-source="dataSource" ignore-failures="DROPS">
    <jdbc:script location="..."/>
</jdbc:initialize-database>

在前面的示例中,我们表示有时脚本是针对空数据库运行的,因此脚本中有一些 DROP 语句会失败。因此,失败的 SQL DROP 语句将被忽略,但其他失败将导致异常。如果您使用的 SQL 方言不支持 DROP …​ IF EXISTS(或类似)功能,但又想在重新创建之前无条件删除所有测试数据,这将很有用。在这种情况下,第一个脚本通常是一组 DROP 语句,然后是一组 CREATE 语句。spring-doc.cadn.net.cn

ignore-failures 选项可以设置为 NONE(默认值)、DROPS(忽略失败的删除操作)或 ALL(忽略所有错误)。spring-doc.cadn.net.cn

每个语句应由 ; 或换行符分隔,如果脚本中完全不存在 ; 字符的话。你可以全局或逐个脚本地控制这一点,如下例所示:spring-doc.cadn.net.cn

<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 脚本覆盖该默认值。spring-doc.cadn.net.cn

如果您需要比XML命名空间提供的更多控制,可以直接使用 DataSourceInitializer 并在您的应用程序中将其定义为一个组件。spring-doc.cadn.net.cn

其他依赖数据库的组件的初始化

一个大型类别的应用程序(那些在Spring上下文启动后才使用数据库的)可以无需进一步的复杂问题就使用数据库初始化程序。如果你的应用程序不属于这些,你可能需要阅读本节的其余部分。spring-doc.cadn.net.cn

数据库初始化程序依赖于一个DataSource实例,并在其初始化回调中运行提供的脚本(类似于XML bean定义中的init-method,组件中的@PostConstruct方法,或实现InitializingBean的组件中的afterPropertiesSet()方法)。如果其他bean依赖于相同的数据源,并在初始化回调中使用数据源,可能会出现问题,因为数据尚未初始化。一个常见的例子是缓存,在应用启动时会立即初始化并从数据库加载数据。spring-doc.cadn.net.cn

为解决此问题,你有两个选项:将缓存初始化策略更改为较晚的阶段,或确保数据库初始化器首先被初始化。spring-doc.cadn.net.cn

更改你的缓存初始化策略,如果应用程序在你的控制之下,可能会很容易。 一些实现此功能的建议包括:spring-doc.cadn.net.cn

  • 在首次使用时延迟初始化缓存,这可以提高应用程序的启动速度。spring-doc.cadn.net.cn

  • 让你的缓存或单独的初始化缓存的组件实现 LifecycleSmartLifecycle。当应用上下文启动时,可以通过设置其 autoStartup 标志自动启动一个 SmartLifecycle,也可以通过在封装上下文中调用 ConfigurableApplicationContext.start() 手动启动一个 Lifecyclespring-doc.cadn.net.cn

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

确保数据库初始化器首先被初始化也可以很容易。实现此目的的一些建议包括:spring-doc.cadn.net.cn

  • 依赖 Spring BeanFactory 的默认行为,即按注册顺序初始化 beans。您可以通过在 XML 配置中采用一组按顺序排列应用程序模块的 <import/> 元素的常见做法,轻松实现这一点,并确保数据库和数据库初始化首先列出。spring-doc.cadn.net.cn

  • DataSource 与使用它的业务组件分开,并通过将它们放在不同的 ApplicationContext 实例中来控制它们的启动顺序(例如,父上下文包含 DataSource,子上下文包含业务组件)。这种结构在 Spring Web 应用程序中很常见,但可以更广泛地应用。spring-doc.cadn.net.cn

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

本节内容涉及使用对象关系映射(ORM)时的数据访问。spring-doc.cadn.net.cn

4.1. 与 Spring 的 ORM 简介

Spring框架支持与Java持久化API(JPA)的集成,并支持原生Hibernate用于资源管理、数据访问对象(DAO)实现和事务策略。例如,对于Hibernate,有第一级的支持,包含多个方便的IoC功能,可解决许多常见的Hibernate集成问题。您可以通过依赖注入配置所有支持的OR(对象关系)映射工具的功能。它们可以参与Spring的资源和事务管理,并符合Spring的通用事务和DAO异常层次结构。推荐的集成方式是针对普通的Hibernate或JPA API编写DAO。spring-doc.cadn.net.cn

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

使用 Spring 框架来创建你的 ORM DAO 可以带来以下好处:spring-doc.cadn.net.cn

  • 更简单的测试。 Spring 的 IoC 方法使交换 Hibernate SessionFactory 实例、JDBC DataSource 实例、事务管理器和映射对象实现(如需要)的实现和配置位置变得容易。这使得对每个与持久化相关的代码部分进行隔离测试变得更加容易。spring-doc.cadn.net.cn

  • 常见的数据访问异常。 Spring 可以将来自您的 ORM 工具的异常进行包装, 将其从专有的(可能为检查型)异常转换为通用的运行时 DataAccessException 层次结构。此功能可让您在适当的层中处理大多数持久化 异常,这些异常是不可恢复的,而无需烦琐的样板 catch、throws 和异常声明。您仍然可以按需捕获和处理异常。请记住,JDBC 异常(包括 DB 特定的方言)也会被转换为同一层次结构,这意味着您可以在一致的编程模型中执行一些 JDBC 操作。spring-doc.cadn.net.cn

  • 通用资源管理。 Spring 应用程序上下文可以处理 Hibernate SessionFactory 实例、JPA EntityManagerFactory 实例、JDBC DataSource 实例及其他相关资源的位置和配置。这使得这些值易于管理和更改。Spring 提供了高效、便捷且安全的持久化资源处理方式。例如,使用 Hibernate 的相关代码通常需要使用相同的 Hibernate Session 以确保效率和正确的事务处理。Spring 可以通过 Hibernate SessionFactory 暴露当前 Session,从而透明地将 Session 创建并绑定到当前线程。因此,Spring 解决了典型 Hibernate 使用中的许多长期问题,适用于任何本地或 JTA 事务环境。spring-doc.cadn.net.cn

  • 集成的事务管理。 您可以通过声明式、面向方面编程(AOP)风格的方法拦截器将ORM代码包装起来,要么通过@Transactional注解,要么通过在XML配置文件中显式配置事务AOP建议。在这两种情况下,事务语义和异常处理(如回滚等)都会为您处理。正如在资源和事务管理中所讨论的,您还可以在不影响您的ORM相关代码的情况下交换各种事务管理器。例如,您可以在本地事务和JTA之间切换,这两种情况都提供相同的完整服务(如声明式事务)。此外,JDBC相关的代码可以与您用于ORM的代码进行事务集成。这对于不适合ORM的数据访问(如批量处理和BLOB流)很有用,但仍然需要与ORM操作共享公共事务。spring-doc.cadn.net.cn

如需更全面的ORM支持,包括对MongoDB等替代数据库技术的支持,您可能需要查看 Spring Data项目系列。如果您是JPA用户,来自https://spring.io使用JPA访问数据入门指南提供了很好的介绍。

4.2. 通用ORM集成注意事项

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

Spring的ORM集成的主要目标是实现清晰的应用层结构(使用任何数据访问和事务技术)以及应用对象的松散耦合——不再有业务服务对数据访问或事务策略的依赖,不再有硬编码的资源查找,不再有难以替换的单例,不再有自定义的服务注册表。目标是采用一种简单且一致的方法来连接应用对象,尽可能使它们可重用并远离容器依赖。所有的单独的数据访问功能都可以独立使用,但能很好地与Spring的应用上下文概念集成,提供基于XML的配置和对不需要了解Spring的普通JavaBean实例的交叉引用。在典型的Spring应用中,许多重要的对象都是JavaBeans:数据访问模板、数据访问对象、事务管理器、使用数据访问对象和事务管理器的业务服务、Web视图解析器、使用业务服务的Web控制器等等。spring-doc.cadn.net.cn

4.2.1. 资源和事务管理

典型的业务应用程序中充斥着重复的资源管理代码。 许多项目试图自己发明解决方案,有时为了编程的便利而牺牲了对失败的正确处理。 Spring 倡导简单的解决方案来正确处理资源,例如在 JDBC 的情况下通过模板化实现 IoC,并针对 ORM 技术应用 AOP 拦截器。spring-doc.cadn.net.cn

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

在事务管理方面,JdbcTemplate类与Spring事务支持集成,并通过各自的Spring事务管理器支持JTA和JDBC事务。对于支持的ORM技术,Spring通过Hibernate和JPA事务管理器以及JTA支持提供Hibernate和JPA支持。有关事务支持的详细信息,请参阅事务管理章节。spring-doc.cadn.net.cn

4.2.2. 异常转换

当在DAO中使用Hibernate或JPA时,您必须决定如何处理持久化技术的原生异常类。DAO会根据技术的不同抛出HibernateExceptionPersistenceException的子类。这些异常都是运行时异常,不需要声明或捕获。您可能还需要处理IllegalArgumentExceptionIllegalStateException。这意味着调用者只能将异常视为一般性致命错误,除非他们希望依赖持久化技术本身的异常结构。如果不将调用者与实现策略绑定,就无法捕获具体的异常原因(例如乐观锁失败)。对于强ORM依赖的应用程序或不需要任何特殊异常处理的应用程序(或两者皆有),这种权衡可能是可以接受的。然而,Spring通过@Repository注解让异常转换可以透明地应用。以下示例(一个用于Java配置,一个用于XML配置)展示了如何操作:spring-doc.cadn.net.cn

Java
@Repository
public class ProductDaoImpl implements ProductDao {

    // class body here...

}
Kotlin
@Repository
class ProductDaoImpl : ProductDao {
    // class body here...
}
<beans>

    <!-- Exception translation bean post processor -->
    <bean class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor"/>

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

</beans>

后处理器会自动查找所有异常转换器(PersistenceExceptionTranslator 接口的实现类),并为所有使用 @Repository 注解标记的 Bean 提供建议,以便发现的转换器可以拦截并应用于抛出的异常的适当转换。spring-doc.cadn.net.cn

总之,你可以基于原始持久化技术的API和注解来实现DAO,同时仍然可以从Spring管理的事务、依赖注入以及(如需)透明的异常转换(转换为Spring的自定义异常层次结构)中获益。spring-doc.cadn.net.cn

4.3. Hibernate

我们首先介绍在Spring环境中的,并使用它来演示Spring在集成OR映射器方面所采取的方法。本节详细介绍了许多问题,并展示了DAO实现和事务分界的不同变体。这些模式中的大多数都可以直接应用于所有其他支持的ORM工具。本章后面的章节将介绍其他ORM技术并给出简要示例。 spring-doc.cadn.net.cn

从 Spring Framework 5.0 开始,Spring 要求使用 Hibernate ORM 4.3 或更高版本以支持 JPA, 并且对于使用原生 Hibernate Session API 进行编程,需要 Hibernate ORM 5.0+。 请注意,Hibernate 团队已不再维护 5.1 之前的任何版本,并且很可能很快将重点放在 5.4+ 上。

4.3.1. SessionFactory 在Spring容器中的设置

为了避免将应用程序对象与硬编码的资源查找绑定,可以将资源(例如一个JDBC DataSource或一个Hibernate SessionFactory)定义为Spring容器中的bean。需要访问资源的应用程序对象可以通过bean引用获得这些预定义实例的引用,如下一节中的DAO定义所示。spring-doc.cadn.net.cn

以下是从XML应用程序上下文定义中摘录的内容,展示了如何在它之上设置一个 JDBC DataSource 和一个 Hibernate SessionFactoryspring-doc.cadn.net.cn

<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(通常由应用服务器管理)只需进行配置,如下例所示:spring-doc.cadn.net.cn

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

您也可以通过 Spring 的 SessionFactory / JndiObjectFactoryBean 来访问一个 JNDI 位置的 SessionFactory,并获取和暴露它。 不过,这在 EJB 上下文之外通常不常见。spring-doc.cadn.net.cn

Spring 还提供了一个 LocalSessionFactoryBuilder 变体,与 @Bean 样式配置和编程设置无缝集成(不涉及 FactoryBean)。spring-doc.cadn.net.cn

LocalSessionFactoryBeanLocalSessionFactoryBuilder 都支持后台引导,其中 Hibernate 初始化会在给定的引导执行器(如 SimpleAsyncTaskExecutor)上与应用程序引导线程并行运行。 在 LocalSessionFactoryBean 上,这是通过 bootstrapExecutor 属性提供的。在编程式的 LocalSessionFactoryBuilder 上,有一个接受引导执行器参数的重载 buildSessionFactory 方法。spring-doc.cadn.net.cn

从 Spring Framework 5.1 开始,这种原生 Hibernate 设置也可以在原生 Hibernate 访问之外,为标准 JPA 交互提供一个 JPA EntityManagerFactory。 有关详细信息,请参阅 用于 JPA 的原生 Hibernate 设置spring-doc.cadn.net.cn

4.3.2. 基于原始 Hibernate API 实现 DAO

Hibernate 有一个名为上下文会话的功能,其中 Hibernate 本身会在每个事务中管理一个当前的 Session。这大致相当于 Spring 每个事务中对 Hibernate Session 的同步。相应的 DAO 实现类似于以下示例,基于普通的 Hibernate API:spring-doc.cadn.net.cn

Java
public class ProductDaoImpl implements ProductDao {

    private SessionFactory sessionFactory;

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

    public Collection loadProductsByCategory(String category) {
        return this.sessionFactory.getCurrentSession()
                .createQuery("from test.Product product where product.category=?")
                .setParameter(0, category)
                .list();
    }
}
Kotlin
class ProductDaoImpl(private val sessionFactory: SessionFactory) : ProductDao {

    fun loadProductsByCategory(category: String): Collection<*> {
        return sessionFactory.currentSession
                .createQuery("from test.Product product where product.category=?")
                .setParameter(0, category)
                .list()
    }
}

这种样式与Hibernate参考文档和示例类似, 只是将SessionFactory保存在实例变量中。我们强烈建议使用这种基于实例的设置,而不是Hibernate的CaveatEmptor示例应用程序中的旧式static HibernateUtil类。(一般来说,除非绝对必要,否则不要将任何资源保存在static变量中。)spring-doc.cadn.net.cn

前面的DAO示例遵循依赖注入模式。它非常适合放入Spring IoC容器中,就像使用Spring的HibernateTemplate编码时一样。 你也可以在纯Java中设置这样的DAO(例如,在单元测试中)。要做到这一点, 实例化它并调用setSessionFactory(..)方法并传入所需的工厂引用。作为Spring bean定义,DAO将如下所示:spring-doc.cadn.net.cn

<beans>

    <bean id="myProductDao" class="product.ProductDaoImpl">
        <property name="sessionFactory" ref="mySessionFactory"/>
    </bean>

</beans>

这种DAO风格的主要优点是它只依赖Hibernate API。不需要导入任何Spring类。从非侵入性的角度来看,这很有吸引力,对Hibernate开发人员来说可能更自然。spring-doc.cadn.net.cn

然而,DAO 抛出的是普通的 HibernateException(这是未检查异常,因此不需要声明或捕获),这意味着调用者只能将异常视为一般性致命错误 — 除非他们希望依赖 Hibernate 自己的异常层次结构。如果不将调用者与实现策略绑定,就无法捕获特定原因(例如乐观锁失败)。对于那些强烈依赖 Hibernate、不需要特殊异常处理的应用程序来说,这种权衡可能是可以接受的。spring-doc.cadn.net.cn

幸运的是,Spring 的 LocalSessionFactoryBean 支持 Hibernate 的 SessionFactory.getCurrentSession() 方法,适用于任何 Spring 事务策略, 返回当前由 Spring 管理的事务 Session,即使使用 HibernateTransactionManager。该方法的标准行为是 返回与当前 JTA 事务相关联的 Session,如果有的话。 无论您使用 Spring 的 JtaTransactionManager、EJB 容器管理的事务(CMTs)还是 JTA,此行为都适用。spring-doc.cadn.net.cn

总之,你可以基于普通的 Hibernate API 实现 DAO,同时仍然可以参与 Spring 管理的事务。spring-doc.cadn.net.cn

4.3.3. 声明式事务界定

我们建议您使用 Spring 的声明式事务支持,这可以让您用 AOP 事务拦截器替换 Java 代码中的显式事务分界 API 调用。您可以通过使用 Java 注解或 XML 在 Spring 容器中配置此事务拦截器。这种声明式事务功能可以让您保持业务服务不包含重复的事务分界代码,并专注于添加业务逻辑,这才是您应用程序的实际价值。spring-doc.cadn.net.cn

在继续之前,我们强烈建议您阅读 声明式事务管理 如果您还没有这样做的话。

您可以使用<code>0</code>注解对服务层进行注解,并指示Spring容器查找这些注解,为这些注解的方法提供事务语义。下面的示例展示了如何操作:spring-doc.cadn.net.cn

Java
public class ProductServiceImpl implements ProductService {

    private ProductDao productDao;

    public void setProductDao(ProductDao productDao) {
        this.productDao = productDao;
    }

    @Transactional
    public void increasePriceOfAllProductsInCategory(final String category) {
        List productsToChange = this.productDao.loadProductsByCategory(category);
        // ...
    }

    @Transactional(readOnly = true)
    public List<Product> findAllProducts() {
        return this.productDao.findAllProducts();
    }
}
Kotlin
class ProductServiceImpl(private val productDao: ProductDao) : ProductService {

    @Transactional
    fun increasePriceOfAllProductsInCategory(category: String) {
        val productsToChange = productDao.loadProductsByCategory(category)
        // ...
    }

    @Transactional(readOnly = true)
    fun findAllProducts() = productDao.findAllProducts()
}

在容器中,您需要设置 PlatformTransactionManager 实现 (作为 bean)和一个 <tx:annotation-driven/> 条目,在运行时选择 @Transactional 处理。下面的示例展示了如何操作:spring-doc.cadn.net.cn

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

4.3.4. 编程式事务界定

您可以在应用程序的更高层次上标记事务,在此基础上是跨越任意数量操作的较低层次数据访问服务。对周围业务服务的实现也没有限制。它只需要一个 Spring PlatformTransactionManager。同样,后者可以来自任何地方,但最好通过 setTransactionManager(..) 方法作为 bean 引用。此外,productDAO 应通过 setProductDao(..) 方法设置。以下两个代码片段显示了 Spring 应用程序上下文中的事务管理器和业务服务定义,以及一个业务方法实现的示例:spring-doc.cadn.net.cn

<beans>

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

    <bean id="myProductService" class="product.ProductServiceImpl">
        <property name="transactionManager" ref="myTxManager"/>
        <property name="productDao" ref="myProductDao"/>
    </bean>

</beans>
Java
public class ProductServiceImpl implements ProductService {

    private TransactionTemplate transactionTemplate;
    private ProductDao productDao;

    public void setTransactionManager(PlatformTransactionManager transactionManager) {
        this.transactionTemplate = new TransactionTemplate(transactionManager);
    }

    public void setProductDao(ProductDao productDao) {
        this.productDao = productDao;
    }

    public void increasePriceOfAllProductsInCategory(final String category) {
        this.transactionTemplate.execute(new TransactionCallbackWithoutResult() {
            public void doInTransactionWithoutResult(TransactionStatus status) {
                List productsToChange = this.productDao.loadProductsByCategory(category);
                // do the price increase...
            }
        });
    }
}
Kotlin
class ProductServiceImpl(transactionManager: PlatformTransactionManager,
                        private val productDao: ProductDao) : ProductService {

    private val transactionTemplate = TransactionTemplate(transactionManager)

    fun increasePriceOfAllProductsInCategory(category: String) {
        transactionTemplate.execute {
            val productsToChange = productDao.loadProductsByCategory(category)
            // do the price increase...
        }
    }
}

Spring 的 TransactionInterceptor 允许在回调代码中抛出任何已检查的应用程序异常,而 TransactionTemplate 仅限于回调中的未检查异常。 TransactionTemplate 在发生未检查的应用程序异常或应用程序将事务标记为回滚(通过设置 TransactionStatus)时触发回滚。默认情况下,TransactionInterceptor 的行为相同,但允许按方法配置回滚策略。spring-doc.cadn.net.cn

4.3.5. 事务管理策略

TransactionTemplateTransactionInterceptor 都将实际的事务处理委托给一个 PlatformTransactionManager 实例(这可以是一个 HibernateTransactionManager(用于单个 Hibernate SessionFactory)通过在后台使用一个 ThreadLocal Session)或者一个 JtaTransactionManager(将事务委托给容器的 JTA 子系统)用于 Hibernate 应用程序。你甚至可以使用自定义的 PlatformTransactionManager 实现。从原生 Hibernate 事务管理切换到 JTA(例如当你的应用程序某些部署需要分布式事务时)只需进行配置即可。你可以将 Hibernate 事务管理器替换为 Spring 的 JTA 事务实现。事务界定和数据访问代码无需更改,因为它们使用的是通用的事务管理 API。spring-doc.cadn.net.cn

对于跨多个Hibernate会话工厂的分布式事务,可以将JtaTransactionManager作为事务策略与多个LocalSessionFactoryBean定义结合使用。每个DAO然后会获得一个特定的SessionFactory引用,传递到其对应的bean属性中。如果所有底层JDBC数据源都是事务容器数据源,业务服务可以在不特别注意的情况下,使用JtaTransactionManager作为策略,在任何数量的DAO和任何数量的会话工厂之间标记事务。spring-doc.cadn.net.cn

HibernateTransactionManagerJtaTransactionManager 都可以在不使用容器特定的事务管理器查找或 JCA 连接器(如果你不使用 EJB 启动事务)的情况下,实现与 Hibernate 的 JVM 级缓存处理。spring-doc.cadn.net.cn

HibernateTransactionManager 可以将 Hibernate JDBC Connection 导出为特定 DataSource 的普通 JDBC 访问代码。此功能允许在完全不使用 JTA 的情况下,通过混合使用 Hibernate 和 JDBC 数据访问进行高级事务声明。前提是您只访问一个数据库。如果已通过 dataSource 属性将传入的 SessionFactoryDataSource 进行了设置,HibernateTransactionManager 会自动将 Hibernate 事务作为 JDBC 事务公开。或者,您可以通过 HibernateTransactionManager 类的 dataSource 属性显式指定要公开事务的 DataSourcespring-doc.cadn.net.cn

4.3.6. 比较容器管理的和本地定义的资源

您可以在不更改任何应用程序代码的情况下,在由容器管理的 JNDI SessionFactory 和本地定义的 JNDI 之间进行切换。是否将资源定义保留在容器中还是在应用程序中本地定义,主要取决于您使用的事务策略。与 Spring 定义的本地 SessionFactory 相比,手动注册的 JNDI SessionFactory 没有任何优势。通过 Hibernate 的 JCA 连接器部署 SessionFactory 可以参与 Java EE 服务器的管理基础设施,但除此之外没有实际价值。spring-doc.cadn.net.cn

Spring的事务支持不依赖于容器。当使用除JTA以外的任何策略进行配置时,事务支持也可以在独立或测试环境中工作。特别是在单数据库事务的典型情况下,Spring的单资源本地事务支持是JTA的轻量级且功能强大的替代方案。当您使用本地EJB无状态会话Bean来驱动事务时,即使您只访问一个数据库并且仅使用无状态会话Bean通过容器管理的事务提供声明式事务,您也需要同时依赖于EJB容器和JTA。直接以编程方式使用JTA也要求有Java EE环境。从JTA本身以及JNDI DataSource实例的角度来看,JTA不仅仅涉及容器依赖。对于非Spring的、由JTA驱动的Hibernate事务,您必须使用Hibernate JCA连接器或带有TransactionManagerLookup配置的额外Hibernate事务代码,以实现正确的JVM级缓存。spring-doc.cadn.net.cn

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

考虑到所有因素,如果您不使用EJB,建议使用本地SessionFactory配置 和Spring的HibernateTransactionManagerJtaTransactionManager。您可以获得所有 好处,包括正确的事务性JVM级缓存和分布式 事务,而无需忍受容器部署的麻烦。通过JCA连接器将Hibernate SessionFactory进行JNDI注册只有在与EJB一起使用时才有价值。spring-doc.cadn.net.cn

4.3.7. 与 Hibernate 相关的虚假应用服务器警告

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

java.sql.SQLException: The transaction is no longer active - status: 'Committed'. No
further JDBC access is allowed within this transaction.

另一个常见问题是 JTA 事务后出现的连接泄漏,Hibernate 会话(以及可能的底层 JDBC 连接)未能正确关闭。spring-doc.cadn.net.cn

你可以通过让 Hibernate 了解 JTA 事务管理器来解决此类问题,Hibernate 会与该事务管理器(以及 Spring)进行同步。为此你有两种选择:spring-doc.cadn.net.cn

  • 将您的 Spring JtaTransactionManager bean 传递给您的 Hibernate 设置。最简单的方法是将一个 bean 引用到您的 jtaTransactionManager 属性中的 LocalSessionFactoryBean bean(参见 Hibernate 事务设置)。 Spring 然后会向 Hibernate 提供相应的 JTA 策略。spring-doc.cadn.net.cn

  • 您也可以显式配置Hibernate的JTA相关属性,特别是“hibernate.transaction.coordinator_class”、“hibernate.connection.handling_mode”以及可能的“hibernate.transaction.jta.platform”,在“hibernateProperties”中设置为“0”(有关这些属性的详细信息,请参阅Hibernate的手册)。spring-doc.cadn.net.cn

本节的其余部分描述了在Hibernate了解JTA PlatformTransactionManager和不了解JTA PlatformTransactionManager的情况下发生的事件顺序。spring-doc.cadn.net.cn

当 Hibernate 没有配置对 JTA 事务管理器的任何了解时,当 JTA 事务提交时会发生以下事件:spring-doc.cadn.net.cn

  • JTA 事务提交。spring-doc.cadn.net.cn

  • Spring的JtaTransactionManager与JTA事务同步,因此通过JTA事务管理器的afterCompletion回调被调用。spring-doc.cadn.net.cn

  • 除了其他活动外,此同步可以通过 Spring 触发对 Hibernate 的回调, 通过 Hibernate 的 afterTransactionCompletion 回调(用于清除 Hibernate 缓存), 然后是对 Hibernate 会话的显式 close() 调用, 这会导致 Hibernate 尝试 close() JDBC 连接。spring-doc.cadn.net.cn

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

当 Hibernate 配置为了解 JTA 事务管理器时, 在 JTA 事务提交时会发生以下事件:spring-doc.cadn.net.cn

  • JTA事务已准备好提交。spring-doc.cadn.net.cn

  • Spring 的 JtaTransactionManager 与 JTA 事务同步,因此事务通过 JTA 事务管理器的 beforeCompletion 回调被调用。spring-doc.cadn.net.cn

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

  • JTA 事务提交。spring-doc.cadn.net.cn

  • Hibernate 与 JTA 事务同步,因此事务会通过 afterCompletion 回调由 JTA 事务管理器调用,并可以正确清除其缓存。spring-doc.cadn.net.cn

4.4. JPA

Spring JPA,可在 org.springframework.orm.jpa 包下获得,提供了对 Java Persistence API 的全面支持,其方式类似于与 Hibernate 的集成,同时了解底层实现以提供额外的功能。spring-doc.cadn.net.cn

4.4.1. 在Spring环境中设置JPA的三种选项

Spring JPA 支持提供了三种设置应用程序用于获取实体管理器的 JPA EntityManagerFactory 的方法。spring-doc.cadn.net.cn

使用 LocalEntityManagerFactoryBean

你只能在简单的部署环境(如独立应用程序和集成测试)中使用此选项。spring-doc.cadn.net.cn

LocalEntityManagerFactoryBean 会创建一个适合于仅使用 JPA 进行数据访问的简单部署环境的 EntityManagerFactory。 工厂 Bean 使用 JPA PersistenceProvider 的自动检测机制(根据 JPA 的 Java SE 引导方式),在大多数情况下,只需指定持久化单元名称即可。以下 XML 示例配置了这样的 Bean:spring-doc.cadn.net.cn

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

这种JPA部署方式最简单,但功能也最有限。您无法引用现有的JDBC DataSource bean定义,并且不支持全局事务。此外,持久化类的编织(字节码转换)是提供商特定的,通常需要在启动时指定特定的JVM代理。此选项仅适用于独立应用程序和测试环境,这正是JPA规范所设计的用途。spring-doc.cadn.net.cn

从JNDI获取EntityManagerFactory

你可以在部署到 Java EE 服务器时使用此选项。查看服务器的文档,了解如何将自定义的 JPA 提供者部署到您的服务器中,从而使用与服务器默认不同的提供者。spring-doc.cadn.net.cn

从 JNDI(例如在 Java EE 环境中)获取 EntityManagerFactory, 只需更改 XML 配置,如下例所示:spring-doc.cadn.net.cn

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

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

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

如果在同一个应用程序中使用多个持久化单元,通过JNDI检索到的持久化单元的bean名称应与应用程序用来引用它们的持久化单元名称匹配(例如,在@PersistenceUnit@PersistenceContext注释中)。spring-doc.cadn.net.cn

使用 LocalContainerEntityManagerFactoryBean

你可以使用此选项在基于 Spring 的应用程序环境中实现完整的 JPA 功能。 这包括如 Tomcat 等 Web 容器、独立应用程序以及具有复杂持久化需求的集成测试。spring-doc.cadn.net.cn

如果您想专门配置一个Hibernate设置,一个直接的替代方法是 选择Hibernate 5.2/5.3/5.4并设置一个原生Hibernate LocalSessionFactoryBean 而不是普通的JPA LocalContainerEntityManagerFactoryBean,让它与JPA访问代码以及原生Hibernate访问代码进行交互。 有关详细信息,请参见 用于JPA交互的原生Hibernate设置

LocalContainerEntityManagerFactoryBean 提供了对 EntityManagerFactory 配置的完全控制,并且适用于需要精细自定义的环境。LocalContainerEntityManagerFactoryBean 会根据 persistence.xml 文件、提供的 dataSourceLookup 策略以及指定的 loadTimeWeaver 创建一个 PersistenceUnitInfo 实例。因此,可以使用 JNDI 之外的自定义数据源并控制织入过程。下面的示例显示了一个 LocalContainerEntityManagerFactoryBean 的典型 bean 定义:spring-doc.cadn.net.cn

<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 文件:spring-doc.cadn.net.cn

<persistence xmlns="http://java.sun.com/xml/ns/persistence" version="1.0">
    <persistence-unit name="myUnit" transaction-type="RESOURCE_LOCAL">
        <mapping-file>META-INF/orm.xml</mapping-file>
        <exclude-unlisted-classes/>
    </persistence-unit>
</persistence>
<exclude-unlisted-classes/> 快捷方式表示不进行带注解的实体类扫描。显式的 'true' 值(<exclude-unlisted-classes>true</exclude-unlisted-classes/>)也表示不进行扫描。 <exclude-unlisted-classes>false</exclude-unlisted-classes/> 会触发扫描。 然而,如果我们希望进行实体类扫描,建议省略 exclude-unlisted-classes 元素。

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

此选项可能与 Java EE 服务器的内置 JPA 功能冲突。在完整的 Java EE 环境中,建议从 JNDI 获取你的 EntityManagerFactory。 或者,在你的 LocalContainerEntityManagerFactoryBean 定义(例如,META-INF/my-persistence.xml)上指定一个自定义的 persistenceXmlLocation,并在应用程序的 jar 文件中仅包含具有该名称的描述符。由于 Java EE 服务器只查找默认的 META-INF/persistence.xml 文件,因此会忽略这些自定义的持久化单元,从而提前避免与 Spring 驱动的 JPA 设置发生冲突。(这适用于 Resin 3.1 等情况。)spring-doc.cadn.net.cn

何时需要使用加载时织入?

并非所有JPA提供程序都需要JVM代理。Hibernate是一个不需要的示例。 如果您的提供程序不需要代理,或者您有其他替代方法,例如通过自定义编译器或Ant任务在构建时应用增强,则不应使用加载时编织器。spring-doc.cadn.net.cn

LoadTimeWeaver 接口是 Spring 提供的类,它允许根据环境是 Web 容器还是应用服务器,以特定方式插入 JPA ClassTransformer 实例。通过 代理 连接 ClassTransformers 通常效率不高。代理会针对整个虚拟机工作,并检查所有加载的类,这在生产服务器环境中通常是不希望的。spring-doc.cadn.net.cn

Spring 为各种环境提供了多个 LoadTimeWeaver 实现, 允许每个类加载器仅应用 ClassTransformer 个实例,而不是每个虚拟机。spring-doc.cadn.net.cn

查看AOP章节中的Spring配置,以获得更多关于LoadTimeWeaver实现及其设置的见解,无论是通用的还是针对各种平台(如Tomcat、JBoss和WebSphere)的定制版本。spring-doc.cadn.net.cn

如在Spring配置中所述,可以通过使用LoadTimeWeaver注解的@EnableLoadTimeWeaving XML元素来配置一个上下文范围的LoadTimeWeaver。这种全局编织器会自动被所有JPA LocalContainerEntityManagerFactoryBean实例所采用。下面的例子展示了设置运行时编织器的首选方法,该方法可自动检测平台(例如Tomcat的具备编织功能的类加载器或Spring的JVM代理)并自动将编织器传播到所有具备编织功能的Bean:spring-doc.cadn.net.cn

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

但是,如果需要,您可以通过loadTimeWeaver属性手动指定一个专用的weaver,如下例所示:spring-doc.cadn.net.cn

<bean id="emf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
    <property name="loadTimeWeaver">
        <bean class="org.springframework.instrument.classloading.ReflectiveLoadTimeWeaver"/>
    </property>
</bean>

无论LTW如何配置,通过此技术,依赖于instrumentation的JPA应用程序可以在目标平台(例如Tomcat)上运行,而无需使用代理。当托管的应用程序依赖于不同的JPA实现时,这一点尤为重要,因为JPA转换器仅在类加载器级别应用,因此彼此之间是隔离的。spring-doc.cadn.net.cn

处理多个持久化单元

对于依赖多个持久化单元位置(例如存储在类路径中的不同JAR中)的应用程序,Spring提供了PersistenceUnitManager作为中心仓库,以避免耗时的持久化单元发现过程。默认实现允许指定多个位置。这些位置会被解析,并通过持久化单元名称 later 检索。(默认情况下,会搜索类路径中的META-INF/persistence.xml文件。)以下示例配置了多个位置:spring-doc.cadn.net.cn

<bean id="pum" class="org.springframework.orm.jpa.persistenceunit.DefaultPersistenceUnitManager">
    <property name="persistenceXmlLocations">
        <list>
            <value>org/springframework/orm/jpa/domain/persistence-multi.xml</value>
            <value>classpath:/my/package/**/custom-persistence.xml</value>
            <value>classpath*:META-INF/persistence.xml</value>
        </list>
    </property>
    <property name="dataSources">
        <map>
            <entry key="localDataSource" value-ref="local-db"/>
            <entry key="remoteDataSource" value-ref="remote-db"/>
        </map>
    </property>
    <!-- if no datasource is specified, use this one -->
    <property name="defaultDataSource" ref="remoteDataSource"/>
</bean>

<bean id="emf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
    <property name="persistenceUnitManager" ref="pum"/>
    <property name="persistenceUnitName" value="myCustomUnit"/>
</bean>

默认实现允许在将PersistenceUnitInfo实例提供给JPA提供程序之前进行自定义,可以通过声明方式(通过其属性,这些属性会影响所有托管单元)或编程方式(通过PersistenceUnitPostProcessor,这允许选择持久化单元)进行自定义。如果没有指定PersistenceUnitManager,则会创建一个并在LocalContainerEntityManagerFactoryBean内部使用。spring-doc.cadn.net.cn

背景引导

LocalContainerEntityManagerFactoryBean 通过 bootstrapExecutor 属性支持后台引导,如下例所示:spring-doc.cadn.net.cn

<bean id="emf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
    <property name="bootstrapExecutor">
        <bean class="org.springframework.core.task.SimpleAsyncTaskExecutor"/>
    </property>
</bean>

实际的JPA提供程序引导由指定的执行器处理,然后,以并行方式,由应用程序引导线程处理。公开的<code>0</code> 代理可以注入到其他应用程序组件中,并且甚至能够响应<code>1</code>配置检查。但是,一旦实际的JPA提供程序被其他组件访问(例如,调用<code>2</code>),这些调用将阻塞,直到后台引导完成。特别是,当您使用Spring Data JPA时,请确保为其仓库也设置延迟引导。spring-doc.cadn.net.cn

4.4.2. 基于JPA实现DAO:EntityManagerFactoryEntityManager

虽然 EntityManagerFactory 实例是线程安全的,但 EntityManager 实例不是。注入的 JPA EntityManager 的行为类似于从应用服务器的 JNDI 环境中获取的 EntityManager,如 JPA 规范所定义。它会将所有调用委托给当前事务的 EntityManager,如果有的话。否则,它会回退到每个操作中创建一个新的 EntityManager,从而使其使用线程安全。

可以在不使用任何Spring依赖项的情况下针对普通JPA编写代码,通过使用注入的EntityManagerFactoryEntityManager。如果启用了PersistenceAnnotationBeanPostProcessor,Spring可以在字段级别和方法级别都理解@PersistenceUnit@PersistenceContext注解。下面的示例显示了一个使用@PersistenceUnit注解的普通JPA DAO实现:spring-doc.cadn.net.cn

Java
public class ProductDaoImpl implements ProductDao {

    private EntityManagerFactory emf;

    @PersistenceUnit
    public void setEntityManagerFactory(EntityManagerFactory emf) {
        this.emf = emf;
    }

    public Collection loadProductsByCategory(String category) {
        try (EntityManager em = this.emf.createEntityManager()) {
            Query query = em.createQuery("from Product as p where p.category = ?1");
            query.setParameter(1, category);
            return query.getResultList();
        }
    }
}
Kotlin
class ProductDaoImpl : ProductDao {

    private lateinit var emf: EntityManagerFactory

    @PersistenceUnit
    fun setEntityManagerFactory(emf: EntityManagerFactory) {
        this.emf = emf
    }

    fun loadProductsByCategory(category: String): Collection<*> {
        val em = this.emf.createEntityManager()
        val query = em.createQuery("from Product as p where p.category = ?1");
        query.setParameter(1, category);
        return query.resultList;
    }
}

前面的DAO不依赖于Spring,但仍可以很好地融入Spring应用程序上下文中。此外,DAO利用注解来要求注入默认的EntityManagerFactory,如下面的bean定义所示:spring-doc.cadn.net.cn

<beans>

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

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

</beans>

作为显式定义 PersistenceAnnotationBeanPostProcessor 的替代方法, 请考虑在应用程序上下文配置中使用 Spring context:annotation-config XML 元素。 这样会自动注册所有 Spring 标准的后处理器, 用于基于注解的配置,包括 CommonAnnotationBeanPostProcessor 等。spring-doc.cadn.net.cn

请考虑以下示例:spring-doc.cadn.net.cn

<beans>

    <!-- post-processors for all standard config annotations -->
    <context:annotation-config/>

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

</beans>

这种DAO的主要问题是它总是通过工厂创建一个新的 EntityManager。你可以通过请求一个事务性的 EntityManager(也称为“共享 EntityManager”,因为它是一个共享的、线程安全的代理,用于实际的事务性 EntityManager)来避免这种情况。下面的例子展示了如何操作:spring-doc.cadn.net.cn

Java
public class ProductDaoImpl implements ProductDao {

    @PersistenceContext
    private EntityManager em;

    public Collection loadProductsByCategory(String category) {
        Query query = em.createQuery("from Product as p where p.category = :category");
        query.setParameter("category", category);
        return query.getResultList();
    }
}
Kotlin
class ProductDaoImpl : ProductDao {

    @PersistenceContext
    private lateinit var em: EntityManager

    fun loadProductsByCategory(category: String): Collection<*> {
        val query = em.createQuery("from Product as p where p.category = :category")
        query.setParameter("category", category)
        return query.resultList
    }
}

@PersistenceContext 注解有一个可选属性称为 type,其默认值为 PersistenceContextType.TRANSACTION。您可以使用此默认值来接收一个共享的 EntityManager 代理。另一种方式是 PersistenceContextType.EXTENDED,这完全是另一回事。这会导致所谓的扩展 EntityManager,它不是线程安全的,因此不能在被并发访问的组件中使用,例如 Spring 管理的单例 bean。扩展的 EntityManager 实例只能在状态型组件中使用,例如位于会话中的组件,其 EntityManager 的生命周期不与当前事务相关,而是完全由应用程序决定。spring-doc.cadn.net.cn

方法和字段级别的注入

您可以将指示依赖注入的注解(如 @PersistenceUnit@PersistenceContext)应用在类中的字段或方法上 — 因此有了“方法级别注入”和“字段级别注入”的说法。字段级别的注解简洁且更易于使用,而方法级别的注解允许对注入的依赖项进行进一步处理。在两种情况下,成员的可见性(public、protected 或 private)都不重要。spring-doc.cadn.net.cn

类级别的注解又如何处理呢?spring-doc.cadn.net.cn

在 Java EE 平台上,它们用于依赖声明,而不是用于资源注入。spring-doc.cadn.net.cn

注入的 EntityManager 是 Spring 管理的(了解当前的事务)。 尽管新的 DAO 实现使用了方法级别的 EntityManager 注入而不是 EntityManagerFactory,但由于使用了注解,应用程序上下文 XML 不需要进行更改。spring-doc.cadn.net.cn

这种DAO风格的主要优点是它仅依赖于Java持久化API。 不需要导入任何Spring类。此外,由于JPA注解被理解, Spring容器会自动应用注入。从非侵入性的角度来看,这很有吸引力,并且对JPA开发者来说可能感觉更自然。spring-doc.cadn.net.cn

4.4.3. 基于Spring的JPA事务

我们强烈建议您阅读 声明式事务管理,如果您尚未这样做的话,以获得有关 Spring 声明式事务支持的更详细内容。

对于JPA的推荐策略是通过JPA的原生事务支持进行本地事务。Spring的JpaTransactionManager在任何常规JDBC连接池上提供了许多从本地JDBC事务中熟知的功能(例如事务特定的隔离级别和资源级别的只读优化)(无需XA要求)。spring-doc.cadn.net.cn

Spring JPA 还允许配置的 JpaTransactionManager 将 JPA 事务暴露给访问相同 DataSource 的 JDBC 访问代码,前提是注册的 JpaDialect 支持检索底层 JDBC Connection。 Spring 为 EclipseLink 和 Hibernate JPA 实现提供了方言。 有关 JpaDialect 机制的详细信息,请参阅下一节。spring-doc.cadn.net.cn

作为直接的替代方案,Spring 的原生 HibernateTransactionManager 自 Spring Framework 5.1 和 Hibernate 5.2/5.3/5.4 起能够与 JPA 访问代码进行交互, 适应多种 Hibernate 特性并提供 JDBC 交互。 这在与 LocalSessionFactoryBean 配置结合使用时尤其有意义。 有关详细信息,请参阅 原生 Hibernate 的 JPA 交互设置

4.4.4. 了解 JpaDialectJpaVendorAdapter

作为一项高级功能,JpaTransactionManagerAbstractEntityManagerFactoryBean 的子类允许将自定义的 JpaDialect 传递到 jpaDialect bean 属性中。一个 JpaDialect 实现可以以提供商特定的方式启用 Spring 支持的以下高级功能:spring-doc.cadn.net.cn

这在具有特殊事务语义和异常的高级转换中尤其有价值。默认实现(DefaultJpaDialect)不提供任何特殊功能,如果需要前面列出的功能,则必须指定适当的语言。spring-doc.cadn.net.cn

作为Spring完整功能的LocalContainerEntityManagerFactoryBean设置的主要提供者适应设施,JpaVendorAdapter结合了JpaDialect与其他特定于提供者的默认值的功能。指定HibernateJpaVendorAdapterEclipseLinkJpaVendorAdapter是为Hibernate或EclipseLink分别自动配置EntityManagerFactory设置的最便捷方式。请注意,这些提供者适配器主要是为与Spring驱动的事务管理(即,用于与JpaTransactionManager一起使用)而设计的。

查看 JpaDialectJpaVendorAdapter 的 javadoc 以了解其操作的更多详细信息以及它们在 Spring 的 JPA 支持中的使用方法。spring-doc.cadn.net.cn

4.4.5. 使用 JTA 事务管理设置 JPA

作为 JpaTransactionManager 的替代方案,Spring 还可以通过 JTA 实现多资源事务协调,既可以在 Java EE 环境中使用,也可以在独立的事务协调器(如 Atomikos)中使用。除了选择 Spring 的 JtaTransactionManager 而不是 JpaTransactionManager 之外,还需要采取一些其他步骤:spring-doc.cadn.net.cn

  • 底层的JDBC连接池需要具备XA功能,并与您的事务协调器集成。这在Java EE环境中通常很简单,通过JNDI公开不同的DataSource。请参阅您的应用服务器文档以获取详细信息。同样,独立的事务协调器通常会带有特殊的XA集成DataSource版本。再次,请查阅其文档。spring-doc.cadn.net.cn

  • JPA EntityManagerFactory 设置需要为 JTA 进行配置。这取决于提供者,通常通过作为 jpaProperties 指定的特殊属性来设置在 LocalContainerEntityManagerFactoryBean 上。在 Hibernate 的情况下,这些属性甚至是版本特定的。请参阅你的 Hibernate 文档以获取详细信息。spring-doc.cadn.net.cn

  • Spring的HibernateJpaVendorAdapter强制执行某些与Spring相关的默认值,例如连接释放模式on-close,这与Hibernate 5.0中的自身默认值匹配,但在Hibernate 5.1+中不再如此。对于JTA设置,请确保将您的持久化单元事务类型声明为“JTA”。或者,将Hibernate 5.2的hibernate.connection.handling_mode属性设置为DELAYED_ACQUISITION_AND_RELEASE_AFTER_STATEMENT以恢复Hibernate自身的默认值。 有关相关说明,请参见使用Hibernate时出现的误报应用服务器警告spring-doc.cadn.net.cn

  • 或者,考虑直接从您的应用服务器获取 EntityManagerFactory(即通过 JNDI 查找而不是本地声明的 LocalContainerEntityManagerFactoryBean)。服务器提供的 EntityManagerFactory 可能需要在您的服务器配置中进行特殊定义(这会使部署的可移植性降低),但已为服务器的 JTA 环境进行了设置。spring-doc.cadn.net.cn

4.4.6. 原生 Hibernate 设置和原生 Hibernate 事务用于 JPA 交互

从Spring Framework 5.1和Hibernate 5.2/5.3/5.4开始,原生LocalSessionFactoryBean的设置结合HibernateTransactionManager可以与@PersistenceContext及其他JPA访问代码进行交互。HibernateSessionFactory现在原生实现了JPA的EntityManagerFactory接口,而HibernateSession句柄原生就是一个JPAEntityManager。Spring的JPA支持功能会自动检测原生Hibernate会话。spring-doc.cadn.net.cn

因此,这种原生的Hibernate配置可以在许多情况下作为标准JPA LocalContainerEntityManagerFactoryBeanJpaTransactionManager组合的替代方案, 允许在同一个本地事务中与SessionFactory.getCurrentSession() (以及HibernateTemplate)一起使用@PersistenceContext EntityManager。这种配置还提供了更强的Hibernate集成 和更多的配置灵活性,因为它不受JPA启动契约的限制。spring-doc.cadn.net.cn

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

LocalSessionFactoryBeanLocalSessionFactoryBuilder 支持背景引导,就像 JPA LocalContainerEntityManagerFactoryBean 一样。 有关介绍,请参见 背景引导spring-doc.cadn.net.cn

LocalSessionFactoryBean 上,可通过 bootstrapExecutor 属性获得。在编程的 LocalSessionFactoryBuilder 上,一个重载的 buildSessionFactory 方法接受一个引导执行器参数。spring-doc.cadn.net.cn

5. 使用对象-XML映射器进行 XML 序列化

5.1. 简介

本章描述了Spring的面向对象-XML映射支持。面向对象-XML映射(简称O-X映射)是指将XML文档与对象之间进行转换的行为。这个转换过程也被称为XML打包,或XML序列化。本章中这些术语会交替使用。spring-doc.cadn.net.cn

在O-X映射领域,marshaller负责将对象(图)序列化为XML。以类似的方式,Unmarshaller将XML反序列化为对象图。此XML可以是DOM文档、输入或输出流,或者SAX处理器的形式。spring-doc.cadn.net.cn

使用 Spring 满足您的 O/X 映射需求的一些好处包括:spring-doc.cadn.net.cn

5.1.1. 配置的简便性

Spring的bean工厂使得配置marshaller变得很容易,而无需构造JAXB上下文、JiBX绑定工厂等。你可以像配置应用程序上下文中的其他bean一样配置marshaller。此外,许多marshaller还提供了基于XML命名空间的配置方式,使配置更加简单。spring-doc.cadn.net.cn

5.1.2. 一致的接口

Spring的O-X映射通过两个全局接口进行操作:MarshallerUnmarshaller。这些抽象使您能够相对轻松地在O-X映射框架之间切换,对执行序列化的类几乎不需要或不需要任何更改。这种方法的另一个优点是,可以以混合搭配的方式进行XML序列化(例如,一些序列化使用JAXB,另一些使用XStream),并且是以非侵入性的方式进行的,使您可以利用每种技术的优势。spring-doc.cadn.net.cn

5.1.3. 一致的异常层次结构

Spring 提供了从底层 O-X 映射工具的异常到其自身异常层次结构的转换,其中 XmlMappingException 作为根异常。 这些运行时异常会包装原始异常,以确保不丢失任何信息。spring-doc.cadn.net.cn

5.2. MarshallerUnmarshaller

如< a t="C0">简介中所述,marshaller 将对象序列化为 XML,而 unmarshaller 将 XML 流反序列化为对象。本节将介绍用于此目的的两个 Spring 接口。spring-doc.cadn.net.cn

5.2.1. 理解 Marshaller

Spring 将所有序列化操作都封装在 org.springframework.oxm.Marshaller 接口中,其主要方法如下:spring-doc.cadn.net.cn

Java
public interface Marshaller {

    /**
     * Marshal the object graph with the given root into the provided Result.
     */
    void marshal(Object graph, Result result) throws XmlMappingException, IOException;
}
Kotlin
interface Marshaller {

    /**
    * Marshal the object graph with the given root into the provided Result.
    */
    @Throws(XmlMappingException::class, IOException::class)
    fun marshal(
            graph: Any,
            result: Result
    )
}

The Marshaller interface 有一个主要方法,该方法将给定的对象转换为给定的 javax.xml.transform.Result。结果是一个标记接口,基本上表示 XML 输出抽象。具体的实现包装了各种 XML 表示形式,如下表所示:spring-doc.cadn.net.cn

结果实现 包装 XML 表示形式

DOMResultspring-doc.cadn.net.cn

org.w3c.dom.Nodespring-doc.cadn.net.cn

SAXResultspring-doc.cadn.net.cn

org.xml.sax.ContentHandlerspring-doc.cadn.net.cn

StreamResultspring-doc.cadn.net.cn

java.io.File, java.io.OutputStream, or java.io.Writerspring-doc.cadn.net.cn

虽然 marshal() 方法接受一个普通对象作为其第一个参数,但大多数 Marshaller 实现无法处理任意对象。相反,必须在映射文件中映射对象类,或用注解标记该对象,或将其注册到marshaller 中,或者该对象必须有一个公共的基类。请参阅本章后面的章节,以确定您的 O-X 技术如何处理此问题。

5.2.2. 理解 Unmarshaller

类似于 Marshaller,我们有 org.springframework.oxm.Unmarshaller 接口,如下列表所示:spring-doc.cadn.net.cn

Java
public interface Unmarshaller {

    /**
     * Unmarshal the given provided Source into an object graph.
     */
    Object unmarshal(Source source) throws XmlMappingException, IOException;
}
Kotlin
interface Unmarshaller {

    /**
    * Unmarshal the given provided Source into an object graph.
    */
    @Throws(XmlMappingException::class, IOException::class)
    fun unmarshal(source: Source): Any
}

此接口还有一个方法,该方法从给定的 javax.xml.transform.Source(XML输入抽象)读取并返回读取的对象。与 Result一样,Source是一个标记接口,有三个具体的实现类。每个实现类都包装了不同的XML表示形式,如下面的表格所示:spring-doc.cadn.net.cn

源代码实现 包装 XML 表示形式

DOMSourcespring-doc.cadn.net.cn

org.w3c.dom.Nodespring-doc.cadn.net.cn

SAXSourcespring-doc.cadn.net.cn

org.xml.sax.InputSource, 和 org.xml.sax.XMLReaderspring-doc.cadn.net.cn

StreamSourcespring-doc.cadn.net.cn

java.io.File, java.io.InputStream, or java.io.Readerspring-doc.cadn.net.cn

尽管有两个独立的序列化接口(MarshallerUnmarshaller),但 Spring-WS 中的所有实现都将在一个类中同时实现这两个接口。 这意味着您可以配置一个序列化器类,并在您的 applicationContext.xml 中将其同时作为序列化器和反序列化器引用。spring-doc.cadn.net.cn

5.2.3. 了解 XmlMappingException

Spring 会将底层 O-X 映射工具引发的异常转换为其自己的异常层次结构,其中 XmlMappingException 作为根异常。 这些运行时异常会包装原始异常,以确保不会丢失任何信息。spring-doc.cadn.net.cn

此外,MarshallingFailureExceptionUnmarshallingFailureException 在序列化和反序列化操作之间提供了区分,尽管底层的O-X映射工具并不这样做。spring-doc.cadn.net.cn

O-X 映射异常层次结构如下图所示:spring-doc.cadn.net.cn

oxm exceptions

5.3. 使用 MarshallerUnmarshaller

你可以使用 Spring 的 OXM 用于各种情况。在下面的例子中,我们使用它将 Spring 管理的应用程序的设置作为 XML 文件进行序列化。在下面的例子中,我们使用一个简单的 JavaBean 来表示这些设置:spring-doc.cadn.net.cn

Java
public class Settings {

    private boolean fooEnabled;

    public boolean isFooEnabled() {
        return fooEnabled;
    }

    public void setFooEnabled(boolean fooEnabled) {
        this.fooEnabled = fooEnabled;
    }
}
Kotlin
class Settings {
    var isFooEnabled: Boolean = false
}

应用程序类使用此bean来存储其设置。除了主方法外,该类还有两个方法: saveSettings() 将设置bean保存到名为 settings.xml 的文件中,而 loadSettings() 会重新加载这些设置。以下 main() 方法构建一个Spring应用上下文并调用这两个方法:spring-doc.cadn.net.cn

Java
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.oxm.Marshaller;
import org.springframework.oxm.Unmarshaller;

public class Application {

    private static final String FILE_NAME = "settings.xml";
    private Settings settings = new Settings();
    private Marshaller marshaller;
    private Unmarshaller unmarshaller;

    public void setMarshaller(Marshaller marshaller) {
        this.marshaller = marshaller;
    }

    public void setUnmarshaller(Unmarshaller unmarshaller) {
        this.unmarshaller = unmarshaller;
    }

    public void saveSettings() throws IOException {
        try (FileOutputStream os = new FileOutputStream(FILE_NAME)) {
            this.marshaller.marshal(settings, new StreamResult(os));
        }
    }

    public void loadSettings() throws IOException {
        try (FileInputStream is = new FileInputStream(FILE_NAME)) {
            this.settings = (Settings) this.unmarshaller.unmarshal(new StreamSource(is));
        }
    }

    public static void main(String[] args) throws IOException {
        ApplicationContext appContext =
                new ClassPathXmlApplicationContext("applicationContext.xml");
        Application application = (Application) appContext.getBean("application");
        application.saveSettings();
        application.loadSettings();
    }
}
Kotlin
class Application {

    lateinit var marshaller: Marshaller

    lateinit var unmarshaller: Unmarshaller

    fun saveSettings() {
        FileOutputStream(FILE_NAME).use { outputStream -> marshaller.marshal(settings, StreamResult(outputStream)) }
    }

    fun loadSettings() {
        FileInputStream(FILE_NAME).use { inputStream -> settings = unmarshaller.unmarshal(StreamSource(inputStream)) as Settings }
    }
}

private const val FILE_NAME = "settings.xml"

fun main(args: Array<String>) {
    val appContext = ClassPathXmlApplicationContext("applicationContext.xml")
    val application = appContext.getBean("application") as Application
    application.saveSettings()
    application.loadSettings()
}

Application 需要同时设置 marshallerunmarshaller 属性。我们可以通过使用以下 applicationContext.xml 来实现:spring-doc.cadn.net.cn

<beans>
    <bean id="application" class="Application">
        <property name="marshaller" ref="xstreamMarshaller" />
        <property name="unmarshaller" ref="xstreamMarshaller" />
    </bean>
    <bean id="xstreamMarshaller" class="org.springframework.oxm.xstream.XStreamMarshaller"/>
</beans>

此应用上下文使用XStream,但我们可以使用本章后面描述的任何其他marshaller实例。请注意,默认情况下,XStream不需要任何进一步的配置,因此bean定义相当简单。还要注意,XStreamMarshaller同时实现了MarshallerUnmarshaller,因此我们可以在应用的marshallerunmarshaller属性中引用xstreamMarshaller bean。spring-doc.cadn.net.cn

此示例应用程序生成以下 settings.xml 文件:spring-doc.cadn.net.cn

<?xml version="1.0" encoding="UTF-8"?>
<settings foo-enabled="false"/>

5.4. XML配置命名空间

你可以通过使用OXM命名空间中的标签更简洁地配置marshaller。 要使这些标签可用,你必须首先在XML配置文件的开头引用相应的模式。下面的例子展示了如何操作:spring-doc.cadn.net.cn

<?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 架构位置。

该模式提供了以下元素:spring-doc.cadn.net.cn

每个标签都在其对应的marshaller部分中进行了说明。例如,尽管如此,JAXB2 marshaller的配置可能如下所示:spring-doc.cadn.net.cn

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

5.5. JAXB

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

Spring 支持 JAXB 2.0 API 作为 XML 序列化策略,遵循在 MarshallerUnmarshaller 中描述的 MarshallerUnmarshaller 接口。 相应的集成类位于 org.springframework.oxm.jaxb 包中。spring-doc.cadn.net.cn

5.5.1. 使用 Jaxb2Marshaller

Jaxb2Marshaller 类实现了 Spring 的 MarshallerUnmarshaller 接口。它需要一个上下文路径才能运行。您可以通过设置 contextPath 属性来设置上下文路径。上下文路径是由冒号分隔的 Java 包名称列表,这些包包含派生的模式类。它还提供了一个 classesToBeBound 属性,允许您设置由 marshaller 支持的类数组。通过向 bean 指定一个或多个模式资源来进行模式验证,如下例所示:spring-doc.cadn.net.cn

<beans>
    <bean id="jaxb2Marshaller" class="org.springframework.oxm.jaxb.Jaxb2Marshaller">
        <property name="classesToBeBound">
            <list>
                <value>org.springframework.oxm.jaxb.Flight</value>
                <value>org.springframework.oxm.jaxb.Flights</value>
            </list>
        </property>
        <property name="schema" value="classpath:org/springframework/oxm/schema.xsd"/>
    </bean>

    ...

</beans>
XML 配置命名空间

jaxb2-marshaller 元素配置一个 org.springframework.oxm.jaxb.Jaxb2Marshaller, 如下面的示例所示:spring-doc.cadn.net.cn

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

或者,您可以使用class-to-be-bound子元素来提供要绑定到marshaller的类列表:spring-doc.cadn.net.cn

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

下表描述了可用的属性:spring-doc.cadn.net.cn

属性 描述 必填

idspring-doc.cadn.net.cn

marshaller 的 IDspring-doc.cadn.net.cn

Nospring-doc.cadn.net.cn

contextPathspring-doc.cadn.net.cn

JAXB 上下文路径spring-doc.cadn.net.cn

Nospring-doc.cadn.net.cn

5.6. JiBX

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

有关JiBX的更多信息,请参阅 JiBX网站。Spring集成类位于 org.springframework.oxm.jibx 包中。spring-doc.cadn.net.cn

5.6.1. 使用 JibxMarshaller

JibxMarshaller 类实现了 MarshallerUnmarshaller 接口。要正常运行,它需要要序列化的类的名称,您可以使用 targetClass 属性进行设置。可选地,您可以通过设置 bindingName 属性来指定绑定名称。在下面的示例中,我们绑定了 Flights 类:spring-doc.cadn.net.cn

<beans>
    <bean id="jibxFlightsMarshaller" class="org.springframework.oxm.jibx.JibxMarshaller">
        <property name="targetClass">org.springframework.oxm.jibx.Flights</property>
    </bean>
    ...
</beans>

一个 JibxMarshaller 用于配置单个类。如果您想要对多个类进行序列化,必须配置多个 JibxMarshaller 实例,并使用不同的 targetClass 属性值。spring-doc.cadn.net.cn

XML 配置命名空间

jibx-marshaller 标签将 org.springframework.oxm.jibx.JibxMarshaller 配置为, 如下例所示:spring-doc.cadn.net.cn

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

下表描述了可用的属性:spring-doc.cadn.net.cn

属性 描述 必填

idspring-doc.cadn.net.cn

marshaller 的 IDspring-doc.cadn.net.cn

Nospring-doc.cadn.net.cn

target-classspring-doc.cadn.net.cn

此marshaller的目标类spring-doc.cadn.net.cn

spring-doc.cadn.net.cn

bindingNamespring-doc.cadn.net.cn

此marshaller使用的绑定名称spring-doc.cadn.net.cn

Nospring-doc.cadn.net.cn

5.7. XStream

XStream 是一个简单的库,用于将对象序列化为 XML 并再次反序列化。它不需要任何映射,并生成干净的 XML。spring-doc.cadn.net.cn

有关XStream的更多信息,请参阅 XStream 网站。Spring集成类位于 org.springframework.oxm.xstream 包中。spring-doc.cadn.net.cn

5.7.1. 使用 XStreamMarshaller

XStreamMarshaller 不需要任何配置,可以直接在应用上下文中进行配置。要进一步自定义XML,可以设置一个别名映射,该映射由字符串别名映射到类组成,如下例所示:spring-doc.cadn.net.cn

<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,因为这可能导致安全漏洞。spring-doc.cadn.net.cn

如果您选择使用 XStreamMarshaller 从外部源反序列化 XML, 请在 supportedClasses 上设置 XStreamMarshaller 属性,如下例所示:spring-doc.cadn.net.cn

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

这样做可以确保只有已注册的类才适用于反序列化。spring-doc.cadn.net.cn

此外,您还可以注册 自定义 转换器,以确保只有您的支持类才能被反序列化。您可能希望在列表的最后添加一个 CatchAllConverter 作为转换器,除了那些显式支持应支持的领域类的转换器之外。结果是,优先级较低且可能存在安全漏洞的默认 XStream 转换器不会被调用。spring-doc.cadn.net.cn

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

附录

6.1. XML 架构

本附录的这一部分列出了数据访问的XML模式,包括以下内容:spring-doc.cadn.net.cn

6.1.1. tx 方案

tx 标签处理配置 Spring 事务全面支持中的所有这些 bean。这些标签将在名为 事务管理 的章节中进行介绍。spring-doc.cadn.net.cn

我们强烈建议您查看随Spring发行版一起提供的'spring-tx.xsd'文件。该文件包含Spring事务配置的XML Schema,并涵盖了tx命名空间中的各种元素,包括属性默认值和类似信息。此文件内联进行了文档说明,因此为了遵循DRY(不要重复自己)原则,此处不再重复相关信息。

为了确保完整性,要使用 tx 模式中的元素,你需要在 Spring XML 配置文件的顶部包含以下引言。以下片段中的文本引用了正确的模式,以便您能够使用 tx 命名空间中的标签:spring-doc.cadn.net.cn

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

    <!-- bean definitions here -->

</beans>
1 声明 tx 命名空间的用法。
2 指定位置(与其他模式位置一起)。
通常,当您使用 tx 命名空间中的元素时,也在使用 aop 命名空间中的元素(因为 Spring 的声明式事务支持是通过使用 AOP 实现的)。前面的 XML 代码片段包含引用 aop 模式的相关行,以便您可以使用 aop 命名空间中的元素。

6.1.2. jdbc 方案

jdbc 元素可让您快速配置嵌入式数据库或初始化现有数据源。这些元素分别在 嵌入式数据库支持初始化数据源 中有记录。spring-doc.cadn.net.cn

要使用 jdbc 架构中的元素,您需要在 Spring XML 配置文件的顶部包含以下前导内容。以下片段中的文本引用了正确的架构,以便您可以使用 jdbc 命名空间中的元素:spring-doc.cadn.net.cn

<?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 指定位置(与其他模式位置一起)。