# 数据访问
引用文档的这一部分涉及数据访问以及数据访问层和业务或服务层之间的交互。
Spring 的全面事务管理支持进行了一些详细的介绍,随后是对 Spring 框架与之集成的各种数据访问框架和技术的全面介绍。
# 1. 事务管理
全面的事务支持是使用 Spring 框架的最有说服力的理由之一。 Spring 框架为事务管理提供了一个一致的抽象,它提供了以下好处:
跨不同事务 API 的一致的编程模型,例如 爪哇 事务 API、JDBC、 Hibernate 和 爪哇 持久性 API( JPA)。
支持声明式事务管理。
用于纲领性的事务管理的比复杂事务 API(如 JTA)更简单的 API。
与 Spring 的数据访问抽象进行了出色的集成。
以下章节描述了 Spring 框架的事务特性和技术:
Advantages of the Spring Framework’s transaction support model描述了为什么要使用 Spring 框架的事务抽象,而不是使用 EJB 容器管理事务(CMT),或者选择通过专有 API(如 Hibernate)来驱动本地事务。
Understanding the Spring Framework transaction abstraction概述了核心类,并描述了如何从各种源配置和获取
DataSource
实例。将资源与事务同步描述了应用程序代码如何确保正确地创建、重用和清理资源。
声明式事务管理描述了对声明式事务管理的支持。
程序化事务管理涵盖了对程序化(即显式编码)事务管理的支持。
事务绑定事件描述如何在事务中使用应用程序事件。
# 1.1. Spring 框架的事务支持模型的优点
传统上,爪哇 EE 开发人员在事务管理方面有两种选择:全局事务或本地事务,这两种选择都有很大的局限性。在接下来的两节中,将介绍全局和本地事务管理,然后讨论 Spring 框架的事务管理支持如何解决全局和本地事务模型的局限性。
# 1.1.1.全球交易
全局事务允许你使用多个事务资源,通常是关系数据库和消息队列。应用程序服务器通过 JTA 管理全局事务,JTA 是一个繁琐的 API(部分原因是其异常模型)。此外,JTAUserTransaction
通常需要来自 JNDI,这意味着你还需要使用 JNDI 才能使用 JTA。全局事务的使用限制了应用程序代码的任何潜在重用,因为 JTA 通常仅在应用程序服务器环境中可用。
以前,使用全局事务的首选方式是通过 EJB CMT(容器管理事务)。CMT 是声明式事务管理的一种形式(区别于程序化事务管理)。EJB CMT 消除了对与事务相关的 JNDI 查找的需求,尽管 EJB 本身的使用需要使用 JNDI。它消除了编写 爪哇 代码来控制事务的大部分(但不是全部)需求。最大的缺点是,CMT 与 JTA 和应用程序服务器环境绑定在一起。而且,只有当你选择在 EJB 中实现业务逻辑(或者至少在事务性 EJB facade 的后面)时,它才可用。总的来说,EJB 的负面影响是如此之大,以至于这不是一个有吸引力的提议,尤其是在面对声明式事务管理的令人信服的替代方案时。
# 1.1.2.本地交易
本地事务是特定于资源的,例如与 JDBC 连接关联的事务。本地事务可能更容易使用,但有一个明显的缺点:它们不能跨多个事务资源工作。例如,通过使用 JDBC 连接来管理事务的代码不能在全局 JTA 事务中运行。由于应用程序服务器不参与事务管理,因此它无法帮助确保跨多个资源的正确性。(值得注意的是,大多数应用程序使用单个事务资源。)另一个缺点是本地事务对编程模型具有侵入性。
# 1.1.3. Spring 框架的一致性编程模型
Spring 解决了全局和本地事务的缺点。它允许应用程序开发人员在任何环境中使用一致的编程模型。你只需编写一次代码,就可以从不同环境中的不同事务管理策略中受益。 Spring 框架提供了声明式和程序化的事务管理。大多数用户更喜欢声明式事务管理,我们在大多数情况下推荐这种方法。
对于程序化事务管理,开发人员使用 Spring 框架事务抽象,它可以运行在任何底层事务基础设施上。对于优选的声明式模型,开发人员通常很少或根本不编写与事务管理相关的代码,因此,不依赖于 Spring Framework Transaction API 或任何其他事务 API。
你需要用于事务管理的应用程序服务器吗?
Spring 框架的事务管理支持改变了 Enterprise爪哇 应用程序何时需要应用服务器的传统规则。
特别是,你不需要一个应用程序服务器,它只用于通过 EJB 进行声明性事务。实际上,即使你的应用程序服务器具有强大的 JTA 功能,你也可能认为 Spring 框架的声明式事务提供了比 EJB CMT 更强大的功能和更高效的编程模型。
通常,只有当你的应用程序需要处理跨多个资源的事务时,你才需要应用程序服务器的 JTA 功能,而这对许多应用程序来说并不是必需的。许多高端应用程序使用单一的、高度可扩展的数据库(如 Oracle RAC)。独立事务管理器(例如Atomikos 交易 (opens new window)和JOTM (opens new window))是其他选项。当然,你可能需要其他应用程序服务器功能,例如 爪哇 消息服务和 爪哇 EE 连接器架构。
Spring 框架为你提供了何时将应用程序扩展到全负载应用程序服务器的选择。使用 EJB CMT 或 JTA 的唯一替代方法是使用本地事务(例如 JDBC 连接上的事务)编写代码,如果你需要在全局的、容器管理的事务中运行这些代码,那么你将面临大量的返工,这种情况已经一去不复返了。在 Spring 框架中,只有配置文件中的 Bean 定义中的一些需要更改(而不是代码)。
# 1.2.理解 Spring 框架事务抽象
Spring 事务抽象的关键是事务策略的概念。事务策略由TransactionManager
定义,特别是用于命令式事务管理的org.springframework.transaction.PlatformTransactionManager
接口和用于反应式事务管理的org.springframework.transaction.ReactiveTransactionManager
接口。下面的清单显示了PlatformTransactionManager
API 的定义:
public interface PlatformTransactionManager extends TransactionManager {
TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;
void commit(TransactionStatus status) throws TransactionException;
void rollback(TransactionStatus status) throws TransactionException;
}
这主要是一个服务提供程序接口,尽管你可以从应用程序代码中使用它程式设计。因为PlatformTransactionManager
是一个接口,所以可以根据需要轻松地对其进行模拟或截断。它不绑定到查找策略,例如 JNDI。实现像 Spring 框架 IoC 容器中的任何其他对象(或 Bean)一样定义。仅这一优点就使 Spring 框架事务成为一个有价值的抽象,即使在使用 JTA 的情况下也是如此。与直接使用 JTA 相比,你可以更容易地测试事务性代码。
同样,根据 Spring 的原理,可以由PlatformTransactionManager
接口的任何方法抛出的TransactionException
不受检查(也就是说,它扩展了java.lang.RuntimeException
类)。事务基础设施故障几乎总是致命的。在应用程序代码实际上可以从事务失败中恢复的极少数情况下,应用程序开发人员仍然可以选择捕获和处理TransactionException
。最重要的一点是,开发商并不是被迫这么做的。
getTransaction(..)
方法返回一个TransactionStatus
对象,取决于TransactionDefinition
参数。如果当前调用堆栈中存在匹配的事务,则返回的TransactionStatus
可能表示一个新事务,或者可以表示一个现有事务。后一种情况的含义是,与 爪哇 EE 事务上下文一样,TransactionStatus
与执行线程相关联。
在 Spring 框架 5.2 中, Spring 还提供了用于使用反应性类型或 Kotlin 协程的反应性应用程序的事务管理抽象。下面的清单显示了由org.springframework.transaction.ReactiveTransactionManager
定义的事务策略:
public interface ReactiveTransactionManager extends TransactionManager {
Mono<ReactiveTransaction> getReactiveTransaction(TransactionDefinition definition) throws TransactionException;
Mono<Void> commit(ReactiveTransaction status) throws TransactionException;
Mono<Void> rollback(ReactiveTransaction status) throws TransactionException;
}
反应式事务管理器主要是一个服务提供程序接口,尽管你可以从应用程序代码中使用它程式设计。因为ReactiveTransactionManager
是一个接口,所以可以根据需要轻松地对其进行模拟或删除。
TransactionDefinition
接口指定:
传播:通常,事务范围内的所有代码都在该事务中运行。但是,如果事务方法在事务上下文已经存在的情况下运行,则可以指定该行为。例如,代码可以在现有的事务中继续运行(常见的情况),或者可以暂停现有的事务并创建新的事务。 Spring 提供了从 EJB CMT 中熟悉的所有事务传播选项。要了解 Spring 中事务传播的语义,请参见事务传播。
隔离:此事务与其他事务的工作隔离的程度。例如,这个事务可以看到来自其他事务的未提交的写吗?
超时:此事务在超时和被底层事务基础设施自动回滚之前运行了多长时间。
只读状态:当你的代码读取但不修改数据时,你可以使用只读事务。只读事务在某些情况下可以是一种有用的优化,例如当你使用 Hibernate 时。
这些设置反映了标准的事务概念。如果有必要,请参考讨论事务隔离级别和其他核心事务概念的资源。理解这些概念对于使用 Spring 框架或任何事务管理解决方案是至关重要的。
TransactionStatus
接口为事务代码控制事务执行和查询事务状态提供了一种简单的方法。这些概念应该是熟悉的,因为它们是所有事务 API 所共有的。下面的清单显示了TransactionStatus
接口:
public interface TransactionStatus extends TransactionExecution, SavepointManager, Flushable {
@Override
boolean isNewTransaction();
boolean hasSavepoint();
@Override
void setRollbackOnly();
@Override
boolean isRollbackOnly();
void flush();
@Override
boolean isCompleted();
}
无论你是否 OPT 用于 Spring 中的声明式或程序化事务管理,定义正确的TransactionManager
实现是绝对必要的。你通常通过依赖注入来定义这个实现。
TransactionManager
实现通常需要了解它们工作的环境:JDBC、JTA、 Hibernate 等等。下面的示例展示了如何定义本地PlatformTransactionManager
实现(在本例中,使用普通的 JDBC)。
你可以通过创建类似于以下内容的 Bean 来定义 JDBCDataSource
:
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="${jdbc.driverClassName}" />
<property name="url" value="${jdbc.url}" />
<property name="username" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
</bean>
相关的PlatformTransactionManager
Bean 定义则具有对DataSource
定义的引用。它应该类似于以下示例:
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
如果在 爪哇 EE 容器中使用 JTA,则使用通过 JNDI 获得的容器DataSource
,并结合 Spring 的JtaTransactionManager
。下面的示例显示了 JTA 和 JNDI 查找版本的外观:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jee="http://www.springframework.org/schema/jee"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/jee
https://www.springframework.org/schema/jee/spring-jee.xsd">
<jee:jndi-lookup id="dataSource" jndi-name="jdbc/jpetstore"/>
<bean id="txManager" class="org.springframework.transaction.jta.JtaTransactionManager" />
<!-- other <bean/> definitions here -->
</beans>
JtaTransactionManager
不需要了解DataSource
(或任何其他特定资源),因为它使用容器的全局事务管理基础架构。
前面的dataSource Bean 的定义使用<jndi-lookup/> 标记来自 jee 名称空间。有关更多信息,请参见JEE 模式。 |
---|
如果你使用 JTA,那么你的事务管理器定义看起来应该是相同的,无论你使用的是什么数据访问技术,无论是 JDBC、 Hibernate JPA,还是任何其他受支持的技术。这是因为 JTA 事务是全局事务, 可以获取任何事务资源。 |
---|
在所有 Spring 事务设置中,应用程序代码不需要更改。你可以仅通过更改配置来更改事务的管理方式,即使这种更改意味着从本地事务转移到全局事务,或者反之亦然。
# 1.2.1. Hibernate 交易设置
还可以轻松地使用 Hibernate 本地事务,如以下示例所示。在这种情况下,你需要定义一个 Hibernate LocalSessionFactoryBean
,你的应用程序代码可以使用它来获得 Hibernate Session
实例。
DataSource
Bean 的定义类似于前面显示的本地 JDBC 示例,因此在下面的示例中没有显示。
如果DataSource (由任何非 JTA 事务管理器使用)通过JNDI 查找并由 爪哇 EE 容器管理,则它应该是非事务性的,因为 Spring 框架(而不是 爪哇 EE 容器)管理事务。 |
---|
在这种情况下,txManager
Bean 是HibernateTransactionManager
类型的。就像DataSourceTransactionManager
需要引用DataSource
一样,HibernateTransactionManager
也需要引用SessionFactory
。下面的示例声明sessionFactory
和txManager
bean:
<bean id="sessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="mappingResources">
<list>
<value>org/springframework/samples/petclinic/hibernate/petclinic.hbm.xml</value>
</list>
</property>
<property name="hibernateProperties">
<value>
hibernate.dialect=${hibernate.dialect}
</value>
</property>
</bean>
<bean id="txManager" class="org.springframework.orm.hibernate5.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory"/>
</bean>
如果使用 Hibernate 和 爪哇 EE 容器管理的 JTA 事务,则应该使用与上一个 JTA 示例中的 JDBC 相同的JtaTransactionManager
,如下例所示。另外,建议 Hibernate 通过 JTA 的事务协调器以及可能的连接释放模式配置来了解 JTA:
<bean id="sessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="mappingResources">
<list>
<value>org/springframework/samples/petclinic/hibernate/petclinic.hbm.xml</value>
</list>
</property>
<property name="hibernateProperties">
<value>
hibernate.dialect=${hibernate.dialect}
hibernate.transaction.coordinator_class=jta
hibernate.connection.handling_mode=DELAYED_ACQUISITION_AND_RELEASE_AFTER_STATEMENT
</value>
</property>
</bean>
<bean id="txManager" class="org.springframework.transaction.jta.JtaTransactionManager"/>
或者,你可以将JtaTransactionManager
传递到你的LocalSessionFactoryBean
中,以执行相同的默认值:
<bean id="sessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="mappingResources">
<list>
<value>org/springframework/samples/petclinic/hibernate/petclinic.hbm.xml</value>
</list>
</property>
<property name="hibernateProperties">
<value>
hibernate.dialect=${hibernate.dialect}
</value>
</property>
<property name="jtaTransactionManager" ref="txManager"/>
</bean>
<bean id="txManager" class="org.springframework.transaction.jta.JtaTransactionManager"/>
# 1.3.将资源与事务同步
如何创建不同的事务管理器以及它们如何链接到需要同步到事务的相关资源(例如DataSourceTransactionManager
到一个 JDBCDataSource
,HibernateTransactionManager
到一个 Hibernate SessionFactory
,等等)现在应该已经很清楚了。本节描述了应用程序代码如何(通过使用持久性 API(如 JDBC、 Hibernate 或 JPA)直接或间接地)确保正确地创建、重用和清理这些资源。本节还讨论了如何(可选地)通过相关的TransactionManager
触发事务同步。
# 1.3.1.高层同步方法
首选的方法是使用 Spring 最高级别的基于模板的持久性集成 API,或者使用带有事务感知工厂 bean 或代理的本地 ORM API 来管理本地资源工厂。这些事务感知解决方案在内部处理资源的创建和重用、清理、资源的可选事务同步以及异常映射。因此,用户数据访问代码不必处理这些任务,而可以完全关注于非样板持久化逻辑。通常,你使用本机 ORM API,或者通过使用JdbcTemplate
对 JDBC 访问采用模板方法。这些解决方案将在参考文档的后续部分中详细介绍。
# 1.3.2.低水平同步方法
诸如DataSourceUtils
(对于 JDBC)、EntityManagerFactoryUtils
(对于 JPA)、SessionFactoryUtils
(对于 Hibernate)等类存在于较低的级别。当你希望应用程序代码直接处理本机持久性 API 的资源类型时,可以使用这些类来确保获得适当的 Spring 框架管理的实例,(可选地)同步事务,以及将过程中发生的异常正确地映射到一致的 API。
例如,在 JDBC 的情况下,你可以使用 Spring 的org.springframework.jdbc.datasource.DataSourceUtils
类,而不是在getConnection()
上调用getConnection()
方法的传统 JDBC 方法,如下所示:
Connection conn = DataSourceUtils.getConnection(dataSource);
如果现有事务已经有一个与之同步(链接)的连接,则返回该实例。否则,方法调用将触发新连接的创建,该连接(可选地)与任何现有事务同步,并可用于该相同事务的后续重用。如前所述,任何SQLException
都包装在 Spring 框架CannotGetJdbcConnectionException
中,这是 Spring 框架的未检查DataAccessException
类型的层次结构之一。这种方法为你提供了比从SQLException
更容易获得的信息,并确保了跨数据库甚至跨不同持久性技术的可移植性。
该方法也可以在没有 Spring 事务管理的情况下工作(事务同步是可选的),因此无论是否使用 Spring 用于事务管理,都可以使用它。
当然,一旦使用了 Spring 的 JDBC 支持、 JPA 支持或 Hibernate 支持,通常不会使用DataSourceUtils
或其他 helper 类,因为与直接使用相关的 API 相比,通过 Spring 抽象进行工作要快乐得多。例如,如果你使用 Spring JdbcTemplate
或jdbc.object
包来简化对 JDBC 的使用,那么正确的连接检索将在幕后发生,并且你不需要编写任何特殊代码。
# 1.3.3.TransactionAwareDataSourceProxy
在最底层存在TransactionAwareDataSourceProxy
类。这是目标DataSource
的代理,该代理封装了目标DataSource
以添加对 Spring 管理事务的感知。在这方面,它类似于事务 JNDIDataSource
,由 爪哇 EE 服务器提供。
你几乎不需要或不想使用这个类,除非必须调用现有代码并传递标准的 JDBCDataSource
接口实现。在这种情况下,该代码可能是可用的,但它参与了 Spring 管理的事务。你可以使用前面提到的更高级别的抽象来编写新代码。
# 1.4.声明式事务管理
Spring 大多数框架用户选择声明式事务管理。此选项对应用程序代码的影响最小,因此最符合 非侵入式轻量级容器的理想。 |
---|
Spring 框架的声明性事务管理通过 Spring 面向方面的编程( AOP)而成为可能。然而,由于事务性方面的代码来自 Spring 框架分布并且可以以样板方式使用, AOP 概念通常不必被理解以有效地使用该代码。
Spring 框架的声明式事务管理类似于 EJB CMT,因为你可以指定事务行为(或缺少事务行为),直至单个方法级别。如果有必要,可以在事务上下文中进行setRollbackOnly()
调用。这两种类型的事务管理的区别在于:
与绑定到 JTA 的 EJB CMT 不同, Spring 框架的声明式事务管理可以在任何环境中工作。它可以通过使用 JDBC、 JPA 或 Hibernate 调整配置文件来处理 JTA 事务或本地事务。
你可以将 Spring 框架声明式事务管理应用于任何类,而不仅仅是 EJB 等特殊类。
Spring 框架提供了声明性回滚规则,这是一个没有 EJB 等价物的特性。提供了对回滚规则的编程支持和声明式支持。
Spring 框架允许你通过使用 AOP 自定义事务行为。例如,你可以在事务回滚的情况下插入自定义行为。你还可以添加任意的建议以及事务性建议。使用 EJB CMT,你不能影响容器的事务管理,除非使用
setRollbackOnly()
。Spring 框架不支持跨远程调用传播事务上下文,就像高端应用服务器所做的那样。如果你需要此功能,我们建议你使用 EJB。然而,在使用这样的特性之前要仔细考虑,因为通常情况下,人们不希望事务跨越远程调用。
回滚规则的概念很重要。它们允许你指定哪些异常(和 throwable)应该导致自动回滚。你可以在配置中,而不是在 爪哇 代码中,以声明式的方式指定此项。因此,尽管你仍然可以在TransactionStatus
对象上调用setRollbackOnly()
来回滚当前事务,但大多数情况下,你可以指定一条规则,即MyApplicationException
必须始终导致回滚。此选项的显著优点是业务对象不依赖于事务基础设施。例如,它们通常不需要导入 Spring 事务 API 或其他 Spring API。
虽然 EJB 容器的默认行为会在系统异常(通常是运行时异常)上自动回滚事务,但 EJB CMT 不会在应用程序异常(即除java.rmi.RemoteException
以外的检查异常)上自动回滚事务。虽然 Spring 声明性事务管理的默认行为遵循 EJB 约定(回滚仅在未检查的异常情况下是自动的),但定制这种行为通常是有用的。
# 1.4.1.理解 Spring 框架的声明式事务实现
仅仅告诉你使用@Transactional
注释对类进行注释,将@EnableTransactionManagement
添加到配置中,并期望你了解它是如何工作的,这是不够的。为了提供更深入的理解,本节将在与事务相关的问题的上下文中解释 Spring 框架的声明式事务基础结构的内部工作方式。
关于 Spring 框架的声明性事务支持,需要理解的最重要的概念是启用了via AOP proxies,并且事务建议是由元数据(目前是基于 XML 或注释)驱动的。 AOP 与事务性元数据的组合产生了 AOP 代理,该代理使用TransactionInterceptor
并结合适当的TransactionManager
实现来驱动围绕方法调用的事务。
Spring AOP 在the AOP section中涵盖。 |
---|
Spring Framework 的TransactionInterceptor
为命令式和反应式编程模型提供事务管理。拦截器通过检查方法返回类型来检测所需的事务管理风格。返回诸如Publisher
或 Kotlin Flow
之类的反应性类型的方法(或其中的一个子类型)符合进行反应性事务管理的条件。包括void
在内的所有其他返回类型都使用强制事务管理的代码路径。
事务管理风格会影响所需的事务管理器。命令式事务需要PlatformTransactionManager
,而反应式事务使用ReactiveTransactionManager
实现。
@Transactional 通常与PlatformTransactionManager 管理的线程绑定事务一起工作,将事务暴露给当前执行线程中的所有数据访问操作。注意:这将不是传播到方法内新启动的线程 。 由 ReactiveTransactionManager 管理的反应式事务使用反应器上下文而不是线程本地属性。因此,所有参与的数据访问 操作都需要在相同的反应性管道中的相同的反应器上下文中执行。 |
---|
下图显示了在事务代理上调用方法的概念视图:
# 1.4.2.声明式事务实现示例
考虑下面的接口及其附带的实现。这个示例使用Foo
和Bar
类作为占位符,这样你就可以专注于事务使用,而不必关注特定的域模型。对于本例的目的,DefaultFooService
类在每个实现的方法的主体中抛出UnsupportedOperationException
实例的事实是好的。这种行为允许你看到正在创建的事务,然后响应UnsupportedOperationException
实例进行回滚。下面的清单显示了FooService
接口:
爪哇
// the service interface that we want to make transactional
package x.y.service;
public interface FooService {
Foo getFoo(String fooName);
Foo getFoo(String fooName, String barName);
void insertFoo(Foo foo);
void updateFoo(Foo foo);
}
Kotlin
// the service interface that we want to make transactional
package x.y.service
interface FooService {
fun getFoo(fooName: String): Foo
fun getFoo(fooName: String, barName: String): Foo
fun insertFoo(foo: Foo)
fun updateFoo(foo: Foo)
}
下面的示例展示了前面接口的一个实现:
爪哇
package x.y.service;
public class DefaultFooService implements FooService {
@Override
public Foo getFoo(String fooName) {
// ...
}
@Override
public Foo getFoo(String fooName, String barName) {
// ...
}
@Override
public void insertFoo(Foo foo) {
// ...
}
@Override
public void updateFoo(Foo foo) {
// ...
}
}
Kotlin
package x.y.service
class DefaultFooService : FooService {
override fun getFoo(fooName: String): Foo {
// ...
}
override fun getFoo(fooName: String, barName: String): Foo {
// ...
}
override fun insertFoo(foo: Foo) {
// ...
}
override fun updateFoo(foo: Foo) {
// ...
}
}
假设FooService
接口的前两个方法getFoo(String)
和getFoo(String, String)
必须在具有只读语义的事务的上下文中运行,而其他方法insertFoo(Foo)
和updateFoo(Foo)
必须在具有读写语义的事务的上下文中运行。下面的几段将详细介绍以下配置:
<!-- from the file 'context.xml' -->
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
https://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- this is the service object that we want to make transactional -->
<bean id="fooService" class="x.y.service.DefaultFooService"/>
<!-- the transactional advice (what 'happens'; see the <aop:advisor/> bean below) -->
<tx:advice id="txAdvice" transaction-manager="txManager">
<!-- the transactional semantics... -->
<tx:attributes>
<!-- all methods starting with 'get' are read-only -->
<tx:method name="get*" read-only="true"/>
<!-- other methods use the default transaction settings (see below) -->
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
<!-- ensure that the above transactional advice runs for any execution
of an operation defined by the FooService interface -->
<aop:config>
<aop:pointcut id="fooServiceOperation" expression="execution(* x.y.service.FooService.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="fooServiceOperation"/>
</aop:config>
<!-- don't forget the DataSource -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="oracle.jdbc.driver.OracleDriver"/>
<property name="url" value="jdbc:oracle:thin:@rj-t42:1521:elvis"/>
<property name="username" value="scott"/>
<property name="password" value="tiger"/>
</bean>
<!-- similarly, don't forget the TransactionManager -->
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- other <bean/> definitions here -->
</beans>
检查前面的配置。它假定你希望创建一个事务性的服务对象fooService
Bean。要应用的事务语义封装在<tx:advice/>
定义中。<tx:advice/>
定义为“所有以get
开头的方法都将在只读事务的上下文中运行,所有其他方法都将使用默认事务语义运行”。<tx:advice/>
标记的transaction-manager
属性被设置为将用于驱动事务的TransactionManager
Bean 的名称(在本例中,txManager
Bean)。
如果要 中的 TransactionManager 线的 Bean 名为transactionManager ,则可以省略事务通知transaction-manager 中的transactionManager 属性。如果要连接的TransactionManager Bean 中的有任何其他名称,则必须显式地使用 transaction-manager 属性,如前面的示例所示。 |
---|
<aop:config/>
定义确保由txAdvice
Bean 定义的事务通知在程序的适当位置运行。首先,定义一个切入点,该切入点与FooService
接口(fooServiceOperation
)中定义的任何操作的执行相匹配。然后使用 advisor 将切入点与txAdvice
关联起来。结果表明,在执行fooServiceOperation
时,运行由txAdvice
定义的通知。
在<aop:pointcut/>
元素中定义的表达式是 AspectJ 切入点表达式。有关 Spring 中的切入点表达式的更多详细信息,请参见the AOP section。
一个常见的需求是使整个服务层具有事务性。实现此目的的最佳方法是更改切入点表达式,以匹配服务层中的任何操作。下面的示例展示了如何做到这一点:
<aop:config>
<aop:pointcut id="fooServiceMethods" expression="execution(* x.y.service.*.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="fooServiceMethods"/>
</aop:config>
在前面的示例中,假设你的所有服务接口都是在x.y.service 包中定义的。有关更多详细信息,请参见the AOP section。 |
---|
既然我们已经分析了配置,那么你可能会问自己:“所有这些配置实际上做了什么?”
前面显示的配置用于围绕根据fooService
Bean 定义创建的对象创建事务代理。该代理配置了事务通知,以便当在代理上调用适当的方法时,根据与该方法关联的事务配置,启动、挂起、标记为只读等等。考虑以下测试驱动前面显示的配置的程序:
爪哇
public final class Boot {
public static void main(final String[] args) throws Exception {
ApplicationContext ctx = new ClassPathXmlApplicationContext("context.xml");
FooService fooService = ctx.getBean(FooService.class);
fooService.insertFoo(new Foo());
}
}
Kotlin
import org.springframework.beans.factory.getBean
fun main() {
val ctx = ClassPathXmlApplicationContext("context.xml")
val fooService = ctx.getBean<FooService>("fooService")
fooService.insertFoo(Foo())
}
运行前一个程序的输出应该类似于以下内容(为清楚起见,对DefaultFooService
类的insertFoo(..)
方法抛出的UnsupportedOperationException
的 log4j 输出和堆栈跟踪进行了截断):
<!-- the Spring container is starting up... -->
[AspectJInvocationContextExposingAdvisorAutoProxyCreator] - Creating implicit proxy for bean 'fooService' with 0 common interceptors and 1 specific interceptors
<!-- the DefaultFooService is actually proxied -->
[JdkDynamicAopProxy] - Creating JDK dynamic proxy for [x.y.service.DefaultFooService]
<!-- ... the insertFoo(..) method is now being invoked on the proxy -->
[TransactionInterceptor] - Getting transaction for x.y.service.FooService.insertFoo
<!-- the transactional advice kicks in here... -->
[DataSourceTransactionManager] - Creating new transaction with name [x.y.service.FooService.insertFoo]
[DataSourceTransactionManager] - Acquired Connection [[email protected]] for JDBC transaction
<!-- the insertFoo(..) method from DefaultFooService throws an exception... -->
[RuleBasedTransactionAttribute] - Applying rules to determine whether transaction should rollback on java.lang.UnsupportedOperationException
[TransactionInterceptor] - Invoking rollback for transaction on x.y.service.FooService.insertFoo due to throwable [java.lang.UnsupportedOperationException]
<!-- and the transaction is rolled back (by default, RuntimeException instances cause rollback) -->
[DataSourceTransactionManager] - Rolling back JDBC transaction on Connection [[email protected]]
[DataSourceTransactionManager] - Releasing JDBC Connection after transaction
[DataSourceUtils] - Returning JDBC Connection to DataSource
Exception in thread "main" java.lang.UnsupportedOperationException at x.y.service.DefaultFooService.insertFoo(DefaultFooService.java:14)
<!-- AOP infrastructure stack trace elements removed for clarity -->
at $Proxy0.insertFoo(Unknown Source)
at Boot.main(Boot.java:11)
要使用反应式事务管理,代码必须使用反应式类型。
Spring 框架使用ReactiveAdapterRegistry 来确定方法的返回类型是否是反应性的。 |
---|
下面的清单显示了以前使用的FooService
的修改版本,但这次代码使用了反应类型:
爪哇
// the reactive service interface that we want to make transactional
package x.y.service;
public interface FooService {
Flux<Foo> getFoo(String fooName);
Publisher<Foo> getFoo(String fooName, String barName);
Mono<Void> insertFoo(Foo foo);
Mono<Void> updateFoo(Foo foo);
}
Kotlin
// the reactive service interface that we want to make transactional
package x.y.service
interface FooService {
fun getFoo(fooName: String): Flow<Foo>
fun getFoo(fooName: String, barName: String): Publisher<Foo>
fun insertFoo(foo: Foo) : Mono<Void>
fun updateFoo(foo: Foo) : Mono<Void>
}
下面的示例展示了前面接口的一个实现:
爪哇
package x.y.service;
public class DefaultFooService implements FooService {
@Override
public Flux<Foo> getFoo(String fooName) {
// ...
}
@Override
public Publisher<Foo> getFoo(String fooName, String barName) {
// ...
}
@Override
public Mono<Void> insertFoo(Foo foo) {
// ...
}
@Override
public Mono<Void> updateFoo(Foo foo) {
// ...
}
}
Kotlin
package x.y.service
class DefaultFooService : FooService {
override fun getFoo(fooName: String): Flow<Foo> {
// ...
}
override fun getFoo(fooName: String, barName: String): Publisher<Foo> {
// ...
}
override fun insertFoo(foo: Foo): Mono<Void> {
// ...
}
override fun updateFoo(foo: Foo): Mono<Void> {
// ...
}
}
对于事务边界和事务属性定义,命令式事务管理和反应式事务管理共享相同的语义。命令式事务和反应式事务之间的主要区别在于后者的延迟性。TransactionInterceptor
使用事务操作符装饰返回的反应类型,以开始并清理事务。因此,调用事务性反应方法将实际事务管理推迟到激活反应类型处理的订阅类型。
反应式事务管理的另一个方面涉及数据转义,这是编程模型的自然结果。
命令式事务的方法返回值在方法成功终止时从事务性方法返回,以便部分计算的结果不会逃脱方法闭包。
Active Transaction 方法返回一个 Active 包装器类型,该类型表示一个计算序列以及开始和完成计算的承诺。
Publisher
可以在事务正在进行但不一定完成时发出数据。因此,依赖于成功完成整个事务的方法需要确保调用代码中的完成和缓冲结果。
# 1.4.3.回滚声明性事务
上一节概述了如何在应用程序中以声明性的方式为类(通常是服务层类)指定事务设置的基础知识。本节介绍如何以简单的声明式方式控制事务的回滚。
向 Spring 框架的事务基础结构指示要回滚事务工作的推荐方法是,从当前在事务上下文中执行的代码中抛出Exception
。 Spring 框架的事务基础设施代码捕获任何未处理的Exception
,因为它在调用堆栈中冒泡,并决定是否将事务标记为回滚。
在其默认配置中, Spring 框架的事务基础设施代码仅在运行时未检查异常的情况下标记用于回滚的事务。也就是说,当抛出的异常是RuntimeException
的实例或子类时。(默认情况下,Error
实例也会导致回滚)。从事务方法抛出的已检查异常不会在默认配置中导致回滚。
你可以准确地配置哪些Exception
类型标记了一个用于回滚的事务,包括选中的异常。下面的 XML 片段演示了如何为选中的、特定于应用程序的Exception
类型配置回滚:
<tx:advice id="txAdvice" transaction-manager="txManager">
<tx:attributes>
<tx:method name="get*" read-only="true" rollback-for="NoProductInStockException"/>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
如果你不希望在抛出异常时回滚事务,也可以指定“没有回滚规则”。下面的示例告诉 Spring 框架的事务基础结构,即使面对未处理的InstrumentNotFoundException
,也要提交伴随的事务:
<tx:advice id="txAdvice">
<tx:attributes>
<tx:method name="updateStock" no-rollback-for="InstrumentNotFoundException"/>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
Spring 框架的事务基础设施捕捉到异常并参考配置的回滚规则以确定是否将事务标记为回滚时,最强的匹配规则胜出。因此,在以下配置的情况下,除InstrumentNotFoundException
以外的任何异常都会导致附带事务的回滚:
<tx:advice id="txAdvice">
<tx:attributes>
<tx:method name="*" rollback-for="Throwable" no-rollback-for="InstrumentNotFoundException"/>
</tx:attributes>
</tx:advice>
你还可以通过编程的方式指示所需的回滚。尽管很简单,但这个过程是非常具有侵入性的,并且将你的代码与 Spring 框架的事务基础结构紧密地结合在一起。下面的示例展示了如何以编程方式指示所需的回滚:
爪哇
public void resolvePosition() {
try {
// some business logic...
} catch (NoProductInStockException ex) {
// trigger rollback programmatically
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
}
Kotlin
fun resolvePosition() {
try {
// some business logic...
} catch (ex: NoProductInStockException) {
// trigger rollback programmatically
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
}
如果可能的话,我们强烈建议你使用声明式方法进行回滚。如果你绝对需要编程回滚,它是可用的,但是它的使用与实现一个干净的基于 POJO 的体系结构是背道而驰的。
# 1.4.4.为不同的 bean 配置不同的事务语义
考虑这样的场景:你有许多服务层对象,并且希望对每个对象应用完全不同的事务配置。你可以通过使用不同的pointcut
和advice-ref
属性值来定义不同的<aop:advisor/>
元素。
作为比较,首先假设你的所有服务层类都是在根x.y.service
包中定义的。要使在该包(或子包)中定义的类实例以及名称以Service
结尾的所有 bean 具有默认的事务配置,你可以编写以下内容:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
https://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<aop:config>
<aop:pointcut id="serviceOperation"
expression="execution(* x.y.service..*Service.*(..))"/>
<aop:advisor pointcut-ref="serviceOperation" advice-ref="txAdvice"/>
</aop:config>
<!-- these two beans will be transactional... -->
<bean id="fooService" class="x.y.service.DefaultFooService"/>
<bean id="barService" class="x.y.service.extras.SimpleBarService"/>
<!-- ... and these two beans won't -->
<bean id="anotherService" class="org.xyz.SomeService"/> <!-- (not in the right package) -->
<bean id="barManager" class="x.y.service.SimpleBarManager"/> <!-- (doesn't end in 'Service') -->
<tx:advice id="txAdvice">
<tx:attributes>
<tx:method name="get*" read-only="true"/>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
<!-- other transaction infrastructure beans such as a TransactionManager omitted... -->
</beans>
下面的示例展示了如何配置具有完全不同的事务设置的两个不同的 bean:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
https://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<aop:config>
<aop:pointcut id="defaultServiceOperation"
expression="execution(* x.y.service.*Service.*(..))"/>
<aop:pointcut id="noTxServiceOperation"
expression="execution(* x.y.service.ddl.DefaultDdlManager.*(..))"/>
<aop:advisor pointcut-ref="defaultServiceOperation" advice-ref="defaultTxAdvice"/>
<aop:advisor pointcut-ref="noTxServiceOperation" advice-ref="noTxAdvice"/>
</aop:config>
<!-- this bean will be transactional (see the 'defaultServiceOperation' pointcut) -->
<bean id="fooService" class="x.y.service.DefaultFooService"/>
<!-- this bean will also be transactional, but with totally different transactional settings -->
<bean id="anotherFooService" class="x.y.service.ddl.DefaultDdlManager"/>
<tx:advice id="defaultTxAdvice">
<tx:attributes>
<tx:method name="get*" read-only="true"/>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
<tx:advice id="noTxAdvice">
<tx:attributes>
<tx:method name="*" propagation="NEVER"/>
</tx:attributes>
</tx:advice>
<!-- other transaction infrastructure beans such as a TransactionManager omitted... -->
</beans>
# 1.4.5.<tx:advice/>设置
本节总结了你可以通过使用<tx:advice/>
标记来指定的各种事务设置。默认的<tx:advice/>
设置是:
传播设定是
REQUIRED.
隔离级别为
DEFAULT.
事务是可读写的。
事务超时默认为基础事务系统的默认超时,如果不支持超时,则无超时。
任何
RuntimeException
都会触发回滚,而任何选中的Exception
都不会。
你可以更改这些默认设置。下表总结了嵌套在<tx:advice/>
和<tx:attributes/>
标记中的<tx:method/>
标记的各种属性:
Attribute | Required? | Default | 说明 |
---|---|---|---|
name | Yes | 将事务属性与之关联的方法名。可以使用 通配符(*)字符将相同的事务属性 设置与许多方法关联(例如, get* ,handle* ,on*Event ,以及forth)。 | |
propagation | No | REQUIRED | 事务传播行为。 |
isolation | No | DEFAULT | 事务隔离级别。仅适用于REQUIRED 或REQUIRES_NEW 的传播设置。 |
timeout | No | -1 | 事务超时(秒)。仅适用于传播REQUIRED 或REQUIRES_NEW 。 |
read-only | No | false | 读写与只读事务。仅适用于REQUIRED 或REQUIRES_NEW 。 |
rollback-for | No | 触发回滚的Exception 实例的逗号分隔列表。例如,com.foo.MyBusinessException,ServletException 。 | |
no-rollback-for | No | 不触发回滚的Exception 实例的逗号分隔列表。例如,com.foo.MyBusinessException,ServletException 。 |
# 1.4.6.使用@Transactional
除了事务配置的基于 XML 的声明式方法之外,还可以使用基于注释的方法。直接在 爪哇 源代码中声明事务语义会使声明更接近受影响的代码。不存在太大的过度耦合的危险,因为本来打算用于交易的代码几乎总是以这种方式部署的。
标准的javax.transaction.Transactional 注释也被支持为 Spring 自己的注释的插入替换。有关更多详细信息,请参阅 JTA1.2 文档 。 |
---|
使用@Transactional
注释所提供的易用性最好用一个示例来说明,下面的文本对此进行了解释。考虑以下类定义:
爪哇
// the service class that we want to make transactional
@Transactional
public class DefaultFooService implements FooService {
@Override
public Foo getFoo(String fooName) {
// ...
}
@Override
public Foo getFoo(String fooName, String barName) {
// ...
}
@Override
public void insertFoo(Foo foo) {
// ...
}
@Override
public void updateFoo(Foo foo) {
// ...
}
}
Kotlin
// the service class that we want to make transactional
@Transactional
class DefaultFooService : FooService {
override fun getFoo(fooName: String): Foo {
// ...
}
override fun getFoo(fooName: String, barName: String): Foo {
// ...
}
override fun insertFoo(foo: Foo) {
// ...
}
override fun updateFoo(foo: Foo) {
// ...
}
}
在类级别上使用,如上面所述,该注释表示声明类的所有方法(以及其子类)的默认值。或者,每个方法都可以单独注释。有关方法 Spring 考虑事务性的更多详细信息,请参见[Method Visibility and@Transactional
](#Transaction-Declarative-Annotations-Method-Visibility)。请注意,类级注释不适用于类层次结构中的祖先类;在这种情况下,继承的方法需要在本地重新声明才能参与子类级注释。
当像上面这样的 POJO 类在 Spring 上下文中被定义为 Bean 时,可以通过@EnableTransactionManagement
类中的@Configuration
注释使 Bean 实例具有事务性。有关详细信息,请参见javadoc (opens new window)。
在 XML 配置中,<tx:annotation-driven/>
标记提供了类似的便利:
<!-- from the file 'context.xml' -->
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
https://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- this is the service object that we want to make transactional -->
<bean id="fooService" class="x.y.service.DefaultFooService"/>
<!-- enable the configuration of transactional behavior based on annotations -->
<!-- a TransactionManager is still required -->
<tx:annotation-driven transaction-manager="txManager"/> (1)
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- (this dependency is defined somewhere else) -->
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- other <bean/> definitions here -->
</beans>
1 | 使 Bean 实例具有事务性的行。 |
---|
如果要连接的TransactionManager 的 Bean 名称为transactionManager ,则可以在<tx:annotation-driven/> 标记中省略transaction-manager 属性。如果要依赖注入的TransactionManager Bean 中的有任何其他名称,则必须使用 transaction-manager 属性,如在前面的示例中所示。 |
---|
反应式事务方法使用反应式返回类型,而不是命令式编程安排,如下所示:
爪哇
// the reactive service class that we want to make transactional
@Transactional
public class DefaultFooService implements FooService {
@Override
public Publisher<Foo> getFoo(String fooName) {
// ...
}
@Override
public Mono<Foo> getFoo(String fooName, String barName) {
// ...
}
@Override
public Mono<Void> insertFoo(Foo foo) {
// ...
}
@Override
public Mono<Void> updateFoo(Foo foo) {
// ...
}
}
Kotlin
// the reactive service class that we want to make transactional
@Transactional
class DefaultFooService : FooService {
override fun getFoo(fooName: String): Flow<Foo> {
// ...
}
override fun getFoo(fooName: String, barName: String): Mono<Foo> {
// ...
}
override fun insertFoo(foo: Foo): Mono<Void> {
// ...
}
override fun updateFoo(foo: Foo): Mono<Void> {
// ...
}
}
请注意,对于返回的Publisher
有关于无功流抵消信号的特殊考虑因素。有关更多详细信息,请参见“使用 TransactionOperator”下的取消信号小节。
方法可见性和@Transactional 当你使用具有 Spring 的标准配置的事务代理时,你应该只对具有 可见性的方法应用 @Transactional 注释。如果你使用注释 protected 、private 或使用@Transactional 注释的包-可见方法,则不会引发错误,但是注释的方法不显示已配置的事务设置。如果你需要对非公共方法进行注释,请考虑下面一段中关于基于类的代理的提示,或者考虑使用 AspectJ Compile-Time 或Load-Time Weaving(稍后将进行说明)。在类中使用时, protected 或包-visible 方法也可以通过 注册一个自定义的 transactionAttributeSource Bean 来使基于类的代理具有事务性,就像下面的示例一样。注意,在基于接口的代理中,事务方法必须始终是 public 并在代理接口中定义。<br/>/**<br/> * Register a custom AnnotationTransactionAttributeSource with the<br/> * publicMethodsOnly flag set to false to enable support for<br/> * protected and package-private @Transactional methods in<br/> * class-based proxies.<br/> *<br/> * @see ProxyTransactionManagementConfiguration#transactionAttributeSource()<br/> */<br/>@Bean<br/>TransactionAttributeSource transactionAttributeSource() {<br/> return new AnnotationTransactionAttributeSource(false);<br/>}<br/> Spring TestContext Framework通过 默认支持非私有 @Transactional 测试方法。参见事务管理中的测试章节中的示例。 |
---|
你可以将@Transactional
注释应用于接口定义、接口上的方法、类定义或类上的方法。然而,仅仅存在@Transactional
注释并不足以激活事务行为。@Transactional
注释仅仅是元数据,它可以被一些运行时基础设施(@Transactional
)使用,并且可以使用元数据来配置具有事务行为的适当 bean。在前面的示例中,<tx:annotation-driven/>
元素在事务行为上进行切换。
Spring 团队建议你只使用@Transactional 注释来注释具体的类(以及具体类的方法),而不是注释接口, 你当然可以将 @Transactional 注释放在接口(或接口方法)上,但是,只有当你使用基于接口的 代理时,这才能正常工作。爪哇 注释不是从接口继承而来的事实意味着,如果你使用基于类的代理( proxy-target-class="true" )或基于编织的方面( mode="aspectj" ),代理和编织基础设施将无法识别事务设置,并且该对象不会包装在事务代理中。 |
---|
在代理模式(这是默认模式)中,只有通过 代理进入的外部方法调用才会被拦截。这意味着自我调用(实际上,在 中的一个方法调用目标对象的另一个方法)在运行时不会导致实际的 事务,即使调用的方法被标记为 @Transactional 。此外,代理必须完全初始化以提供预期的行为,因此你不应该在初始化代码中依赖 这个特性——例如,在 @PostConstruct 方法中。 |
---|
如果你希望使用事务来包装自调用,请考虑使用 AspectJ 模式(参见下表中的mode
属性)。在这种情况下,首先不存在代理。相反,目标类被编织(也就是说,它的字节码被修改)以支持在任何类型的方法上的@Transactional
运行时行为。
XML Attribute | Annotation Attribute | Default | 说明 |
---|---|---|---|
transaction-manager | N/A (see TransactionManagementConfigurer (opens new window) javadoc) | transactionManager | 要使用的事务管理器的名称。仅当事务 管理器的名称不是 transactionManager 时才需要,如前面的示例所示。 |
mode | mode | proxy | 默认模式(proxy )处理要通过使用 Spring 的 AOP 框架来处理注释的 bean(遵循代理语义,如前所述,应用于仅通过代理进入的方法调用 )。替代模式( aspectj )使用 Spring 的 AspectJ 事务方面来编织受影响的类,修改目标类 字节码以应用于任何类型的方法调用。AspectJ Weaving 在 Classpath 中需要 spring-aspects.jar 以及启用加载时编织(或编译时编织)。(有关如何设置加载时编织的详细信息,请参见Spring configuration。 |
proxy-target-class | proxyTargetClass | false | 仅适用于proxy 模式。控制为使用@Transactional 注释的类创建的事务代理类型。如果 proxy-target-class 属性设置为true ,则创建基于类的代理。如果 proxy-target-class 是false ,或者如果省略了该属性,则创建标准的基于接口的代理。(有关不同代理类型的详细检查,请参见代理机制。 |
order | order | Ordered.LOWEST_PRECEDENCE | 定义应用于使用@Transactional 注释的 bean 的事务通知的顺序。(有关与 AOP 通知的排序有关的规则的更多信息,请参见。)没有指定的排序意味着 AOP 子系统确定通知的顺序。 |
处理@Transactional 注释的默认通知模式是proxy ,,这仅允许通过代理拦截调用。在 相同的类内的本地调用不能以这种方式被拦截。对于更高级的拦截模式, 可以考虑结合编译时或加载时编织切换到 aspectj 模式。 |
---|
proxy-target-class 属性控制为使用@Transactional 注释的类创建的事务代理类型。如果 proxy-target-class 设置为true ,则创建基于类的代理。如果proxy-target-class 是false ,或者省略了该属性,则创建标准的基于接口的 JDK代理。(有关不同代理类型的讨论,请参见代理机制。 |
---|
@EnableTransactionManagement 和<tx:annotation-driven/> 仅在定义它们的相同应用程序上下文中的 bean 上查找@Transactional 。这意味着,如果将注释驱动的配置放在 WebApplicationContext 中,用于DispatcherServlet ,它仅在你的控制器中检查 @Transactional bean,而不是在你的服务中。有关更多信息,请参见MVC。 |
---|
在计算方法的事务设置时,派生最多的位置优先。在下面的示例中,DefaultFooService
类在类级别上使用只读事务的设置进行注释,但是在同一个类中的@Transactional
方法上的@Transactional
注释优先于在类级别上定义的事务设置。
爪哇
@Transactional(readOnly = true)
public class DefaultFooService implements FooService {
public Foo getFoo(String fooName) {
// ...
}
// these settings have precedence for this method
@Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW)
public void updateFoo(Foo foo) {
// ...
}
}
Kotlin
@Transactional(readOnly = true)
class DefaultFooService : FooService {
override fun getFoo(fooName: String): Foo {
// ...
}
// these settings have precedence for this method
@Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW)
override fun updateFoo(foo: Foo) {
// ...
}
}
# @Transactional
设置
@Transactional
注释是元数据,它指定接口、类或方法必须具有事务语义(例如,“在调用该方法时启动一个全新的只读事务,挂起任何现有事务”)。默认的@Transactional
设置如下:
传播设置为
PROPAGATION_REQUIRED.
隔离级别为
ISOLATION_DEFAULT.
事务是可读写的。
事务超时默认为基础事务系统的默认超时,如果不支持超时,则为零。
任何
RuntimeException
都会触发回滚,而任何选中的Exception
都不会。
你可以更改这些默认设置。下表总结了@Transactional
注释的各种属性:
Property | Type | 说明 |
---|---|---|
value | String | 指定要使用的事务管理器的可选限定符。 |
propagation | enum : Propagation | 可选的传播设置。 |
isolation | enum : Isolation | 可选隔离级别。仅适用于REQUIRED 或REQUIRES_NEW 的传播值。 |
timeout | int (in seconds of granularity) | 可选事务超时。仅适用于REQUIRED 或REQUIRES_NEW 的传播值。 |
readOnly | boolean | 读写与只读事务。仅适用于REQUIRED 或REQUIRES_NEW 的值。 |
rollbackFor | Array of Class objects, which must be derived from Throwable. | 必须引起回滚的异常类的可选数组. |
rollbackForClassName | Array of class names. The classes must be derived from Throwable. | 必须引起回滚的异常类名称的可选数组。 |
noRollbackFor | Array of Class objects, which must be derived from Throwable. | 不能导致回滚的异常类的可选数组. |
noRollbackForClassName | Array of String class names, which must be derived from Throwable. | 不能导致回滚的异常类名称的可选数组。 |
label | Array of String labels to add an expressive description to the transaction. | 标签可以由事务管理器进行评估,以将 实现特定的行为与实际事务相关联。 |
目前,你无法对事务的名称进行显式控制,其中“name”是指事务监视器(如果适用)(例如,WebLogic 的事务监视器)和日志输出中出现的事务名称。对于声明式事务,事务名称始终是完全限定类名称 +.
+ 事务建议类的方法名称。例如,如果BusinessService
类的handlePayment(..)
方法启动了一个事务,那么事务的名称将是:com.example.BusinessService.handlePayment
。
# 具有@Transactional
的多个事务管理器
Spring 大多数应用程序只需要一个事务管理器,但是可能存在希望在单个应用程序中有多个独立事务管理器的情况。你可以使用value
或transactionManager
注释的属性来指定要使用的TransactionManager
的标识。这可以是 Bean 名称,也可以是事务管理器 Bean 的限定符值。例如,使用限定符表示法,你可以在应用程序上下文中将以下 爪哇 代码与以下事务管理器 Bean 声明结合起来:
爪哇
public class TransactionalService {
@Transactional("order")
public void setSomething(String name) { ... }
@Transactional("account")
public void doSomething() { ... }
@Transactional("reactive-account")
public Mono<Void> doSomethingReactive() { ... }
}
Kotlin
class TransactionalService {
@Transactional("order")
fun setSomething(name: String) {
// ...
}
@Transactional("account")
fun doSomething() {
// ...
}
@Transactional("reactive-account")
fun doSomethingReactive(): Mono<Void> {
// ...
}
}
下面的清单显示了 Bean 项声明:
<tx:annotation-driven/>
<bean id="transactionManager1" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
...
<qualifier value="order"/>
</bean>
<bean id="transactionManager2" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
...
<qualifier value="account"/>
</bean>
<bean id="transactionManager3" class="org.springframework.data.r2dbc.connectionfactory.R2dbcTransactionManager">
...
<qualifier value="reactive-account"/>
</bean>
在这种情况下,TransactionalService
上的各个方法在单独的事务管理器下运行,由order
、account
和reactive-account
限定符区分。如果没有找到特别限定的TransactionManager
Bean,则仍然使用默认的<tx:annotation-driven>
目标名称transactionManager
。
# 自定义合成注释
如果你发现在许多不同的方法上重复使用与@Transactional
相同的属性,Spring’s meta-annotation support允许你为特定的用例定义自定义组合注释。例如,考虑以下注释定义:
爪哇
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Transactional(transactionManager = "order", label = "causal-consistency")
public @interface OrderTx {
}
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Transactional(transactionManager = "account", label = "retryable")
public @interface AccountTx {
}
Kotlin
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.TYPE)
@Retention(AnnotationRetention.RUNTIME)
@Transactional(transactionManager = "order", label = ["causal-consistency"])
annotation class OrderTx
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.TYPE)
@Retention(AnnotationRetention.RUNTIME)
@Transactional(transactionManager = "account", label = ["retryable"])
annotation class AccountTx
前面的注释让我们将上一节的示例写如下:
爪哇
public class TransactionalService {
@OrderTx
public void setSomething(String name) {
// ...
}
@AccountTx
public void doSomething() {
// ...
}
}
Kotlin
class TransactionalService {
@OrderTx
fun setSomething(name: String) {
// ...
}
@AccountTx
fun doSomething() {
// ...
}
}
在前面的示例中,我们使用语法来定义事务管理器限定符和事务标签,但也可以包括传播行为、回滚规则、超时和其他功能。
# 1.4.7.事务传播
本节描述 Spring 中事务传播的一些语义。请注意,本节不是事务传播的适当介绍。相反,它详细介绍了 Spring 中有关事务传播的一些语义。
在 Spring-管理事务中,要注意物理事务和逻辑事务之间的区别,以及传播设置如何应用于这种区别。
# 理解PROPAGATION_REQUIRED
PROPAGATION_REQUIRED
强制执行一个物理事务,如果当前范围还不存在事务,则在本地执行,或者参与为更大范围定义的现有“外部”事务。这是相同线程内的公共调用堆栈安排中的一个很好的默认设置(例如,一个服务 facade,它将委托给几个存储库方法,在这些存储库中,所有底层资源都必须参与服务级事务)。
默认情况下,参与事务将加入外部作用域的特性, 静默地忽略本地隔离级别、超时值,或只读标志(如果有的话)。 如果你希望在参与 具有不同隔离级别的现有事务时拒绝隔离级别声明,请考虑在事务 validateExistingTransactions 管理器上将true 标志切换为true 。这种不宽松的模式还拒绝只读不匹配(即,试图参与 只读外部作用域的内部读写事务)。 |
---|
当传播设置为PROPAGATION_REQUIRED
时,将为应用该设置的每个方法创建一个逻辑事务范围。每个这样的逻辑事务作用域可以单独确定仅回滚状态,外部事务作用域在逻辑上独立于内部事务作用域。在标准PROPAGATION_REQUIRED
行为的情况下,所有这些作用域都映射到相同的物理事务。因此,内部事务范围中设置的只回滚标记确实会影响外部事务实际提交的机会。
但是,在内部事务作用域设置仅回滚标记的情况下,外部事务本身并未决定回滚,因此回滚(由内部事务作用域静默触发)是意外的。在该点抛出相应的UnexpectedRollbackException
。这是一种预期的行为,这样事务的调用者就永远不会被误导,以为提交是在它实际上没有执行的时候执行的。因此,如果内部事务(外部调用方不知道该事务)静默地将事务标记为只回滚,则外部调用方仍然调用 commit。外部调用者需要接收UnexpectedRollbackException
,以清楚地表明执行了回滚。
# 理解PROPAGATION_REQUIRES_NEW
PROPAGATION_REQUIRES_NEW
,与PROPAGATION_REQUIRED
相反,总是对每个受影响的事务范围使用独立的物理事务,而不是参与外部范围的现有事务。在这种安排中,底层资源事务是不同的,因此可以独立地提交或回滚,外部事务不受内部事务的回滚状态的影响,内部事务的锁在完成后立即释放。这种独立的内部事务还可以声明自己的隔离级别、超时和只读设置,而不会继承外部事务的特征。
# 理解PROPAGATION_NESTED
PROPAGATION_NESTED
使用一个具有多个保存点的单个物理事务,它可以回滚到这些保存点。这样的部分回滚让内部事务作用域触发其作用域的回滚,外部事务能够继续物理事务,尽管有些操作已经回滚。该设置通常映射到 JDBC 保存点上,因此它仅适用于 JDBC 资源事务。参见 Spring 的[DataSourceTransactionManager
](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/jdbc/datasource/datasourcetransactionmanager.html)。
# 1.4.8.为交易业务提供咨询
假设你希望同时运行事务操作和一些基本的分析建议。在<tx:annotation-driven/>
的上下文中,如何实现这一点?
调用updateFoo(Foo)
方法时,你希望看到以下操作:
配置的分析方面开始。
事务性建议运行。
被建议对象上的方法运行。
事务提交。
分析方面报告了整个事务方法调用的确切持续时间。
本章不涉及对 AOP 进行任何详细的解释(除非 适用于交易)。关于 AOP 配置和 AOP 配置的详细覆盖范围,请参见AOP。 |
---|
下面的代码展示了前面讨论的简单分析方面:
爪哇
package x.y;
import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.util.StopWatch;
import org.springframework.core.Ordered;
public class SimpleProfiler implements Ordered {
private int order;
// allows us to control the ordering of advice
public int getOrder() {
return this.order;
}
public void setOrder(int order) {
this.order = order;
}
// this method is the around advice
public Object profile(ProceedingJoinPoint call) throws Throwable {
Object returnValue;
StopWatch clock = new StopWatch(getClass().getName());
try {
clock.start(call.toShortString());
returnValue = call.proceed();
} finally {
clock.stop();
System.out.println(clock.prettyPrint());
}
return returnValue;
}
}
Kotlin
class SimpleProfiler : Ordered {
private var order: Int = 0
// allows us to control the ordering of advice
override fun getOrder(): Int {
return this.order
}
fun setOrder(order: Int) {
this.order = order
}
// this method is the around advice
fun profile(call: ProceedingJoinPoint): Any {
var returnValue: Any
val clock = StopWatch(javaClass.name)
try {
clock.start(call.toShortString())
returnValue = call.proceed()
} finally {
clock.stop()
println(clock.prettyPrint())
}
return returnValue
}
}
建议的排序是通过Ordered
接口控制的。有关建议订购的详细信息,请参见建议订购。
下面的配置创建了一个fooService
Bean,该配置具有按所需顺序应用于它的分析和事务方面:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
https://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="fooService" class="x.y.service.DefaultFooService"/>
<!-- this is the aspect -->
<bean id="profiler" class="x.y.SimpleProfiler">
<!-- run before the transactional advice (hence the lower order number) -->
<property name="order" value="1"/>
</bean>
<tx:annotation-driven transaction-manager="txManager" order="200"/>
<aop:config>
<!-- this advice runs around the transactional advice -->
<aop:aspect id="profilingAspect" ref="profiler">
<aop:pointcut id="serviceMethodWithReturnValue"
expression="execution(!void x.y..*Service.*(..))"/>
<aop:around method="profile" pointcut-ref="serviceMethodWithReturnValue"/>
</aop:aspect>
</aop:config>
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="oracle.jdbc.driver.OracleDriver"/>
<property name="url" value="jdbc:oracle:thin:@rj-t42:1521:elvis"/>
<property name="username" value="scott"/>
<property name="password" value="tiger"/>
</bean>
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
</beans>
你可以以类似的方式配置任意数量的附加方面。
下面的示例创建了与前两个示例相同的设置,但使用了纯 XML 声明式方法:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
https://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="fooService" class="x.y.service.DefaultFooService"/>
<!-- the profiling advice -->
<bean id="profiler" class="x.y.SimpleProfiler">
<!-- run before the transactional advice (hence the lower order number) -->
<property name="order" value="1"/>
</bean>
<aop:config>
<aop:pointcut id="entryPointMethod" expression="execution(* x.y..*Service.*(..))"/>
<!-- runs after the profiling advice (cf. the order attribute) -->
<aop:advisor advice-ref="txAdvice" pointcut-ref="entryPointMethod" order="2"/>
<!-- order value is higher than the profiling aspect -->
<aop:aspect id="profilingAspect" ref="profiler">
<aop:pointcut id="serviceMethodWithReturnValue"
expression="execution(!void x.y..*Service.*(..))"/>
<aop:around method="profile" pointcut-ref="serviceMethodWithReturnValue"/>
</aop:aspect>
</aop:config>
<tx:advice id="txAdvice" transaction-manager="txManager">
<tx:attributes>
<tx:method name="get*" read-only="true"/>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
<!-- other <bean/> definitions such as a DataSource and a TransactionManager here -->
</beans>
上述配置的结果是一个fooService
Bean,它具有按该顺序应用于它的分析和事务方面。如果你希望在进入事务建议之后和退出事务建议之前运行分析建议,则可以交换分析方面 Bean 的order
属性的值,使其高于事务建议的订单值。
你可以以类似的方式配置其他方面。
# 1.4.9.使用@Transactional
与 AspectJ
你还可以通过 AspectJ 方面在 Spring 容器之外使用 Spring 框架的@Transactional
支持。为此,首先使用@Transactional
注释对类(以及可选的类的方法)进行注释,然后使用spring-aspects.jar
文件中定义的org.springframework.transaction.aspectj.AnnotationTransactionAspect
链接(编织)应用程序。你还必须使用事务管理器配置方面。你可以使用 Spring 框架的 IOC 容器来处理依赖关系-注入方面。配置事务管理方面的最简单方法是使用<tx:annotation-driven/>
元素,并将mode
属性指定为aspectj
,如[using@Transactional
](#transaction-declarative-annotations)中所述。因为我们在这里关注的是在 Spring 容器之外运行的应用程序,所以我们向你展示了如何以编程方式完成它。
在继续之前,你可能希望分别阅读[使用@Transactional ](#transaction-declarative-annotations)和AOP。 |
---|
下面的示例展示了如何创建事务管理器并配置AnnotationTransactionAspect
来使用它:
爪哇
// construct an appropriate transaction manager
DataSourceTransactionManager txManager = new DataSourceTransactionManager(getDataSource());
// configure the AnnotationTransactionAspect to use it; this must be done before executing any transactional methods
AnnotationTransactionAspect.aspectOf().setTransactionManager(txManager);
Kotlin
// construct an appropriate transaction manager
val txManager = DataSourceTransactionManager(getDataSource())
// configure the AnnotationTransactionAspect to use it; this must be done before executing any transactional methods
AnnotationTransactionAspect.aspectOf().transactionManager = txManager
当你使用这个方面时,你必须注释实现类(或该类中的方法 或两者),而不是该类实现的接口(如果有的话)。AspectJ 遵循 爪哇 的规则,即接口上的注释不会被继承。 |
---|
类上的@Transactional
注释指定了类中任何公共方法的执行的默认事务语义。
类中方法上的@Transactional
注释重写了类注释给出的默认事务语义(如果存在的话)。无论可见性如何,你都可以对任何方法进行注释。
要使用AnnotationTransactionAspect
编织应用程序,你必须使用 AspectJ 构建应用程序(请参见AspectJ 开发指南 (opens new window))或使用加载时编织。请参阅Load-time weaving with AspectJ in the Spring Framework,以了解有关使用 AspectJ 进行加载时编织的讨论。
# 1.5.程序化事务管理
Spring 框架通过使用以下工具提供了两种程序化交易管理手段:
TransactionTemplate
或TransactionalOperator
。直接实现
TransactionManager
。
Spring 团队通常推荐用于命令流中的程序化事务管理的TransactionTemplate
和用于反应性代码的TransactionalOperator
。第二种方法类似于使用 JTAUserTransaction
API,尽管异常处理不那么麻烦。
# 1.5.1.使用TransactionTemplate
TransactionTemplate
采用了与其他 Spring 模板相同的方法,例如JdbcTemplate
。它使用一种回调方法(将应用程序代码从必须执行样板获取和释放事务资源的过程中解放出来),并生成意图驱动的代码,因为你的代码只关注你想要做的事情。
正如下面的示例所示,使用TransactionTemplate 绝对将你与 Spring 的事务基础设施和 API 结合在一起。程序化的 事务管理是否适合你的开发需求是你自己必须做出的决定。 |
---|
必须在事务上下文中运行并显式使用TransactionTemplate
的应用程序代码类似于下一个示例。作为应用程序开发人员,你可以编写TransactionCallback
实现(通常表示为匿名内部类),其中包含你需要在事务上下文中运行的代码。然后,你可以将自定义TransactionCallback
的实例传递到execute(..)
方法上公开的TransactionTemplate
方法。下面的示例展示了如何做到这一点:
爪哇
public class SimpleService implements Service {
// single TransactionTemplate shared amongst all methods in this instance
private final TransactionTemplate transactionTemplate;
// use constructor-injection to supply the PlatformTransactionManager
public SimpleService(PlatformTransactionManager transactionManager) {
this.transactionTemplate = new TransactionTemplate(transactionManager);
}
public Object someServiceMethod() {
return transactionTemplate.execute(new TransactionCallback() {
// the code in this method runs in a transactional context
public Object doInTransaction(TransactionStatus status) {
updateOperation1();
return resultOfUpdateOperation2();
}
});
}
}
Kotlin
// use constructor-injection to supply the PlatformTransactionManager
class SimpleService(transactionManager: PlatformTransactionManager) : Service {
// single TransactionTemplate shared amongst all methods in this instance
private val transactionTemplate = TransactionTemplate(transactionManager)
fun someServiceMethod() = transactionTemplate.execute<Any?> {
updateOperation1()
resultOfUpdateOperation2()
}
}
如果没有返回值,你可以使用带有匿名类的方便的TransactionCallbackWithoutResult
类,如下所示:
爪哇
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
protected void doInTransactionWithoutResult(TransactionStatus status) {
updateOperation1();
updateOperation2();
}
});
Kotlin
transactionTemplate.execute(object : TransactionCallbackWithoutResult() {
override fun doInTransactionWithoutResult(status: TransactionStatus) {
updateOperation1()
updateOperation2()
}
})
回调中的代码可以通过在提供的TransactionStatus
对象上调用setRollbackOnly()
方法回滚事务,如下所示:
爪哇
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
protected void doInTransactionWithoutResult(TransactionStatus status) {
try {
updateOperation1();
updateOperation2();
} catch (SomeBusinessException ex) {
status.setRollbackOnly();
}
}
});
Kotlin
transactionTemplate.execute(object : TransactionCallbackWithoutResult() {
override fun doInTransactionWithoutResult(status: TransactionStatus) {
try {
updateOperation1()
updateOperation2()
} catch (ex: SomeBusinessException) {
status.setRollbackOnly()
}
}
})
# 指定事务设置
你可以在TransactionTemplate
上以编程方式或在配置中指定事务设置(例如传播模式、隔离级别、超时等等)。默认情况下,TransactionTemplate
实例具有默认事务设置。下面的示例显示了对特定TransactionTemplate:
的事务设置的程序化定制。
爪哇
public class SimpleService implements Service {
private final TransactionTemplate transactionTemplate;
public SimpleService(PlatformTransactionManager transactionManager) {
this.transactionTemplate = new TransactionTemplate(transactionManager);
// the transaction settings can be set here explicitly if so desired
this.transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_UNCOMMITTED);
this.transactionTemplate.setTimeout(30); // 30 seconds
// and so forth...
}
}
Kotlin
class SimpleService(transactionManager: PlatformTransactionManager) : Service {
private val transactionTemplate = TransactionTemplate(transactionManager).apply {
// the transaction settings can be set here explicitly if so desired
isolationLevel = TransactionDefinition.ISOLATION_READ_UNCOMMITTED
timeout = 30 // 30 seconds
// and so forth...
}
}
下面的示例通过使用 Spring XML 配置定义带有一些自定义事务设置的TransactionTemplate
:
<bean id="sharedTransactionTemplate"
class="org.springframework.transaction.support.TransactionTemplate">
<property name="isolationLevelName" value="ISOLATION_READ_UNCOMMITTED"/>
<property name="timeout" value="30"/>
</bean>
然后,你可以将sharedTransactionTemplate
注入到所需的尽可能多的服务中。
最后,TransactionTemplate
类的实例是线程安全的,因为实例不维护任何会话状态。TransactionTemplate
实例确实保持配置状态。因此,虽然许多类可能共享TransactionTemplate
的单个实例,但是如果一个类需要使用具有不同设置(例如,不同的隔离级别)的TransactionTemplate
,则需要创建两个不同的TransactionTemplate
实例。
# 1.5.2.使用TransactionOperator
TransactionOperator
遵循类似于其他无功运算符的运算符设计。它使用一种回调方法(将应用程序代码从必须执行样板获取和释放事务资源的过程中解放出来),并生成意图驱动的代码,因为你的代码只关注你想要做的事情。
正如下面的示例所示,使用TransactionOperator 绝对将你与 Spring 的事务基础设施和 API 结合在一起。程序化的 事务管理是否适合你的开发需求是你自己必须做出的决定。 |
---|
必须在事务上下文中运行并显式使用TransactionOperator
的应用程序代码类似于下一个示例:
爪哇
public class SimpleService implements Service {
// single TransactionOperator shared amongst all methods in this instance
private final TransactionalOperator transactionalOperator;
// use constructor-injection to supply the ReactiveTransactionManager
public SimpleService(ReactiveTransactionManager transactionManager) {
this.transactionOperator = TransactionalOperator.create(transactionManager);
}
public Mono<Object> someServiceMethod() {
// the code in this method runs in a transactional context
Mono<Object> update = updateOperation1();
return update.then(resultOfUpdateOperation2).as(transactionalOperator::transactional);
}
}
Kotlin
// use constructor-injection to supply the ReactiveTransactionManager
class SimpleService(transactionManager: ReactiveTransactionManager) : Service {
// single TransactionalOperator shared amongst all methods in this instance
private val transactionalOperator = TransactionalOperator.create(transactionManager)
suspend fun someServiceMethod() = transactionalOperator.executeAndAwait<Any?> {
updateOperation1()
resultOfUpdateOperation2()
}
}
TransactionalOperator
可以通过两种方式使用:
操作员式使用项目反应堆类型(
mono.as(transactionalOperator::transactional)
)其他情况的回调样式(
transactionalOperator.execute(TransactionCallback<T>)
)
回调中的代码可以通过在提供的ReactiveTransaction
对象上调用setRollbackOnly()
方法回滚事务,如下所示:
爪哇
transactionalOperator.execute(new TransactionCallback<>() {
public Mono<Object> doInTransaction(ReactiveTransaction status) {
return updateOperation1().then(updateOperation2)
.doOnError(SomeBusinessException.class, e -> status.setRollbackOnly());
}
}
});
Kotlin
transactionalOperator.execute(object : TransactionCallback() {
override fun doInTransactionWithoutResult(status: ReactiveTransaction) {
updateOperation1().then(updateOperation2)
.doOnError(SomeBusinessException.class, e -> status.setRollbackOnly())
}
})
# 取消信号
在反应流中,aSubscriber
可以取消其Subscription
并停止其Publisher
。Project Reactor 中的操作员,以及其他库中的操作员,例如next()
,take(long)
,timeout(Duration)
,以及其他人可以发出取消通知。没有办法知道取消的原因,无论是由于错误还是仅仅是缺乏进一步消费的兴趣。由于版本 5.3 取消信号导致回滚。因此,重要的是要考虑从事务Publisher
向下游使用的运算符。特别是在 aFlux
或其它多值Publisher
的情况下,必须消耗完整的输出以允许事务完成。
# 指定事务设置
你可以为TransactionalOperator
指定事务设置(例如传播模式、隔离级别、超时等等)。默认情况下,TransactionalOperator
实例具有默认事务设置。下面的示例显示了针对特定TransactionalOperator:
的事务设置的自定义。
Java
public class SimpleService implements Service {
private final TransactionalOperator transactionalOperator;
public SimpleService(ReactiveTransactionManager transactionManager) {
DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
// the transaction settings can be set here explicitly if so desired
definition.setIsolationLevel(TransactionDefinition.ISOLATION_READ_UNCOMMITTED);
definition.setTimeout(30); // 30 seconds
// and so forth...
this.transactionalOperator = TransactionalOperator.create(transactionManager, definition);
}
}
Kotlin
class SimpleService(transactionManager: ReactiveTransactionManager) : Service {
private val definition = DefaultTransactionDefinition().apply {
// the transaction settings can be set here explicitly if so desired
isolationLevel = TransactionDefinition.ISOLATION_READ_UNCOMMITTED
timeout = 30 // 30 seconds
// and so forth...
}
private val transactionalOperator = TransactionalOperator(transactionManager, definition)
}
# 1.5.3.使用TransactionManager
下面的部分解释命令式事务管理器和反应式事务管理器的编程使用。
# 使用PlatformTransactionManager
对于命令式事务,可以直接使用org.springframework.transaction.PlatformTransactionManager
来管理事务。要做到这一点,通过 Bean 引用将你使用的PlatformTransactionManager
的实现传递给你的 Bean。然后,通过使用TransactionDefinition
和TransactionStatus
对象,你可以发起事务、回滚和提交。下面的示例展示了如何做到这一点:
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。然后,通过使用TransactionDefinition
和ReactiveTransaction
对象,你可以发起事务、回滚和提交。下面的示例展示了如何做到这一点:
Java
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
// explicitly setting the transaction name is something that can be done only programmatically
def.setName("SomeTxName");
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
Mono<ReactiveTransaction> reactiveTx = txManager.getReactiveTransaction(def);
reactiveTx.flatMap(status -> {
Mono<Object> tx = ...; // put your business logic here
return tx.then(txManager.commit(status))
.onErrorResume(ex -> txManager.rollback(status).then(Mono.error(ex)));
});
Kotlin
val def = DefaultTransactionDefinition()
// explicitly setting the transaction name is something that can be done only programmatically
def.setName("SomeTxName")
def.propagationBehavior = TransactionDefinition.PROPAGATION_REQUIRED
val reactiveTx = txManager.getReactiveTransaction(def)
reactiveTx.flatMap { status ->
val tx = ... // put your business logic here
tx.then(txManager.commit(status))
.onErrorResume { ex -> txManager.rollback(status).then(Mono.error(ex)) }
}
# 1.6.在程序化事务管理和声明式事务管理之间进行选择
只有当你有少量的事务操作时,程序化事务管理通常才是一个好主意。例如,如果你有一个 Web 应用程序,该应用程序仅需要用于某些更新操作的事务,那么你可能不希望通过使用 Spring 或任何其他技术来设置事务代理。在这种情况下,使用TransactionTemplate
可能是一种很好的方法。能够显式地设置事务名称也只能通过使用程序化的事务管理方法来完成。
另一方面,如果你的应用程序有许多事务操作,声明式事务管理通常是值得的。它使事务管理脱离了业务逻辑,并且不难配置。 Spring 当使用框架而不是 EJB CMT 时,声明式事务管理的配置成本大大降低。
# 1.7.事务绑定事件
从 Spring 4.2 开始,事件的侦听器可以绑定到事务的一个阶段。典型的示例是在事务成功完成时处理事件。当当前事务的结果对侦听器确实很重要时,这样做可以更灵活地使用事件。
你可以使用@EventListener
注释来注册一个常规的事件侦听器。如果需要将其绑定到事务,请使用@TransactionalEventListener
。这样做时,默认情况下侦听器将绑定到事务的提交阶段。
下一个示例展示了这个概念。假设一个组件发布了一个命令创建的事件,并且我们希望定义一个侦听器,该侦听器只应在发布该组件的事务成功提交之后处理该事件。下面的示例设置了这样的事件侦听器:
Java
@Component
public class MyComponent {
@TransactionalEventListener
public void handleOrderCreatedEvent(CreationEvent<Order> creationEvent) {
// ...
}
}
Kotlin
@Component
class MyComponent {
@TransactionalEventListener
fun handleOrderCreatedEvent(creationEvent: CreationEvent<Order>) {
// ...
}
}
@TransactionalEventListener
注释公开了一个phase
属性,该属性允许你定制侦听器应该绑定到的事务的阶段。有效的阶段是BEFORE_COMMIT
,AFTER_COMMIT
(默认),AFTER_ROLLBACK
,以及AFTER_COMPLETION
,它们聚合了事务完成(无论是提交还是回滚)。
如果没有事务正在运行,则根本不会调用侦听器,因为我们无法遵守所需的语义。但是,你可以通过将注释的fallbackExecution
属性设置为true
来覆盖该行为。
@TransactionalEventListener 仅适用于由PlatformTransactionManager 管理的线程绑定事务。由ReactiveTransactionManager 管理的反应性事务使用反应器上下文而不是线程本地属性,因此从事件侦听器的角度来看,它不能参与兼容的活动事务。 |
---|
# 1.8.特定于应用服务器的集成
Spring 的事务抽象通常与应用服务器无关。此外, Spring 的JtaTransactionManager
类(它可以选择性地为 JTAUserTransaction
和TransactionManager
对象执行 JNDI 查找)自动检测后一个对象的位置,该位置因应用服务器的不同而不同。对 JTATransactionManager
的访问允许增强事务语义——特别是支持事务挂起。详情见[JtaTransactionManager
](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/transaction/jta/jtatransactionmanager.html)Javadoc。
Spring 的JtaTransactionManager
是在 Java EE 应用程序服务器上运行的标准选择,并且已知可在所有公共服务器上运行。高级功能(如事务挂起)也可以在许多服务器上运行(包括 GlassFish、JBoss 和 Geronimo),而不需要任何特殊配置。然而,对于完全支持的事务挂起和进一步的高级集成, Spring 包括用于 WebLogic Server 和 WebSphere 的特殊适配器。这些适配器将在下面的小节中进行讨论。
对于包括 WebLogic Server 和 WebSphere 在内的标准场景,可以考虑使用方便的<tx:jta-transaction-manager/>
配置元素。在进行配置时,此元素会自动检测基础服务器,并为平台选择最佳的事务管理器。这意味着你不需要显式地配置特定于服务器的适配器类(如以下小节中讨论的那样)。相反,它们是自动选择的,标准的JtaTransactionManager
是默认的回退。
# 1.8.1.IBMWebSphere
在 WebSphere6.1.0.9 及以上版本中,推荐使用的 Spring JTA 事务管理器是WebSphereUowTransactionManager
。这个特殊的适配器使用 IBM 的UOWManager
API,它在 WebSphereApplicationServer6.1.0.9 及更高版本中可用。有了这个适配器, Spring 驱动的事务暂停(由PROPAGATION_REQUIRES_NEW
发起的挂起和恢复)得到了 IBM 的正式支持。
# 1.8.2.Oracle WebLogic 服务器
在 WebLogic Server9.0 或更高版本中,通常使用WebLogicJtaTransactionManager
而不是 stockJtaTransactionManager
类。普通JtaTransactionManager
的这个特殊的特定于 WebLogic 的子类在 WebLogic 管理的事务环境中支持 Spring 事务定义的全部功能,而不是标准的 JTA 语义。功能包括事务名称、每个事务隔离级别,以及在所有情况下正确地恢复事务。
# 1.9.常见问题的解决方案
这一节描述了一些常见问题的解决方案。
# 1.9.1.对特定的DataSource
#### 使用错误的事务管理器
根据事务技术和需求的选择,使用正确的PlatformTransactionManager
实现。 Spring 框架使用得当,仅提供了一种直接的、可移植的抽象。如果使用全局事务,则必须对所有事务操作使用org.springframework.transaction.jta.JtaTransactionManager
类(或其特定于应用服务器的子类)。否则,事务基础结构将尝试在容器DataSource
实例等资源上执行本地事务。这样的本地事务是没有意义的,一个好的应用程序服务器会将它们视为错误。
# 1.10.更多资源
有关 Spring 框架的事务支持的更多信息,请参见:
Distributed transactions in Spring, with and without XA (opens new window)是一个 JavaWorld 演示文稿,其中 Spring 的 David Syer 指导你了解 Spring 应用程序中分布式事务的七种模式,其中三种有 XA,四种没有 XA。
*Java 事务设计策略 * (opens new window)是一本可从InfoQ (opens new window)获得的书,它提供了关于 Java 事务的快节奏的介绍。它还包括关于如何配置和使用 Spring 框架和 EJB3 的事务的并排示例。
# 2. DAO 支持
Spring 中的数据访问对象支持旨在使其易于以一致的方式与数据访问技术(例如 JDBC、 Hibernate 或 JPA)一起工作。这使你可以相当容易地在上述持久性技术之间进行切换,并且还允许你编写代码,而无需担心捕获特定于每种技术的异常。
# 2.1.一致的异常层次结构
Spring 提供了一种从技术特定的异常(例如)到其自身的异常类层次结构的方便转换,其具有将作为根异常的特性。这些异常包装了原始异常,这样就不会有任何风险,你可能会丢失任何有关可能出了什么问题的信息。
除了 JDBC 异常之外, Spring 还可以包装 JPA 和 Hibernate 特定的异常,将它们转换为一组集中的运行时异常。这样,你就可以只在适当的层中处理大多数不可恢复的持久性异常,而不需要在 DAO 中有烦人的样板抓取和抛出块和异常声明。正如上面提到的,JDBC 异常(包括特定于数据库的方言)也被转换为相同的层次结构,这意味着你可以在一致的编程模型中使用 JDBC 执行一些操作。
前面的讨论适用于 Spring 对各种 ORM 框架的支持中的各种模板类。如果使用基于拦截器的类,则应用程序必须关心如何处理HibernateExceptions
和PersistenceExceptions
本身,最好是将其分别委托给convertHibernateAccessException(..)
或convertJpaAccessException(..)
中的SessionFactoryUtils
方法。这些方法将异常转换为与org.springframework.dao
异常层次结构中的异常兼容的异常。由于PersistenceExceptions
未被选中,它们也可能被抛出(尽管在异常方面牺牲了泛型 DAO 抽象)。
下面的图像显示了 Spring 提供的异常层次结构。(请注意,图像中详细介绍的类层次结构只显示了整个DataAccessException
层次结构的一个子集。
# 2.2.用于配置 DAO 或存储库类的注释
保证你的数据访问对象或存储库提供异常转换的最佳方法是使用@Repository
注释。这种注释还允许组件扫描支持查找和配置你的 DAO 和存储库,而无需为它们提供 XML 配置条目。下面的示例展示了如何使用@Repository
注释:
Java
@Repository (1)
public class SomeMovieFinder implements MovieFinder {
// ...
}
1 | @Repository 注释。 |
---|
Kotlin
@Repository (1)
class SomeMovieFinder : MovieFinder {
// ...
}
1 | @Repository 注释。 |
---|
任何 DAO 或存储库实现都需要访问持久性资源,这取决于所使用的持久性技术。例如,基于 JDBC 的存储库需要访问 JDBCDataSource
,而基于 JPA 的存储库需要访问EntityManager
。实现这一点的最简单方法是通过使用@Autowired
、@Inject
、@Resource
或@PersistenceContext
注释中的一个来注入这种资源依赖关系。下面的示例适用于 JPA 存储库:
Java
@Repository
public class JpaMovieFinder implements MovieFinder {
@PersistenceContext
private EntityManager entityManager;
// ...
}
Kotlin
@Repository
class JpaMovieFinder : MovieFinder {
@PersistenceContext
private lateinit var entityManager: EntityManager
// ...
}
如果使用经典的 Hibernate API,则可以插入SessionFactory
,如下例所示:
Java
@Repository
public class HibernateMovieFinder implements MovieFinder {
private SessionFactory sessionFactory;
@Autowired
public void setSessionFactory(SessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
}
// ...
}
Kotlin
@Repository
class HibernateMovieFinder(private val sessionFactory: SessionFactory) : MovieFinder {
// ...
}
我们在这里展示的最后一个示例是典型的 JDBC 支持。你可以将DataSource
注入到初始化方法或构造函数中,在这里你将通过使用JdbcTemplate
创建JdbcTemplate
和其他数据访问支持类(例如SimpleJdbcCall
和其他类)。以下示例自动连接DataSource
:
Java
@Repository
public class JdbcMovieFinder implements MovieFinder {
private JdbcTemplate jdbcTemplate;
@Autowired
public void init(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
// ...
}
Kotlin
@Repository
class JdbcMovieFinder(dataSource: DataSource) : MovieFinder {
private val jdbcTemplate = JdbcTemplate(dataSource)
// ...
}
有关如何 配置应用程序上下文以利用这些注释的详细信息,请参见每个持久性技术的具体覆盖范围。 |
---|
# 3. 使用 JDBC 进行数据访问
Spring 框架 JDBC 抽象所提供的值可能最好通过下表中概述的动作序列来显示。该表显示了 Spring 处理哪些操作以及哪些操作是你的责任。
行动 | Spring | You |
---|---|---|
定义连接参数。 | X | |
打开连接。 | X | |
指定 SQL 语句。 | X | |
声明参数并提供参数值 | X | |
准备并运行该语句。 | X | |
设置循环以遍历结果(如果有的话)。 | X | |
完成每个迭代的工作。 | X | |
处理任何异常。 | X | |
处理交易。 | X | |
关闭连接、语句和结果集。 | X |
Spring 框架负责处理所有可能使 JDBC 成为如此乏味的 API 的底层细节。
# 3.1.一种 JDBC 数据库访问方法的选择
你可以在几种方法中进行选择,以形成你的 JDBC 数据库访问的基础。除了JdbcTemplate
的三种风格之外,新的SimpleJdbcInsert
和SimpleJdbcCall
方法优化了数据库元数据,并且 RDBMS 对象风格采用了一种类似于 JDO 查询设计的更面向对象的方法。一旦你开始使用这些方法之一,你仍然可以进行混合和匹配,以包含来自不同方法的功能。所有的方法都需要一个符合 JDBC2.0 的驱动程序,而一些高级特性则需要一个 JDBC3.0 驱动程序。
JdbcTemplate
是经典且最流行的 Spring JDBC 方法。这种“最底层”的方法和所有其他方法都使用了 JDBCtemplate。NamedParameterJdbcTemplate
封装一个JdbcTemplate
以提供命名参数,而不是传统的 JDBC?
占位符。当你对 SQL 语句有多个参数时,这种方法提供了更好的文档和易用性。SimpleJdbcInsert
和SimpleJdbcCall
优化数据库元数据,以限制所需配置的数量。这种方法简化了编码,因此只需要提供表或过程的名称,并提供匹配列名称的参数映射。只有当数据库提供了足够的元数据时,这种方法才会起作用。如果数据库不提供此元数据,则必须提供参数的显式配置。RDBMS 对象——包括
MappingSqlQuery
、SqlUpdate
和StoredProcedure
——要求你在数据访问层的初始化过程中创建可重用和线程安全的对象。这种方法是仿照 JDO 查询建模的,在 JDO 查询中,你定义查询字符串、声明参数并编译查询。一旦这样做,execute(…)
、update(…)
和findObject(…)
方法就可以使用各种参数值多次调用。
# 3.2.包层次结构
Spring 框架的 JDBC 抽象框架由四个不同的包组成:
core
:org.springframework.jdbc.core
包包含JdbcTemplate
类及其各种回调接口,以及各种相关的类。名为org.springframework.jdbc.core.simple
的子包包含SimpleJdbcInsert
和SimpleJdbcCall
类。另一个名为org.springframework.jdbc.core.namedparam
的子包包含NamedParameterJdbcTemplate
类和相关的支持类。参见使用 JDBC 核心类来控制基本的 JDBC 处理和错误处理,JDBC 批处理操作,以及[用SimpleJdbc
类简化 JDBC 操作]。datasource
:org.springframework.jdbc.datasource
包包含一个用于 easyDataSource
访问的实用程序类和各种简单的DataSource
实现,你可以使用这些实现在 Java EE 容器之外测试和运行未经修改的 JDBC 代码。一个名为org.springfamework.jdbc.datasource.embedded
的子包提供了通过使用 Java 数据库引擎创建嵌入式数据库的支持,例如 HSQL、H2 和 Derby。见控制数据库连接和嵌入式数据库支持。object
:org.springframework.jdbc.object
包包含表示 RDBMS 查询、更新和存储过程的类,这些类是线程安全的、可重用的对象。见将 JDBC 操作建模为 Java 对象。这种方法是由 JDO 建模的,尽管查询返回的对象自然地与数据库断开连接。这个较高级别的 JDBC 抽象依赖于org.springframework.jdbc.core
包中的较低级别的抽象。support
:org.springframework.jdbc.support
包提供了SQLException
翻译功能和一些实用程序类。在 JDBC 处理过程中抛出的异常被转换为org.springframework.dao
包中定义的异常。这意味着使用 Spring JDBC 抽象层的代码不需要实现 JDBC 或 RDBMS 特定的错误处理。所有转换后的异常都未被选中,这使你可以选择捕获异常,以便在允许将其他异常传播给调用方的同时恢复这些异常。参见[使用SQLExceptionTranslator
]。
# 3.3.使用 JDBC 核心类来控制基本的 JDBC 处理和错误处理
本节介绍如何使用 JDBC 核心类来控制基本的 JDBC 处理,包括错误处理。它包括以下主题:
[使用
JdbcTemplate
](#jdbc-jdbctemplate)[使用
NamedParameterJdbcTemplate
](#jdbc-namedparameterjdbctemplate)[使用
SQLExceptionTranslator
]
# 3.3.1.使用JdbcTemplate
JdbcTemplate
是 JDBC 核心包中的中心类。它处理资源的创建和释放,这有助于你避免常见的错误,例如忘记关闭连接。它执行核心 JDBC 工作流的基本任务(例如语句的创建和执行),留下应用程序代码来提供 SQL 和提取结果。JdbcTemplate
类:
运行 SQL 查询
更新语句和存储过程调用
在
ResultSet
实例上执行迭代并提取返回的参数值。捕获 JDBC 异常,并将它们转换为在
org.springframework.dao
包中定义的通用的、信息量更大的异常层次结构。(见一致的异常层次结构。
当你为代码使用JdbcTemplate
时,你只需要实现回调接口,并为它们提供一个明确定义的契约。给定由JdbcTemplate
类提供的Connection
,PreparedStatementCreator
回调接口将创建一个准备好的语句,提供 SQL 和任何必要的参数。对于创建可调用语句的CallableStatementCreator
接口也是如此。RowCallbackHandler
接口从ResultSet
的每一行中提取值。
你可以通过使用DataSource
引用的直接实例化在 DAO 实现中使用JdbcTemplate
,或者可以在 Spring IoC 容器中配置它,并将其作为 Bean 引用提供给 DAO。
DataSource 应该始终配置为 Spring IOC 容器中的 Bean。在中,第一种情况将 Bean 直接赋予服务;在第二种情况下,将 赋予准备好的模板。 |
---|
这个类发布的所有 SQL 都记录在DEBUG
级别的目录下,该目录对应于模板实例的完全限定类名称(通常为JdbcTemplate
,但如果使用JdbcTemplate
类的自定义子类,则可能会有所不同)。
下面的部分提供了JdbcTemplate
用法的一些示例。这些示例并不是JdbcTemplate
公开的所有功能的详尽列表。参见乘务员javadoc (opens new window)。
# 查询(SELECT
)
下面的查询获取关系中的行数:
Java
int rowCount = this.jdbcTemplate.queryForObject("select count(*) from t_actor", Integer.class);
Kotlin
val rowCount = jdbcTemplate.queryForObject<Int>("select count(*) from t_actor")!!
以下查询使用了一个绑定变量:
Java
int countOfActorsNamedJoe = this.jdbcTemplate.queryForObject(
"select count(*) from t_actor where first_name = ?", Integer.class, "Joe");
Kotlin
val countOfActorsNamedJoe = jdbcTemplate.queryForObject<Int>(
"select count(*) from t_actor where first_name = ?", arrayOf("Joe"))!!
下面的查询查找String
:
Java
String lastName = this.jdbcTemplate.queryForObject(
"select last_name from t_actor where id = ?",
String.class, 1212L);
Kotlin
val lastName = this.jdbcTemplate.queryForObject<String>(
"select last_name from t_actor where id = ?",
arrayOf(1212L))!!
下面的查询查找并填充一个域对象:
Java
Actor actor = jdbcTemplate.queryForObject(
"select first_name, last_name from t_actor where id = ?",
(resultSet, rowNum) -> {
Actor newActor = new Actor();
newActor.setFirstName(resultSet.getString("first_name"));
newActor.setLastName(resultSet.getString("last_name"));
return newActor;
},
1212L);
Kotlin
val actor = jdbcTemplate.queryForObject(
"select first_name, last_name from t_actor where id = ?",
arrayOf(1212L)) { rs, _ ->
Actor(rs.getString("first_name"), rs.getString("last_name"))
}
以下查询查找并填充域对象列表:
Java
List<Actor> actors = this.jdbcTemplate.query(
"select first_name, last_name from t_actor",
(resultSet, rowNum) -> {
Actor actor = new Actor();
actor.setFirstName(resultSet.getString("first_name"));
actor.setLastName(resultSet.getString("last_name"));
return actor;
});
Kotlin
val actors = jdbcTemplate.query("select first_name, last_name from t_actor") { rs, _ ->
Actor(rs.getString("first_name"), rs.getString("last_name"))
如果最后两个代码片段实际上存在于同一个应用程序中,那么删除两个RowMapper
lambda 表达式中存在的重复并将它们提取到一个字段中是有意义的,然后可以根据需要由 DAO 方法引用。例如,将前面的代码片段编写如下可能更好:
Java
private final RowMapper<Actor> actorRowMapper = (resultSet, rowNum) -> {
Actor actor = new Actor();
actor.setFirstName(resultSet.getString("first_name"));
actor.setLastName(resultSet.getString("last_name"));
return actor;
};
public List<Actor> findAllActors() {
return this.jdbcTemplate.query("select first_name, last_name from t_actor", actorRowMapper);
}
Kotlin
val actorMapper = RowMapper<Actor> { rs: ResultSet, rowNum: Int ->
Actor(rs.getString("first_name"), rs.getString("last_name"))
}
fun findAllActors(): List<Actor> {
return jdbcTemplate.query("select first_name, last_name from t_actor", actorMapper)
}
# 更新(INSERT
,UPDATE
,和DELETE
)与JdbcTemplate
可以使用update(..)
方法执行插入、更新和删除操作。参数值通常以变量参数或对象数组的形式提供。
下面的示例插入一个新条目:
Java
this.jdbcTemplate.update(
"insert into t_actor (first_name, last_name) values (?, ?)",
"Leonor", "Watling");
Kotlin
jdbcTemplate.update(
"insert into t_actor (first_name, last_name) values (?, ?)",
"Leonor", "Watling")
下面的示例更新了一个现有条目:
Java
this.jdbcTemplate.update(
"update t_actor set last_name = ? where id = ?",
"Banjo", 5276L);
Kotlin
jdbcTemplate.update(
"update t_actor set last_name = ? where id = ?",
"Banjo", 5276L)
下面的示例删除一个条目:
Java
this.jdbcTemplate.update(
"delete from t_actor where id = ?",
Long.valueOf(actorId));
Kotlin
jdbcTemplate.update("delete from t_actor where id = ?", actorId.toLong())
# 其它JdbcTemplate
操作
你可以使用execute(..)
方法来运行任意 SQL。因此,该方法通常用于 DDL 语句。它被接受回调接口、绑定变量数组等的变量严重超载。下面的示例创建了一个表:
Java
this.jdbcTemplate.execute("create table mytable (id integer, name varchar(100))");
Kotlin
jdbcTemplate.execute("create table mytable (id integer, name varchar(100))")
下面的示例调用一个存储过程:
爪哇
this.jdbcTemplate.update(
"call SUPPORT.REFRESH_ACTORS_SUMMARY(?)",
Long.valueOf(unionId));
Kotlin
jdbcTemplate.update(
"call SUPPORT.REFRESH_ACTORS_SUMMARY(?)",
unionId.toLong())
更复杂的存储过程支持是稍后覆盖。
# JdbcTemplate
最佳实践
一旦配置好,JdbcTemplate
类的实例是线程安全的。这很重要,因为这意味着你可以配置JdbcTemplate
的单个实例,然后将这个共享引用安全地注入到多个 DAO(或存储库)中。JdbcTemplate
是有状态的,因为它维护了对DataSource
的引用,但该状态不是会话状态。
在使用JdbcTemplate
类(以及相关的[NamedParameterJdbcTemplate
](#jdbc-namedParameterJDbcTemplate)类时,一种常见的做法是在 Spring 配置文件中配置DataSource
,然后在依赖项中-将共享的DataSource
Bean 注入到 DAO 类中。在 setter 中为DataSource
创建了JdbcTemplate
。这导致 DAO 类似于以下内容:
爪哇
public class JdbcCorporateEventDao implements CorporateEventDao {
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
// JDBC-backed implementations of the methods on the CorporateEventDao follow...
}
Kotlin
class JdbcCorporateEventDao(dataSource: DataSource) : CorporateEventDao {
private val jdbcTemplate = JdbcTemplate(dataSource)
// JDBC-backed implementations of the methods on the CorporateEventDao follow...
}
下面的示例展示了相应的 XML 配置:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
<bean id="corporateEventDao" class="com.example.JdbcCorporateEventDao">
<property name="dataSource" ref="dataSource"/>
</bean>
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="${jdbc.driverClassName}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<context:property-placeholder location="jdbc.properties"/>
</beans>
显式配置的另一种选择是使用组件扫描和注释支持来进行依赖项注入。在这种情况下,你可以使用@Repository
对类进行注释(这使它成为组件扫描的候选项),并使用DataSource
setter 方法对@Autowired
进行注释。下面的示例展示了如何做到这一点:
爪哇
@Repository (1)
public class JdbcCorporateEventDao implements CorporateEventDao {
private JdbcTemplate jdbcTemplate;
@Autowired (2)
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource); (3)
}
// JDBC-backed implementations of the methods on the CorporateEventDao follow...
}
1 | 用@Repository 注释类。 |
---|---|
2 | 用@Autowired 注释DataSource setter 方法。 |
3 | 用DataSource 创建一个新的JdbcTemplate 。 |
Kotlin
@Repository (1)
class JdbcCorporateEventDao(dataSource: DataSource) : CorporateEventDao { (2)
private val jdbcTemplate = JdbcTemplate(dataSource) (3)
// JDBC-backed implementations of the methods on the CorporateEventDao follow...
}
1 | 用@Repository 注释类。 |
---|---|
2 | 构造函数注入DataSource 。 |
3 | 创建一个新的JdbcTemplate ,并使用NamedParameterJdbcTemplate 。 |
下面的示例展示了相应的 XML 配置:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
<!-- Scans within the base package of the application for @Component classes to configure as beans -->
<context:component-scan base-package="org.springframework.docs.test" />
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="${jdbc.driverClassName}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<context:property-placeholder location="jdbc.properties"/>
</beans>
如果你使用 Spring 的JdbcDaoSupport
类,并且你的各种支持 JDBC 的 DAO 类从中扩展,那么你的子类将从setDataSource(..)
类继承一个setDataSource(..)
方法。你可以选择是否从这个类继承。JdbcDaoSupport
类仅作为一种方便而提供。
无论你选择使用(或不使用)上述哪种模板初始化样式,每次要运行 SQL 时,都很少需要创建JdbcTemplate
类的新实例。一旦配置好,JdbcTemplate
实例就是线程安全的。如果你的应用程序访问多个数据库,你可能需要多个JdbcTemplate
实例,这需要多个DataSources
实例,然后需要多个配置不同的JdbcTemplate
实例。
# 3.3.2.使用NamedParameterJdbcTemplate
NamedParameterJdbcTemplate
类增加了对使用命名参数编程 JDBC 语句的支持,而不是仅使用经典占位符('?'
)参数编程 JDBC 语句。NamedParameterJdbcTemplate
类包装了 aJdbcTemplate
,并将其委托给包装好的JdbcTemplate
,以完成其大部分工作。本节仅描述NamedParameterJdbcTemplate
类中与JdbcTemplate
本身不同的区域——即通过使用命名参数编程 JDBC 语句。下面的示例展示了如何使用NamedParameterJdbcTemplate
:
爪哇
// some JDBC-backed DAO class...
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
}
public int countOfActorsByFirstName(String firstName) {
String sql = "select count(*) from T_ACTOR where first_name = :first_name";
SqlParameterSource namedParameters = new MapSqlParameterSource("first_name", firstName);
return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Integer.class);
}
Kotlin
private val namedParameterJdbcTemplate = NamedParameterJdbcTemplate(dataSource)
fun countOfActorsByFirstName(firstName: String): Int {
val sql = "select count(*) from T_ACTOR where first_name = :first_name"
val namedParameters = MapSqlParameterSource("first_name", firstName)
return namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Int::class.java)!!
}
请注意,在分配给sql
变量的值和插入namedParameters
变量(类型MapSqlParameterSource
)的对应值中使用了命名参数表示法。
或者,你可以使用基于Map
的样式,将命名参数及其对应值传递给NamedParameterJdbcTemplate
实例。由NamedParameterJdbcOperations
公开并由NamedParameterJdbcTemplate
类实现的其余方法遵循类似的模式,在此不涉及。
下面的示例展示了基于Map
的样式的使用:
爪哇
// some JDBC-backed DAO class...
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
}
public int countOfActorsByFirstName(String firstName) {
String sql = "select count(*) from T_ACTOR where first_name = :first_name";
Map<String, String> namedParameters = Collections.singletonMap("first_name", firstName);
return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Integer.class);
}
Kotlin
// some JDBC-backed DAO class...
private val namedParameterJdbcTemplate = NamedParameterJdbcTemplate(dataSource)
fun countOfActorsByFirstName(firstName: String): Int {
val sql = "select count(*) from T_ACTOR where first_name = :first_name"
val namedParameters = mapOf("first_name" to firstName)
return namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Int::class.java)!!
}
与NamedParameterJdbcTemplate
(存在于同一个 爪哇 包中)相关的一个不错的特性是SqlParameterSource
接口。你已经在前面的代码片段(MapSqlParameterSource
类)中看到了该接口实现的示例。SqlParameterSource
是NamedParameterJdbcTemplate
的命名参数值的来源。MapSqlParameterSource
类是一个简单的实现,它是一个围绕java.util.Map
的适配器,其中键是参数名称,值是参数值。
另一个SqlParameterSource
实现是BeanPropertySqlParameterSource
类。这个类包装一个任意的 爪哇Bean(即一个坚持爪哇Bean 公约 (opens new window)的类的实例),并使用包装好的 爪哇Bean 的属性作为命名参数值的源。
下面的示例展示了一个典型的 爪哇Bean:
爪哇
public class Actor {
private Long id;
private String firstName;
private String lastName;
public String getFirstName() {
return this.firstName;
}
public String getLastName() {
return this.lastName;
}
public Long getId() {
return this.id;
}
// setters omitted...
}
Kotlin
data class Actor(val id: Long, val firstName: String, val lastName: String)
下面的示例使用NamedParameterJdbcTemplate
返回前面示例中所示的类成员的计数:
爪哇
// some JDBC-backed DAO class...
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
}
public int countOfActors(Actor exampleActor) {
// notice how the named parameters match the properties of the above 'Actor' class
String sql = "select count(*) from T_ACTOR where first_name = :firstName and last_name = :lastName";
SqlParameterSource namedParameters = new BeanPropertySqlParameterSource(exampleActor);
return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Integer.class);
}
Kotlin
// some JDBC-backed DAO class...
private val namedParameterJdbcTemplate = NamedParameterJdbcTemplate(dataSource)
private val namedParameterJdbcTemplate = NamedParameterJdbcTemplate(dataSource)
fun countOfActors(exampleActor: Actor): Int {
// notice how the named parameters match the properties of the above 'Actor' class
val sql = "select count(*) from T_ACTOR where first_name = :firstName and last_name = :lastName"
val namedParameters = BeanPropertySqlParameterSource(exampleActor)
return namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Int::class.java)!!
}
请记住,NamedParameterJdbcTemplate
类封装了一个经典的JdbcTemplate
模板。如果你需要访问打包的JdbcTemplate
实例以访问仅在JdbcTemplate
类中存在的功能,则可以使用getJdbcOperations()
方法通过JdbcOperations
接口访问打包的JdbcTemplate
实例。
另请参阅[JdbcTemplate
最佳实践](#jdbc-jdbctemplate-idioms),以获取在应用程序上下文中使用NamedParameterJdbcTemplate
类的指导方针。
# 3.3.3.使用SQLExceptionTranslator
NamedParameterJdbcTemplate
是一个由类实现的接口,它可以在SQLException
s 和 Spring 自己的org.springframework.dao.DataAccessException
之间转换,这在数据访问策略方面是不可知的。实现可以是通用的(例如,为 JDBC 使用 SQLState 代码),也可以是专有的(例如,使用 Oracle 错误代码),以获得更高的精度。
SQLErrorCodeSQLExceptionTranslator
是默认使用的SQLExceptionTranslator
的实现。该实现使用特定的供应商代码。它比SQLState
实现更精确。错误代码转换是基于一个名为SQLErrorCodes
的 爪哇Bean 类型类中的代码。该类由SQLErrorCodesFactory
创建和填充,它(顾名思义)是一个工厂,用于基于名为sql-error-codes.xml
的配置文件的内容创建SQLErrorCodes
。该文件填充了供应商代码,并基于DatabaseProductName
取自DatabaseMetaData
。使用了你正在使用的实际数据库的代码。
SQLErrorCodeSQLExceptionTranslator
按以下顺序应用匹配规则:
由子类实现的任何自定义转换。通常情况下,使用提供的具体
SQLErrorCodeSQLExceptionTranslator
,因此此规则不适用。它仅在你实际提供了一个子类实现的情况下才适用。作为
SQLErrorCodes
类的customSqlExceptionTranslator
属性提供的SQLExceptionTranslator
接口的任何自定义实现。搜索
CustomSQLErrorCodesTranslation
类的实例列表(为SQLErrorCodes
类的customTranslations
属性提供)以查找匹配项。应用了错误码匹配。
使用后备翻译程序。
SQLExceptionSubclassTranslator
是默认的后备翻译程序。如果此翻译不可用,则下一个后备翻译程序是SQLStateSQLExceptionTranslator
。
默认情况下,SQLErrorCodesFactory 用于定义Error 代码和自定义异常翻译。它们是在一个名为 sql-error-codes.xml 的文件中从Classpath 中查找的,并且匹配的 SQLErrorCodes 实例是基于数据库中所使用的数据库元数据的SimpleJdbcCall 中的名称。 |
---|
你可以扩展SQLErrorCodeSQLExceptionTranslator
,如下例所示:
爪哇
public class CustomSQLErrorCodesTranslator extends SQLErrorCodeSQLExceptionTranslator {
protected DataAccessException customTranslate(String task, String sql, SQLException sqlEx) {
if (sqlEx.getErrorCode() == -12345) {
return new DeadlockLoserDataAccessException(task, sqlEx);
}
return null;
}
}
Kotlin
class CustomSQLErrorCodesTranslator : SQLErrorCodeSQLExceptionTranslator() {
override fun customTranslate(task: String, sql: String?, sqlEx: SQLException): DataAccessException? {
if (sqlEx.errorCode == -12345) {
return DeadlockLoserDataAccessException(task, sqlEx)
}
return null
}
}
在前面的示例中,将翻译特定的错误代码(-12345
),而其他错误将由默认的翻译实现来翻译。要使用此自定义转换器,必须通过方法setExceptionTranslator
将其传递给JdbcTemplate
,并且必须将此JdbcTemplate
用于需要此转换器的所有数据访问处理。下面的示例展示了如何使用这个自定义转换器:
爪哇
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
// create a JdbcTemplate and set data source
this.jdbcTemplate = new JdbcTemplate();
this.jdbcTemplate.setDataSource(dataSource);
// create a custom translator and set the DataSource for the default translation lookup
CustomSQLErrorCodesTranslator tr = new CustomSQLErrorCodesTranslator();
tr.setDataSource(dataSource);
this.jdbcTemplate.setExceptionTranslator(tr);
}
public void updateShippingCharge(long orderId, long pct) {
// use the prepared JdbcTemplate for this update
this.jdbcTemplate.update("update orders" +
" set shipping_charge = shipping_charge * ? / 100" +
" where id = ?", pct, orderId);
}
Kotlin
// create a JdbcTemplate and set data source
private val jdbcTemplate = JdbcTemplate(dataSource).apply {
// create a custom translator and set the DataSource for the default translation lookup
exceptionTranslator = CustomSQLErrorCodesTranslator().apply {
this.dataSource = dataSource
}
}
fun updateShippingCharge(orderId: Long, pct: Long) {
// use the prepared JdbcTemplate for this update
this.jdbcTemplate!!.update("update orders" +
" set shipping_charge = shipping_charge * ? / 100" +
" where id = ?", pct, orderId)
}
在sql-error-codes.xml
中,将向自定义转换器传递一个数据源,以便查找错误代码。
# 3.3.4.正在运行的语句
运行 SQL 语句只需要很少的代码。你需要一个DataSource
和一个JdbcTemplate
,包括JdbcTemplate
提供的方便方法。下面的示例展示了创建一个新表的最小但功能齐全的类需要包括哪些内容:
爪哇
import javax.sql.DataSource;
import org.springframework.jdbc.core.JdbcTemplate;
public class ExecuteAStatement {
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
public void doExecute() {
this.jdbcTemplate.execute("create table mytable (id integer, name varchar(100))");
}
}
Kotlin
import javax.sql.DataSource
import org.springframework.jdbc.core.JdbcTemplate
class ExecuteAStatement(dataSource: DataSource) {
private val jdbcTemplate = JdbcTemplate(dataSource)
fun doExecute() {
jdbcTemplate.execute("create table mytable (id integer, name varchar(100))")
}
}
# 3.3.5.正在运行的查询
一些查询方法返回一个值。要从一行中检索计数或特定值,请使用queryForObject(..)
。后者将返回的 JDBCType
转换为作为参数传入的 爪哇 类。如果类型转换无效,则抛出InvalidDataAccessApiUsageException
。下面的示例包含两个查询方法,一个用于查询int
,另一个用于查询String
:
爪哇
import javax.sql.DataSource;
import org.springframework.jdbc.core.JdbcTemplate;
public class RunAQuery {
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
public int getCount() {
return this.jdbcTemplate.queryForObject("select count(*) from mytable", Integer.class);
}
public String getName() {
return this.jdbcTemplate.queryForObject("select name from mytable", String.class);
}
}
Kotlin
import javax.sql.DataSource
import org.springframework.jdbc.core.JdbcTemplate
class RunAQuery(dataSource: DataSource) {
private val jdbcTemplate = JdbcTemplate(dataSource)
val count: Int
get() = jdbcTemplate.queryForObject("select count(*) from mytable")!!
val name: String?
get() = jdbcTemplate.queryForObject("select name from mytable")
}
除了单个结果查询方法外,还有几个方法返回一个列表,其中包含查询返回的每一行的条目。最通用的方法是queryForList(..)
,它返回一个List
,其中每个元素都是Map
,包含每个列的一个条目,并使用列名作为键。如果在前面的示例中添加一个方法来检索所有行的列表,它可能如下:
爪哇
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
public List<Map<String, Object>> getList() {
return this.jdbcTemplate.queryForList("select * from mytable");
}
Kotlin
private val jdbcTemplate = JdbcTemplate(dataSource)
fun getList(): List<Map<String, Any>> {
return jdbcTemplate.queryForList("select * from mytable")
}
返回的列表类似于以下内容:
[{name=Bob, id=1}, {name=Mary, id=2}]
# 3.3.6.更新数据库
下面的示例更新了某个主键的列:
爪哇
import javax.sql.DataSource;
import org.springframework.jdbc.core.JdbcTemplate;
public class ExecuteAnUpdate {
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
public void setName(int id, String name) {
this.jdbcTemplate.update("update mytable set name = ? where id = ?", name, id);
}
}
Kotlin
import javax.sql.DataSource
import org.springframework.jdbc.core.JdbcTemplate
class ExecuteAnUpdate(dataSource: DataSource) {
private val jdbcTemplate = JdbcTemplate(dataSource)
fun setName(id: Int, name: String) {
jdbcTemplate.update("update mytable set name = ? where id = ?", name, id)
}
}
在前面的示例中,SQL 语句具有行参数的占位符。你可以将参数值以 varargs 的形式传递,也可以以对象数组的形式传递。因此,你应该在原语包装器类中显式地包装原语,或者应该使用自动装箱。
# 3.3.7.检索自动生成的密钥
一种update()
方便的方法支持对数据库生成的主键进行检索。这种支持是 JDBC3.0 标准的一部分。有关详细信息,请参见说明书第 13.6 章。该方法以PreparedStatementCreator
作为其第一个参数,这就是指定所需 INSERT 语句的方式。另一个参数是KeyHolder
,它包含在更新成功返回时生成的键。没有标准的单一方法来创建适当的PreparedStatement
(这解释了为什么方法签名是这样的)。以下示例可以在 Oracle 上运行,但可能无法在其他平台上运行:
爪哇
final String INSERT_SQL = "insert into my_test (name) values(?)";
final String name = "Rob";
KeyHolder keyHolder = new GeneratedKeyHolder();
jdbcTemplate.update(connection -> {
PreparedStatement ps = connection.prepareStatement(INSERT_SQL, new String[] { "id" });
ps.setString(1, name);
return ps;
}, keyHolder);
// keyHolder.getKey() now contains the generated key
Kotlin
val INSERT_SQL = "insert into my_test (name) values(?)"
val name = "Rob"
val keyHolder = GeneratedKeyHolder()
jdbcTemplate.update({
it.prepareStatement (INSERT_SQL, arrayOf("id")).apply { setString(1, name) }
}, keyHolder)
// keyHolder.getKey() now contains the generated key
# 3.4.控制数据库连接
本节内容包括:
[使用
DataSource
][使用
DataSourceUtils
](#jdbc-datasourceutils)[实现
SmartDataSource
][扩展
AbstractDataSource
][使用
SingleConnectionDataSource
](#JDBC-SingleConnectionDataSource)[使用
DriverManagerDataSource
][使用
TransactionAwareDataSourceProxy
][使用
DataSourceTransactionManager
]
# 3.4.1.使用InvalidDataAccessApiUsageException
Spring 通过
获得到数据库的连接。aDataSource
是 JDBC 规范的一部分,是一个通用的连接工厂。它允许容器或框架从应用程序代码中隐藏连接池和事务管理问题。作为开发人员,你不需要了解有关如何连接到数据库的详细信息。这是设置数据源的管理员的责任。在开发和测试代码时,你最有可能同时填充这两个角色,但是你并不一定要知道生产数据源是如何配置的。
当你使用 Spring 的 JDBC 层时,你可以从 JNDI 获得数据源,也可以使用第三方提供的连接池实现来配置你自己的数据源。传统的选择是带有 Bean 样式DataSource
类的 ApacheCommonsDBCP 和 C3P0;对于现代的 JDBC 连接池,请考虑使用其 Builder 样式的 API 的 HikarICP。
你应该使用AbstractDataSource 和SimpleDriverDataSource 类(包含在 Spring 发行版中)仅用于测试目的!这些变体不 提供池,并且在发出多个连接请求时性能很差。 |
---|
下面的部分使用 Spring 的DriverManagerDataSource
实现。后面将介绍其他几种DataSource
变体。
要配置DriverManagerDataSource
:
获得与
DriverManagerDataSource
的连接,就像你通常获得 JDBC 连接一样。指定 JDBC 驱动程序的完全限定类名,以便
DriverManager
可以加载驱动程序类。提供不同于 JDBC 驱动程序的 URL。(请参阅驱动程序的文档以获得正确的值。
提供连接到数据库的用户名和密码。
下面的示例展示了如何在 爪哇 中配置DriverManagerDataSource
:
爪哇
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("org.hsqldb.jdbcDriver");
dataSource.setUrl("jdbc:hsqldb:hsql://localhost:");
dataSource.setUsername("sa");
dataSource.setPassword("");
Kotlin
val dataSource = DriverManagerDataSource().apply {
setDriverClassName("org.hsqldb.jdbcDriver")
url = "jdbc:hsqldb:hsql://localhost:"
username = "sa"
password = ""
}
下面的示例展示了相应的 XML 配置:
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="${jdbc.driverClassName}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<context:property-placeholder location="jdbc.properties"/>
接下来的两个示例展示了 DBCP 和 C3P0 的基本连接和配置。要了解有助于控制池特性的更多选项,请参阅相应的连接池实现的产品文档。
下面的示例展示了 DBCP 配置:
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="${jdbc.driverClassName}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<context:property-placeholder location="jdbc.properties"/>
下面的示例展示了 C3P0 配置:
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close">
<property name="driverClass" value="${jdbc.driverClassName}"/>
<property name="jdbcUrl" value="${jdbc.url}"/>
<property name="user" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<context:property-placeholder location="jdbc.properties"/>
# 3.4.2.使用DataSourceUtils
DataSourceUtils
类是一个方便且功能强大的助手类,它提供static
方法来从 JNDI 获得连接,并在必要时提供关闭连接。它支持线程绑定连接,例如,DataSourceTransactionManager
。
# 3.4.3.实现SmartDataSource
SmartDataSource
接口应该由能够提供到关系数据库的连接的类来实现。它扩展了String
接口,让使用它的类查询在给定的操作之后是否应该关闭连接。当你知道需要重用某个连接时,这种用法是有效的。
# 3.4.4.扩展AbstractDataSource
AbstractDataSource
是 Spring 的abstract
实现的基类。它实现了所有DataSource
实现所共有的代码。如果你编写自己的DataSource
实现,则应该扩展AbstractDataSource
类。
# 3.4.5.使用SingleConnectionDataSource
DataSource
类是SmartDataSource
接口的一个实现,该接口封装单个Connection
,该接口在每次使用后都不会关闭。这不是多线程能力。
如果任何客户机代码在假定池连接的情况下调用close
(如使用持久性工具时),则应将suppressClose
属性设置为true
。此设置返回一个封装物理连接的关闭抑制代理。请注意,你不能再将其强制转换为原生 OracleConnection
或类似的对象。
SingleConnectionDataSource
主要是一个测试类。它通常支持在应用程序服务器之外,结合简单的 JNDI 环境,对代码进行简单的测试。与DriverManagerDataSource
相反,它始终重用相同的连接,避免了过多地创建物理连接。
# 3.4.6.使用DriverManagerDataSource
DriverManagerDataSource
类是标准DataSource
接口的实现,该接口通过 Bean 属性配置一个普通的 JDBC 驱动程序,并且每次返回一个新的Connection
。
这种实现对于 爪哇 EE 容器之外的测试和独立环境非常有用,可以作为 Spring IoC 容器中的DataSource
Bean,也可以与简单的 JNDI 环境结合使用。池-假设DataSource
调用关闭连接,那么任何DataSource
可感知的持久性代码都应该工作。然而,使用 爪哇Bean 风格的连接池(例如commons-dbcp
)非常容易,即使在测试环境中也是如此,因此在DriverManagerDataSource
上使用这样的连接池几乎总是更好。
# 3.4.7.使用TransactionAwareDataSourceProxy
TransactionAwareDataSourceProxy
是目标DataSource
的代理。代理封装了目标DataSource
,以添加对 Spring 管理的事务的感知。在这方面,它类似于事务 JNDIDataSource
,由 爪哇 EE 服务器提供。
很少希望使用这个类,除非已经存在的代码必须被 调用并传递一个标准的 JDBC DataSourceUtils 接口实现。在这种情况下,仍然可以使该代码可用,同时,使该代码 参与 Spring 托管事务。通常更好的做法是使用用于资源管理的更高级抽象来编写你的 自己的新代码,例如 JdbcTemplate 或DataSourceUtils 。 |
---|
参见[STRUCT
](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/jdbc/datasource/transactionawardatasourceproxy.html)爪哇doc 以获取更多细节。
# 3.4.8.使用DataSourceTransactionManager
DataSourceTransactionManager
类是单个 JDBC 数据源的PlatformTransactionManager
实现。它将来自指定数据源的 JDBC 连接绑定到当前正在执行的线程,可能允许每个数据源有一个线程连接。
需要应用程序代码来通过DataSourceUtils.getConnection(DataSource)
而不是 爪哇 EE 的标准SimpleJdbcCall
检索 JDBC 连接。它抛出未选中的org.springframework.dao
异常,而不是选中的SQLExceptions
。所有框架类(如JdbcTemplate
)都隐式地使用此策略。如果不与此事务管理器一起使用,则查找策略的行为与常见策略完全相同。因此,它可以在任何情况下使用。
DataSourceTransactionManager
类支持自定义隔离级别和超时,这些级别和超时将作为适当的 JDBC 语句查询超时应用。为了支持后者,应用程序代码必须使用JdbcTemplate
,或者为每个创建的语句调用DataSourceUtils.applyTransactionTimeout(..)
方法。
在单资源情况下,你可以使用这个实现,而不是JtaTransactionManager
,因为它不需要容器支持 JTA。在两者之间切换只是一个配置问题,只要你坚持所需的连接查找模式。JTA 不支持自定义隔离级别。
# 3.5.JDBC 批处理操作
如果对同一条准备好的语句进行多个调用的批处理,大多数 JDBC 驱动程序都会提供更好的性能。通过将更新分组为批,可以限制往返数据库的次数。
# 3.5.1.带有JdbcTemplate
的基本批处理操作
通过实现一个特殊接口的两个方法BatchPreparedStatementSetter
,并将该实现作为batchUpdate
方法调用中的第二个参数传入,可以完成JdbcTemplate
批处理。你可以使用getBatchSize
方法来提供当前批处理的大小。你可以使用setValues
方法来设置 prepared 语句的参数的值。这个方法被称为在getBatchSize
调用中指定的次数。下面的示例基于列表中的条目更新t_actor
表,并将整个列表用作批处理:
爪哇
public class JdbcActorDao implements ActorDao {
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
public int[] batchUpdate(final List<Actor> actors) {
return this.jdbcTemplate.batchUpdate(
"update t_actor set first_name = ?, last_name = ? where id = ?",
new BatchPreparedStatementSetter() {
public void setValues(PreparedStatement ps, int i) throws SQLException {
Actor actor = actors.get(i);
ps.setString(1, actor.getFirstName());
ps.setString(2, actor.getLastName());
ps.setLong(3, actor.getId().longValue());
}
public int getBatchSize() {
return actors.size();
}
});
}
// ... additional methods
}
Kotlin
class JdbcActorDao(dataSource: DataSource) : ActorDao {
private val jdbcTemplate = JdbcTemplate(dataSource)
fun batchUpdate(actors: List<Actor>): IntArray {
return jdbcTemplate.batchUpdate(
"update t_actor set first_name = ?, last_name = ? where id = ?",
object: BatchPreparedStatementSetter {
override fun setValues(ps: PreparedStatement, i: Int) {
ps.setString(1, actors[i].firstName)
ps.setString(2, actors[i].lastName)
ps.setLong(3, actors[i].id)
}
override fun getBatchSize() = actors.size
})
}
// ... additional methods
}
如果你处理一个更新流或从一个文件中读取数据,那么你可能有一个首选批处理大小,但是最后一个批处理可能没有那么多条目。在这种情况下,你可以使用InterruptibleBatchPreparedStatementSetter
接口,它允许你在输入源耗尽时中断批处理。isBatchExhausted
方法让你发出批处理结束的信号。
# 3.5.2.具有对象列表的批处理操作
JdbcTemplate
和NamedParameterJdbcTemplate
都提供了一种提供批更新的替代方式。你不是实现一个特殊的批处理接口,而是以列表的形式提供调用中的所有参数值。框架在这些值上循环,并使用内部准备的语句设置器。API 会有所不同,这取决于你是否使用命名参数。对于已命名的参数,你提供一个SqlParameterSource
的数组,这是批处理的每个成员的一个条目。你可以使用SqlParameterSourceUtils.createBatch
便利方法来创建这个数组,传入一个由 Bean 样式的对象组成的数组(具有与参数对应的 getter 方法),String
-keyedMap
实例(包含相应的参数作为值),或者两者的混合。
下面的示例显示了使用命名参数的批处理更新:
爪哇
public class JdbcActorDao implements ActorDao {
private NamedParameterTemplate namedParameterJdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
}
public int[] batchUpdate(List<Actor> actors) {
return this.namedParameterJdbcTemplate.batchUpdate(
"update t_actor set first_name = :firstName, last_name = :lastName where id = :id",
SqlParameterSourceUtils.createBatch(actors));
}
// ... additional methods
}
Kotlin
class JdbcActorDao(dataSource: DataSource) : ActorDao {
private val namedParameterJdbcTemplate = NamedParameterJdbcTemplate(dataSource)
fun batchUpdate(actors: List<Actor>): IntArray {
return this.namedParameterJdbcTemplate.batchUpdate(
"update t_actor set first_name = :firstName, last_name = :lastName where id = :id",
SqlParameterSourceUtils.createBatch(actors));
}
// ... additional methods
}
对于使用经典?
占位符的 SQL 语句,你将传入一个包含带更新值的对象数组的列表。这个对象数组必须为 SQL 语句中的每个占位符有一个条目,并且它们的顺序必须与 SQL 语句中定义的顺序相同。
下面的示例与前面的示例相同,只是它使用了经典的 JDBC?
占位符:
爪哇
public class JdbcActorDao implements ActorDao {
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
public int[] batchUpdate(final List<Actor> actors) {
List<Object[]> batch = new ArrayList<Object[]>();
for (Actor actor : actors) {
Object[] values = new Object[] {
actor.getFirstName(), actor.getLastName(), actor.getId()};
batch.add(values);
}
return this.jdbcTemplate.batchUpdate(
"update t_actor set first_name = ?, last_name = ? where id = ?",
batch);
}
// ... additional methods
}
Kotlin
class JdbcActorDao(dataSource: DataSource) : ActorDao {
private val jdbcTemplate = JdbcTemplate(dataSource)
fun batchUpdate(actors: List<Actor>): IntArray {
val batch = mutableListOf<Array<Any>>()
for (actor in actors) {
batch.add(arrayOf(actor.firstName, actor.lastName, actor.id))
}
return jdbcTemplate.batchUpdate(
"update t_actor set first_name = ?, last_name = ? where id = ?", batch)
}
// ... additional methods
}
我们前面描述的所有批更新方法都返回一个int
数组,该数组包含每个批处理条目的受影响行数。这个计数是由 JDBC 驱动程序报告的。如果计数不可用,则 JDBC 驱动程序返回一个-2
的值。
在这样的场景中,在底层PreparedStatement 上自动设定值,每个值对应的 JDBC 类型需要从给定的 爪哇 类型派生。 虽然这通常工作得很好,但可能会出现问题(例如,对于包含映射的 null 值)。 Spring,默认情况下,在这样的ParameterMetaData.getParameterType 情况下调用,这在使用你的 JDBC 驱动程序时可能是昂贵的。你应该使用最近的驱动程序 版本,并考虑将 spring.jdbc.getParameterType.ignore 属性设置为true (作为 JVM 系统属性或通过[SpringProperties ](Appendix.html#Appendix- Spring-properties)机制),如果你遇到性能问题(正如 Oracle12c、JBoss 和 PostgreSQL 所报告的那样)。 或者,你可以考虑通过 BatchPreparedStatementSetter (如前面所示),通过显式类型,通过 List<Object[]> 调用的显式类型,显式地指定相应的 JDBC 类型,通过 registerSqlType 调用自定义 MapSqlParameterSource 实例,或者通过BeanPropertySqlParameterSource 从 爪哇 声明的属性类型派生 SQL 类型,甚至对于空值也是如此。 |
---|
# 3.5.3.具有多批处理的批处理操作
前面的批更新示例处理的批是如此之大,以至于你希望将它们分解为几个较小的批。你可以通过多次调用batchUpdate
方法来使用前面提到的方法来实现这一点,但是现在有了一个更方便的方法。除了 SQL 语句之外,该方法还接受一个Collection
的对象,这些对象包含参数,每个批处理要进行的更新的数量,以及一个ParameterizedPreparedStatementSetter
来设置准备语句的参数值。框架在提供的值上循环,并将更新调用分成指定大小的批。
下面的示例显示了一个使用批处理大小为 100 的批处理更新:
爪哇
public class JdbcActorDao implements ActorDao {
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
public int[][] batchUpdate(final Collection<Actor> actors) {
int[][] updateCounts = jdbcTemplate.batchUpdate(
"update t_actor set first_name = ?, last_name = ? where id = ?",
actors,
100,
(PreparedStatement ps, Actor actor) -> {
ps.setString(1, actor.getFirstName());
ps.setString(2, actor.getLastName());
ps.setLong(3, actor.getId().longValue());
});
return updateCounts;
}
// ... additional methods
}
Kotlin
class JdbcActorDao(dataSource: DataSource) : ActorDao {
private val jdbcTemplate = JdbcTemplate(dataSource)
fun batchUpdate(actors: List<Actor>): Array<IntArray> {
return jdbcTemplate.batchUpdate(
"update t_actor set first_name = ?, last_name = ? where id = ?",
actors, 100) { ps, argument ->
ps.setString(1, argument.firstName)
ps.setString(2, argument.lastName)
ps.setLong(3, argument.id)
}
}
// ... additional methods
}
此调用的批更新方法返回一个int
数组的数组,其中包含每个批处理的数组条目,以及每个更新的受影响行数组。顶级数组的长度表示运行的批处理的数量,而二级数组的长度表示该批处理中的更新数量。每个批处理中的更新数量应该是为所有批处理提供的批处理大小(除了最后一个可能更小),这取决于所提供的更新对象的总数。每个更新语句的更新计数是 JDBC 驱动程序报告的。如果计数不可用,则 JDBC 驱动程序返回一个-2
的值。
# 3.6.使用SimpleJdbc
类简化 JDBC 操作
SimpleJdbcInsert
和SimpleJdbcCall
类利用可以通过 JDBC 驱动程序检索的数据库元数据,提供了一种简化的配置。这意味着你需要预先配置的内容较少,但是如果你希望在代码中提供所有细节,则可以重写或关闭元数据处理。
# 3.6.1.使用SimpleJdbcInsert
插入数据
我们首先查看配置选项最少的SimpleJdbcInsert
类。你应该在数据访问层的初始化方法中实例化SimpleJdbcInsert
。对于这个示例,初始化方法是setDataSource
方法。你不需要子类SimpleJdbcInsert
类。相反,你可以使用withTableName
方法创建一个新实例并设置表名。该类的配置方法遵循fluid
样式,该样式返回withTableName
的实例,它允许你链接所有配置方法。下面的示例只使用一种配置方法(稍后我们将展示多个方法的示例):
爪哇
public class JdbcActorDao implements ActorDao {
private SimpleJdbcInsert insertActor;
public void setDataSource(DataSource dataSource) {
this.insertActor = new SimpleJdbcInsert(dataSource).withTableName("t_actor");
}
public void add(Actor actor) {
Map<String, Object> parameters = new HashMap<String, Object>(3);
parameters.put("id", actor.getId());
parameters.put("first_name", actor.getFirstName());
parameters.put("last_name", actor.getLastName());
insertActor.execute(parameters);
}
// ... additional methods
}
Kotlin
class JdbcActorDao(dataSource: DataSource) : ActorDao {
private val insertActor = SimpleJdbcInsert(dataSource).withTableName("t_actor")
fun add(actor: Actor) {
val parameters = mutableMapOf<String, Any>()
parameters["id"] = actor.id
parameters["first_name"] = actor.firstName
parameters["last_name"] = actor.lastName
insertActor.execute(parameters)
}
// ... additional methods
}
这里使用的execute
方法将一个普通的java.util.Map
作为其唯一参数。这里需要注意的重要事项是,用于withTableName
的键必须与数据库中定义的表的列名匹配。这是因为我们读取元数据来构造实际的 INSERT 语句。
# 3.6.2.使用SimpleJdbcInsert
检索自动生成的密钥
下一个示例使用与前一个示例相同的 INSERT,但是,它不是传入id
,而是检索自动生成的键并将其设置在新的withTableName
对象上。当它创建SimpleJdbcInsert
时,除了指定表名外,它还使用usingGeneratedKeyColumns
方法指定生成的键列的名称。下面的清单展示了它的工作原理:
爪哇
public class JdbcActorDao implements ActorDao {
private SimpleJdbcInsert insertActor;
public void setDataSource(DataSource dataSource) {
this.insertActor = new SimpleJdbcInsert(dataSource)
.withTableName("t_actor")
.usingGeneratedKeyColumns("id");
}
public void add(Actor actor) {
Map<String, Object> parameters = new HashMap<String, Object>(2);
parameters.put("first_name", actor.getFirstName());
parameters.put("last_name", actor.getLastName());
Number newId = insertActor.executeAndReturnKey(parameters);
actor.setId(newId.longValue());
}
// ... additional methods
}
Kotlin
class JdbcActorDao(dataSource: DataSource) : ActorDao {
private val insertActor = SimpleJdbcInsert(dataSource)
.withTableName("t_actor").usingGeneratedKeyColumns("id")
fun add(actor: Actor): Actor {
val parameters = mapOf(
"first_name" to actor.firstName,
"last_name" to actor.lastName)
val newId = insertActor.executeAndReturnKey(parameters);
return actor.copy(id = newId.toLong())
}
// ... additional methods
}
在使用第二种方法运行 INSERT 时,主要的区别是你没有将id
添加到Map
中,而是调用executeAndReturnKey
方法。这返回一个java.lang.Number
对象,你可以使用它创建域类中使用的数值类型的实例。在这里,你不能依赖所有的数据库来返回特定的 爪哇 类。java.lang.Number
是你可以依赖的基类。如果有多个自动生成的列,或者生成的值是非数字的,则可以使用从executeAndReturnKeyHolder
方法返回的KeyHolder
。
# 3.6.3.指定executeAndReturnKeyHolder
的列
可以通过使用usingColumns
方法指定列名列表来限制插入的列,如下例所示:
爪哇
public class JdbcActorDao implements ActorDao {
private SimpleJdbcInsert insertActor;
public void setDataSource(DataSource dataSource) {
this.insertActor = new SimpleJdbcInsert(dataSource)
.withTableName("t_actor")
.usingColumns("first_name", "last_name")
.usingGeneratedKeyColumns("id");
}
public void add(Actor actor) {
Map<String, Object> parameters = new HashMap<String, Object>(2);
parameters.put("first_name", actor.getFirstName());
parameters.put("last_name", actor.getLastName());
Number newId = insertActor.executeAndReturnKey(parameters);
actor.setId(newId.longValue());
}
// ... additional methods
}
Kotlin
class JdbcActorDao(dataSource: DataSource) : ActorDao {
private val insertActor = SimpleJdbcInsert(dataSource)
.withTableName("t_actor")
.usingColumns("first_name", "last_name")
.usingGeneratedKeyColumns("id")
fun add(actor: Actor): Actor {
val parameters = mapOf(
"first_name" to actor.firstName,
"last_name" to actor.lastName)
val newId = insertActor.executeAndReturnKey(parameters);
return actor.copy(id = newId.toLong())
}
// ... additional methods
}
INSERT 的执行与依赖元数据来确定要使用哪些列的情况相同。
# 3.6.4.使用SqlParameterSource
提供参数值
使用Map
提供参数值可以很好地工作,但它不是使用最方便的类。 Spring 提供了可以替代使用的SqlParameterSource
接口的两个实现方式。第一个是BeanPropertySqlParameterSource
,如果你有一个兼容 爪哇Bean 的类,其中包含你的值,那么它是一个非常方便的类。它采用相应的 getter 方法提取参数值。下面的示例展示了如何使用BeanPropertySqlParameterSource
:
爪哇
public class JdbcActorDao implements ActorDao {
private SimpleJdbcInsert insertActor;
public void setDataSource(DataSource dataSource) {
this.insertActor = new SimpleJdbcInsert(dataSource)
.withTableName("t_actor")
.usingGeneratedKeyColumns("id");
}
public void add(Actor actor) {
SqlParameterSource parameters = new BeanPropertySqlParameterSource(actor);
Number newId = insertActor.executeAndReturnKey(parameters);
actor.setId(newId.longValue());
}
// ... additional methods
}
Kotlin
class JdbcActorDao(dataSource: DataSource) : ActorDao {
private val insertActor = SimpleJdbcInsert(dataSource)
.withTableName("t_actor")
.usingGeneratedKeyColumns("id")
fun add(actor: Actor): Actor {
val parameters = BeanPropertySqlParameterSource(actor)
val newId = insertActor.executeAndReturnKey(parameters)
return actor.copy(id = newId.toLong())
}
// ... additional methods
}
另一个选项是MapSqlParameterSource
,它类似于Map
,但提供了一个更方便的addValue
方法,可以链接。下面的示例展示了如何使用它:
爪哇
public class JdbcActorDao implements ActorDao {
private SimpleJdbcInsert insertActor;
public void setDataSource(DataSource dataSource) {
this.insertActor = new SimpleJdbcInsert(dataSource)
.withTableName("t_actor")
.usingGeneratedKeyColumns("id");
}
public void add(Actor actor) {
SqlParameterSource parameters = new MapSqlParameterSource()
.addValue("first_name", actor.getFirstName())
.addValue("last_name", actor.getLastName());
Number newId = insertActor.executeAndReturnKey(parameters);
actor.setId(newId.longValue());
}
// ... additional methods
}
Kotlin
class JdbcActorDao(dataSource: DataSource) : ActorDao {
private val insertActor = SimpleJdbcInsert(dataSource)
.withTableName("t_actor")
.usingGeneratedKeyColumns("id")
fun add(actor: Actor): Actor {
val parameters = MapSqlParameterSource()
.addValue("first_name", actor.firstName)
.addValue("last_name", actor.lastName)
val newId = insertActor.executeAndReturnKey(parameters)
return actor.copy(id = newId.toLong())
}
// ... additional methods
}
正如你所看到的,配置是相同的。只有执行代码必须更改才能使用这些可选的输入类。
# 3.6.5.用SimpleJdbcCall
调用存储过程
SimpleJdbcCall
类使用数据库中的元数据查找in
和SqlParameterSource
参数的名称,这样你就不必显式地声明它们。如果你愿意这样做,或者如果你的参数(例如ARRAY
或STRUCT
)没有自动映射到 爪哇 类,则可以声明参数。第一个示例显示了一个简单的过程,该过程仅从 MySQL 数据库返回VARCHAR
和DATE
格式中的标量值。示例过程读取指定的参与者条目,并以out
参数的形式返回first_name
、last_name
和birth_date
列。下面的清单展示了第一个示例:
CREATE PROCEDURE read_actor (
IN in_id INTEGER,
OUT out_first_name VARCHAR(100),
OUT out_last_name VARCHAR(100),
OUT out_birth_date DATE)
BEGIN
SELECT first_name, last_name, birth_date
INTO out_first_name, out_last_name, out_birth_date
FROM t_actor where id = in_id;
END;
in_id
参数包含你正在查找的参与者的id
。STRUCT
参数返回从表中读取的数据。
可以以类似于声明SimpleJdbcInsert
的方式声明SimpleJdbcCall
。你应该在数据访问层的初始化方法中实例化和配置类。与StoredProcedure
类相比,不需要创建子类,也不需要声明可以在数据库元数据中查找的参数。下面的SimpleJdbcCall
配置示例使用了前面的存储过程(除了DataSource
之外,唯一的配置选项是存储过程的名称):
爪哇
public class JdbcActorDao implements ActorDao {
private SimpleJdbcCall procReadActor;
public void setDataSource(DataSource dataSource) {
this.procReadActor = new SimpleJdbcCall(dataSource)
.withProcedureName("read_actor");
}
public Actor readActor(Long id) {
SqlParameterSource in = new MapSqlParameterSource()
.addValue("in_id", id);
Map out = procReadActor.execute(in);
Actor actor = new Actor();
actor.setId(id);
actor.setFirstName((String) out.get("out_first_name"));
actor.setLastName((String) out.get("out_last_name"));
actor.setBirthDate((Date) out.get("out_birth_date"));
return actor;
}
// ... additional methods
}
Kotlin
class JdbcActorDao(dataSource: DataSource) : ActorDao {
private val procReadActor = SimpleJdbcCall(dataSource)
.withProcedureName("read_actor")
fun readActor(id: Long): Actor {
val source = MapSqlParameterSource().addValue("in_id", id)
val output = procReadActor.execute(source)
return Actor(
id,
output["out_first_name"] as String,
output["out_last_name"] as String,
output["out_birth_date"] as Date)
}
// ... additional methods
}
你为执行调用而编写的代码涉及创建一个包含 IN 参数的SqlParameterSource
。你必须将为输入值提供的名称与存储过程中声明的参数名称的名称匹配。这种情况不一定要匹配,因为你使用元数据来确定在存储过程中应该如何引用数据库对象。源文件中为存储过程指定的内容不一定是存储在数据库中的方式。一些数据库将名称转换为所有大写,而另一些数据库则使用小写或使用指定的大小写。
execute
方法接受 IN 参数并返回一个Map
,该参数包含由名称键入的任何out
参数,如在存储过程中指定的那样。在这种情况下,它们是out_first_name
,out_last_name
和out_birth_date
。
execute
方法的最后一部分将创建一个Actor
实例,用于返回检索到的数据。同样,使用在存储过程中声明的out
参数的名称也很重要。而且,存储在结果映射中的out
参数的名称与数据库中的SqlParameterSourceUtils.createBatch
参数名称的情况匹配,这在不同的数据库中可能会有所不同。为了使代码更具可移植性,你应该进行不区分大小写的查找,或者指示 Spring 使用LinkedCaseInsensitiveMap
。要实现后者,你可以创建自己的JdbcTemplate
,并将setResultsMapCaseInsensitive
属性设置为true
。然后,你可以将这个定制的JdbcTemplate
实例传递到你的SimpleJdbcCall
的构造函数中。下面的示例展示了这种配置:
爪哇
public class JdbcActorDao implements ActorDao {
private SimpleJdbcCall procReadActor;
public void setDataSource(DataSource dataSource) {
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
jdbcTemplate.setResultsMapCaseInsensitive(true);
this.procReadActor = new SimpleJdbcCall(jdbcTemplate)
.withProcedureName("read_actor");
}
// ... additional methods
}
Kotlin
class JdbcActorDao(dataSource: DataSource) : ActorDao {
private var procReadActor = SimpleJdbcCall(JdbcTemplate(dataSource).apply {
isResultsMapCaseInsensitive = true
}).withProcedureName("read_actor")
// ... additional methods
}
通过执行此操作,可以避免在用于返回的out
参数的名称的情况下发生冲突。
# 3.6.6.显式声明用于SimpleJdbcCall
的参数
在本章的前面,我们描述了参数是如何从元数据推导出来的,但是如果你愿意,你可以显式地声明它们。你可以通过使用declareParameters
方法创建和配置SimpleJdbcCall
来实现这一点,该方法接受数量可变的SqlParameter
对象作为输入。有关如何定义SqlParameter
的详细信息,请参见下一节。
如果你使用的数据库不是支持 Spring 的 数据库,则需要显式声明。目前, Spring 支持 以下数据库的存储过程调用的元数据查找:Apache Derby、DB2、MySQL、Microsoft SQL Server、Oracle 和 Sybase。 我们还支持 MySQL、Microsoft SQL Server、 和 Oracle 的存储函数的元数据查找。 |
---|
你可以 OPT 显式地声明一个、一些或所有参数。在未显式声明参数的情况下,仍使用参数元数据。要绕过对潜在参数的元数据查找的所有处理并仅使用声明的参数,你可以调用方法withoutProcedureColumnMetaDataAccess
作为声明的一部分。假设你为一个数据库函数声明了两个或多个不同的调用签名。在这种情况下,调用useInParameterNames
来指定给定签名要包含的 IN 参数名列表。
下面的示例展示了一个完全声明的过程调用,并使用了前面示例中的信息:
爪哇
public class JdbcActorDao implements ActorDao {
private SimpleJdbcCall procReadActor;
public void setDataSource(DataSource dataSource) {
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
jdbcTemplate.setResultsMapCaseInsensitive(true);
this.procReadActor = new SimpleJdbcCall(jdbcTemplate)
.withProcedureName("read_actor")
.withoutProcedureColumnMetaDataAccess()
.useInParameterNames("in_id")
.declareParameters(
new SqlParameter("in_id", Types.NUMERIC),
new SqlOutParameter("out_first_name", Types.VARCHAR),
new SqlOutParameter("out_last_name", Types.VARCHAR),
new SqlOutParameter("out_birth_date", Types.DATE)
);
}
// ... additional methods
}
Kotlin
class JdbcActorDao(dataSource: DataSource) : ActorDao {
private val procReadActor = SimpleJdbcCall(JdbcTemplate(dataSource).apply {
isResultsMapCaseInsensitive = true
}).withProcedureName("read_actor")
.withoutProcedureColumnMetaDataAccess()
.useInParameterNames("in_id")
.declareParameters(
SqlParameter("in_id", Types.NUMERIC),
SqlOutParameter("out_first_name", Types.VARCHAR),
SqlOutParameter("out_last_name", Types.VARCHAR),
SqlOutParameter("out_birth_date", Types.DATE)
)
// ... additional methods
}
这两个示例的执行和最终结果是相同的。第二个示例显式地指定了所有细节,而不是依赖于元数据。
# 3.6.7.如何定义SqlParameters
要为SimpleJdbc
类和 RDBMS 操作类(在将 JDBC 操作建模为 爪哇 对象中涵盖)定义参数,你可以使用SqlParameter
或它的一个子类。为此,你通常在构造函数中指定参数名和 SQL 类型。SQL 类型是通过使用java.sql.Types
常量指定的。在本章的前面,我们看到了类似的声明:
爪哇
new SqlParameter("in_id", Types.NUMERIC),
new SqlOutParameter("out_first_name", Types.VARCHAR),
Kotlin
SqlParameter("in_id", Types.NUMERIC),
SqlOutParameter("out_first_name", Types.VARCHAR),
带有SqlParameter
的第一行声明了一个 IN 参数。你可以通过使用SqlQuery
及其子类(在[understandingSqlQuery
](#JDBC-SQLQuery)中涵盖),将参数用于存储过程调用和查询。
第二行(带有SqlOutParameter
)声明一个out
参数,该参数将在存储过程调用中使用。对于InOut
参数也有SqlInOutParameter
(这些参数为过程提供了一个 IN 值,并且还返回了一个值)。
只有声明为SqlParameter 和SqlInOutParameter 的参数用于提供输入值。这与 StoredProcedure 类不同,后者(出于向后兼容性的原因)允许为参数 提供输入值,声明为 SqlOutParameter 。 |
---|
对于 IN 参数,除了名称和 SQL 类型之外,你还可以为数字数据指定一个比例,或者为自定义数据库类型指定一个类型名称。对于out
参数,可以提供SqlParameters
来处理从REF
游标返回的行的映射。另一种选择是指定SqlReturnType
,它提供了定义对返回值的定制处理的机会。
# 3.6.8.使用SimpleJdbcCall
调用存储函数
你可以以调用存储过程的几乎相同的方式调用存储函数,只是提供了一个函数名而不是过程名。将withFunctionName
方法用作配置的一部分,以指示你想要对函数进行调用,并为函数调用生成相应的字符串。一个专门的调用(executeFunction
)用于运行函数,它以指定类型的对象返回函数返回值,这意味着你不必从结果映射中检索返回值。对于只有一个out
参数的存储过程,也可以使用类似的方便方法(名为executeObject
)。下面的示例(对于 MySQL)基于一个名为get_actor_name
的存储函数,该函数返回参与者的全名:
CREATE FUNCTION get_actor_name (in_id INTEGER)
RETURNS VARCHAR(200) READS SQL DATA
BEGIN
DECLARE out_name VARCHAR(200);
SELECT concat(first_name, ' ', last_name)
INTO out_name
FROM t_actor where id = in_id;
RETURN out_name;
END;
要调用这个函数,我们再次在初始化方法中创建SimpleJdbcCall
,如下例所示:
爪哇
public class JdbcActorDao implements ActorDao {
private SimpleJdbcCall funcGetActorName;
public void setDataSource(DataSource dataSource) {
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
jdbcTemplate.setResultsMapCaseInsensitive(true);
this.funcGetActorName = new SimpleJdbcCall(jdbcTemplate)
.withFunctionName("get_actor_name");
}
public String getActorName(Long id) {
SqlParameterSource in = new MapSqlParameterSource()
.addValue("in_id", id);
String name = funcGetActorName.executeFunction(String.class, in);
return name;
}
// ... additional methods
}
Kotlin
class JdbcActorDao(dataSource: DataSource) : ActorDao {
private val jdbcTemplate = JdbcTemplate(dataSource).apply {
isResultsMapCaseInsensitive = true
}
private val funcGetActorName = SimpleJdbcCall(jdbcTemplate)
.withFunctionName("get_actor_name")
fun getActorName(id: Long): String {
val source = MapSqlParameterSource().addValue("in_id", id)
return funcGetActorName.executeFunction(String::class.java, source)
}
// ... additional methods
}
使用的executeFunction
方法返回一个String
,它包含函数调用的返回值。
# 3.6.9.从SimpleJdbcCall
返回RowMapper
或 ref 游标
调用返回结果集的存储过程或函数有点困难。一些数据库在 JDBC 结果处理过程中返回结果集,而另一些数据库则需要显式注册特定类型的out
参数。这两种方法都需要额外的处理来循环结果集并处理返回的行。使用SimpleJdbcCall
,你可以使用returningResultSet
方法并声明用于特定参数的RowMapper
实现。如果在结果处理过程中返回了结果集,则没有定义名称,因此返回的结果必须与声明RowMapper
实现的顺序匹配。指定的名称仍用于将已处理的结果列表存储在从execute
语句返回的结果映射中。
下一个示例(对于 MySQL)使用一个存储过程,该过程接受 no in 参数并返回t_actor
表中的所有行:
CREATE PROCEDURE read_all_actors()
BEGIN
SELECT a.id, a.first_name, a.last_name, a.birth_date FROM t_actor a;
END;
要调用这个过程,你可以声明RowMapper
。因为要映射到的类遵循 爪哇Bean 规则,所以可以使用BeanPropertyRowMapper
,它是通过在newInstance
方法中传递要映射到的类来创建的。下面的示例展示了如何做到这一点:
爪哇
public class JdbcActorDao implements ActorDao {
private SimpleJdbcCall procReadAllActors;
public void setDataSource(DataSource dataSource) {
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
jdbcTemplate.setResultsMapCaseInsensitive(true);
this.procReadAllActors = new SimpleJdbcCall(jdbcTemplate)
.withProcedureName("read_all_actors")
.returningResultSet("actors",
BeanPropertyRowMapper.newInstance(Actor.class));
}
public List getActorsList() {
Map m = procReadAllActors.execute(new HashMap<String, Object>(0));
return (List) m.get("actors");
}
// ... additional methods
}
Kotlin
class JdbcActorDao(dataSource: DataSource) : ActorDao {
private val procReadAllActors = SimpleJdbcCall(JdbcTemplate(dataSource).apply {
isResultsMapCaseInsensitive = true
}).withProcedureName("read_all_actors")
.returningResultSet("actors",
BeanPropertyRowMapper.newInstance(Actor::class.java))
fun getActorsList(): List<Actor> {
val m = procReadAllActors.execute(mapOf<String, Any>())
return m["actors"] as List<Actor>
}
// ... additional methods
}
execute
调用传入一个空的Map
,因为这个调用不接受任何参数。然后从结果映射中检索参与者列表,并将其返回给调用者。
# 3.7.将 JDBC 操作建模为 爪哇 对象
org.springframework.jdbc.object
包包含允许你以更面向对象的方式访问数据库的类。例如,你可以运行查询并以列表的形式获得结果,该列表包含业务对象,并将关系列数据映射到业务对象的属性。你还可以运行存储过程和运行更新、删除和插入语句。
许多 Spring 开发人员认为,下面描述的各种 RDBMS 操作类 (除了[ StoredProcedure ](#JDBC-storedProcedure)类)通常可以将替换为直接的 JdbcTemplate 调用。通常,编写一个直接在JdbcTemplate 上调用方法的 DAO方法更简单(而不是 将查询封装为一个成熟的类)。 但是,如果你正在通过使用 RDBMS 操作类获得可测量的值, 你应该继续使用这些类。 |
---|
# 3.7.1.理解SqlQuery
SqlQuery
是一个可重用的、线程安全的类,它封装了一个 SQL 查询。子类必须实现SqlParameterSource
方法,以提供一个RowMapper
实例,该实例可以通过在执行查询期间创建的ResultSet
上进行迭代,为每行创建一个对象。很少直接使用SqlQuery
类,因为MappingSqlQuery
子类为将行映射到 Java 类提供了一个更方便的实现。扩展SqlQuery
的其他实现是MappingSqlQueryWithParameters
和UpdatableSqlQuery
。
# 3.7.2.使用MappingSqlQuery
MappingSqlQuery
是一个可重用的查询,在这个查询中,具体的子类必须实现抽象的mapRow(..)
方法,以将提供的ResultSet
的每一行转换为指定类型的对象。下面的示例显示了一个自定义查询,该查询将来自t_actor
关系的数据映射到Actor
类的实例:
Java
public class ActorMappingQuery extends MappingSqlQuery<Actor> {
public ActorMappingQuery(DataSource ds) {
super(ds, "select id, first_name, last_name from t_actor where id = ?");
declareParameter(new SqlParameter("id", Types.INTEGER));
compile();
}
@Override
protected Actor mapRow(ResultSet rs, int rowNumber) throws SQLException {
Actor actor = new Actor();
actor.setId(rs.getLong("id"));
actor.setFirstName(rs.getString("first_name"));
actor.setLastName(rs.getString("last_name"));
return actor;
}
}
Kotlin
class ActorMappingQuery(ds: DataSource) : MappingSqlQuery<Actor>(ds, "select id, first_name, last_name from t_actor where id = ?") {
init {
declareParameter(SqlParameter("id", Types.INTEGER))
compile()
}
override fun mapRow(rs: ResultSet, rowNumber: Int) = Actor(
rs.getLong("id"),
rs.getString("first_name"),
rs.getString("last_name")
)
}
该类扩展了MappingSqlQuery
参数化的Actor
类型。此客户查询的构造函数将DataSource
作为唯一的参数。在此构造函数中,你可以使用DataSource
调用超类上的构造函数,并调用应该运行的 SQL 来检索此查询的行。此 SQL 用于创建PreparedStatement
,因此它可能包含用于在执行过程中传递的任何参数的占位符。你必须使用declareParameter
方法声明每个参数,并传入SqlParameter
。SqlParameter
接受一个名称和在java.sql.Types
中定义的 JDBC 类型。在定义了所有参数之后,你可以调用compile()
方法,这样就可以准备语句并在以后运行它。这个类在编译后是线程安全的,因此,只要在初始化 DAO 时创建了这些实例,它们就可以保留为实例变量并被重用。下面的示例展示了如何定义这样的类:
Java
private ActorMappingQuery actorMappingQuery;
@Autowired
public void setDataSource(DataSource dataSource) {
this.actorMappingQuery = new ActorMappingQuery(dataSource);
}
public Customer getCustomer(Long id) {
return actorMappingQuery.findObject(id);
}
Kotlin
private val actorMappingQuery = ActorMappingQuery(dataSource)
fun getCustomer(id: Long) = actorMappingQuery.findObject(id)
前面示例中的方法使用id
检索作为唯一参数传入的客户。由于我们只希望返回一个对象,所以我们调用findObject
便利方法,并以id
作为参数。如果我们有一个返回对象列表并获取附加参数的查询,那么我们将使用execute
方法中的一个,该方法接受以 vargs 形式传递的参数值数组。下面的示例展示了这样的方法:
Java
public List<Actor> searchForActors(int age, String namePattern) {
List<Actor> actors = actorSearchMappingQuery.execute(age, namePattern);
return actors;
}
Kotlin
fun searchForActors(age: Int, namePattern: String) =
actorSearchMappingQuery.execute(age, namePattern)
# 3.7.3.使用SqlUpdate
SqlUpdate
类封装了一个 SQL 更新。与查询一样,更新对象是可重用的,并且,与所有RdbmsOperation
类一样,更新可以有参数,并在 SQL 中定义。这个类提供了许多update(..)
方法,类似于查询对象的execute(..)
方法。SqlUpdate
类是具体的。它可以被子类——例如,添加一个自定义更新方法。但是,你不必子类SqlUpdate
类,因为可以通过设置 SQL 和声明参数来轻松地对它进行参数化。下面的示例创建了一个名为execute
的自定义更新方法:
Java
import java.sql.Types;
import javax.sql.DataSource;
import org.springframework.jdbc.core.SqlParameter;
import org.springframework.jdbc.object.SqlUpdate;
public class UpdateCreditRating extends SqlUpdate {
public UpdateCreditRating(DataSource ds) {
setDataSource(ds);
setSql("update customer set credit_rating = ? where id = ?");
declareParameter(new SqlParameter("creditRating", Types.NUMERIC));
declareParameter(new SqlParameter("id", Types.NUMERIC));
compile();
}
/**
* @param id for the Customer to be updated
* @param rating the new value for credit rating
* @return number of rows updated
*/
public int execute(int id, int rating) {
return update(rating, id);
}
}
Kotlin
import java.sql.Types
import javax.sql.DataSource
import org.springframework.jdbc.core.SqlParameter
import org.springframework.jdbc.object.SqlUpdate
class UpdateCreditRating(ds: DataSource) : SqlUpdate() {
init {
setDataSource(ds)
sql = "update customer set credit_rating = ? where id = ?"
declareParameter(SqlParameter("creditRating", Types.NUMERIC))
declareParameter(SqlParameter("id", Types.NUMERIC))
compile()
}
/**
* @param id for the Customer to be updated
* @param rating the new value for credit rating
* @return number of rows updated
*/
fun execute(id: Int, rating: Int): Int {
return update(rating, id)
}
}
# 3.7.4.使用StoredProcedure
StoredProcedure
类是用于 RDBMS 存储过程的对象抽象的abstract
超类。
继承的sql
属性是 RDBMS 中存储过程的名称。
要为StoredProcedure
类定义参数,可以使用SqlParameter
或它的一个子类。你必须在构造函数中指定参数名和 SQL 类型,如下列代码片段所示:
Java
new SqlParameter("in_id", Types.NUMERIC),
new SqlOutParameter("out_first_name", Types.VARCHAR),
Kotlin
SqlParameter("in_id", Types.NUMERIC),
SqlOutParameter("out_first_name", Types.VARCHAR),
SQL 类型是使用java.sql.Types
常量指定的。
第一行(带有SqlParameter
)声明一个 IN 参数。你可以在参数中使用SqlQuery
及其子类的存储过程调用和查询(在[understandingSqlQuery
](#JDBC-SQLQuery)中涉及)。
第二行(带有SqlOutParameter
)声明一个out
参数,该参数将在存储过程调用中使用。对于InOut
参数也有SqlInOutParameter
(这些参数为过程提供了一个in
值,并且还返回了一个值)。
对于in
参数,除了名称和 SQL 类型之外,还可以为数字数据指定一个比例,或者为自定义数据库类型指定一个类型名称。对于out
参数,可以提供RowMapper
来处理从REF
游标返回的行的映射。另一个选项是指定SqlReturnType
,它允许你定义对返回值的定制处理。
简单 DAO 的下一个示例使用StoredProcedure
调用函数(sysdate()
),该函数与任何 Oracle 数据库一起提供。要使用存储过程功能,你必须创建一个扩展StoredProcedure
的类。在本例中,StoredProcedure
类是一个内部类。但是,如果需要重用StoredProcedure
,则可以将其声明为顶级类。这个示例没有输入参数,但是通过使用SqlOutParameter
类将输出参数声明为日期类型。execute()
方法运行过程,并从结果Map
中提取返回的日期。通过使用参数名称作为键,resultsMap
为每个声明的输出参数(在本例中只有一个)有一个条目。下面的清单显示了我们的自定义存储过程类:
Java
import java.sql.Types;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.SqlOutParameter;
import org.springframework.jdbc.object.StoredProcedure;
public class StoredProcedureDao {
private GetSysdateProcedure getSysdate;
@Autowired
public void init(DataSource dataSource) {
this.getSysdate = new GetSysdateProcedure(dataSource);
}
public Date getSysdate() {
return getSysdate.execute();
}
private class GetSysdateProcedure extends StoredProcedure {
private static final String SQL = "sysdate";
public GetSysdateProcedure(DataSource dataSource) {
setDataSource(dataSource);
setFunction(true);
setSql(SQL);
declareParameter(new SqlOutParameter("date", Types.DATE));
compile();
}
public Date execute() {
// the 'sysdate' sproc has no input parameters, so an empty Map is supplied...
Map<String, Object> results = execute(new HashMap<String, Object>());
Date sysdate = (Date) results.get("date");
return sysdate;
}
}
}
Kotlin
import java.sql.Types
import java.util.Date
import java.util.Map
import javax.sql.DataSource
import org.springframework.jdbc.core.SqlOutParameter
import org.springframework.jdbc.object.StoredProcedure
class StoredProcedureDao(dataSource: DataSource) {
private val SQL = "sysdate"
private val getSysdate = GetSysdateProcedure(dataSource)
val sysdate: Date
get() = getSysdate.execute()
private inner class GetSysdateProcedure(dataSource: DataSource) : StoredProcedure() {
init {
setDataSource(dataSource)
isFunction = true
sql = SQL
declareParameter(SqlOutParameter("date", Types.DATE))
compile()
}
fun execute(): Date {
// the 'sysdate' sproc has no input parameters, so an empty Map is supplied...
val results = execute(mutableMapOf<String, Any>())
return results["date"] as Date
}
}
}
下面的StoredProcedure
示例具有两个输出参数(在本例中是 Oracle Ref 游标):
Java
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import oracle.jdbc.OracleTypes;
import org.springframework.jdbc.core.SqlOutParameter;
import org.springframework.jdbc.object.StoredProcedure;
public class TitlesAndGenresStoredProcedure extends StoredProcedure {
private static final String SPROC_NAME = "AllTitlesAndGenres";
public TitlesAndGenresStoredProcedure(DataSource dataSource) {
super(dataSource, SPROC_NAME);
declareParameter(new SqlOutParameter("titles", OracleTypes.CURSOR, new TitleMapper()));
declareParameter(new SqlOutParameter("genres", OracleTypes.CURSOR, new GenreMapper()));
compile();
}
public Map<String, Object> execute() {
// again, this sproc has no input parameters, so an empty Map is supplied
return super.execute(new HashMap<String, Object>());
}
}
Kotlin
import java.util.HashMap
import javax.sql.DataSource
import oracle.jdbc.OracleTypes
import org.springframework.jdbc.core.SqlOutParameter
import org.springframework.jdbc.object.StoredProcedure
class TitlesAndGenresStoredProcedure(dataSource: DataSource) : StoredProcedure(dataSource, SPROC_NAME) {
companion object {
private const val SPROC_NAME = "AllTitlesAndGenres"
}
init {
declareParameter(SqlOutParameter("titles", OracleTypes.CURSOR, TitleMapper()))
declareParameter(SqlOutParameter("genres", OracleTypes.CURSOR, GenreMapper()))
compile()
}
fun execute(): Map<String, Any> {
// again, this sproc has no input parameters, so an empty Map is supplied
return super.execute(HashMap<String, Any>())
}
}
请注意,在TitlesAndGenresStoredProcedure
构造函数中使用的declareParameter(..)
方法的重载变量是如何传递RowMapper
实现实例的。这是一种非常方便且功能强大的重用现有功能的方法。接下来的两个示例提供了两个RowMapper
实现的代码。
对于所提供的ResultSet
中的每一行,TitleMapper
类将ResultSet
映射到Title
域对象,如下所示:
Java
import java.sql.ResultSet;
import java.sql.SQLException;
import com.foo.domain.Title;
import org.springframework.jdbc.core.RowMapper;
public final class TitleMapper implements RowMapper<Title> {
public Title mapRow(ResultSet rs, int rowNum) throws SQLException {
Title title = new Title();
title.setId(rs.getLong("id"));
title.setName(rs.getString("name"));
return title;
}
}
Kotlin
import java.sql.ResultSet
import com.foo.domain.Title
import org.springframework.jdbc.core.RowMapper
class TitleMapper : RowMapper<Title> {
override fun mapRow(rs: ResultSet, rowNum: Int) =
Title(rs.getLong("id"), rs.getString("name"))
}
对于所提供的ResultSet
中的每一行,GenreMapper
类将ResultSet
映射到Genre
域对象,如下所示:
Java
import java.sql.ResultSet;
import java.sql.SQLException;
import com.foo.domain.Genre;
import org.springframework.jdbc.core.RowMapper;
public final class GenreMapper implements RowMapper<Genre> {
public Genre mapRow(ResultSet rs, int rowNum) throws SQLException {
return new Genre(rs.getString("name"));
}
}
Kotlin
import java.sql.ResultSet
import com.foo.domain.Genre
import org.springframework.jdbc.core.RowMapper
class GenreMapper : RowMapper<Genre> {
override fun mapRow(rs: ResultSet, rowNum: Int): Genre {
return Genre(rs.getString("name"))
}
}
要将参数传递给在 RDBMS 中的定义中具有一个或多个输入参数的存储过程,可以对强类型execute(..)
方法进行编码,该方法将委托给超类中的非类型execute(Map)
方法,如下例所示:
Java
import java.sql.Types;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import oracle.jdbc.OracleTypes;
import org.springframework.jdbc.core.SqlOutParameter;
import org.springframework.jdbc.core.SqlParameter;
import org.springframework.jdbc.object.StoredProcedure;
public class TitlesAfterDateStoredProcedure extends StoredProcedure {
private static final String SPROC_NAME = "TitlesAfterDate";
private static final String CUTOFF_DATE_PARAM = "cutoffDate";
public TitlesAfterDateStoredProcedure(DataSource dataSource) {
super(dataSource, SPROC_NAME);
declareParameter(new SqlParameter(CUTOFF_DATE_PARAM, Types.DATE);
declareParameter(new SqlOutParameter("titles", OracleTypes.CURSOR, new TitleMapper()));
compile();
}
public Map<String, Object> execute(Date cutoffDate) {
Map<String, Object> inputs = new HashMap<String, Object>();
inputs.put(CUTOFF_DATE_PARAM, cutoffDate);
return super.execute(inputs);
}
}
Kotlin
import java.sql.Types
import java.util.Date
import javax.sql.DataSource
import oracle.jdbc.OracleTypes
import org.springframework.jdbc.core.SqlOutParameter
import org.springframework.jdbc.core.SqlParameter
import org.springframework.jdbc.object.StoredProcedure
class TitlesAfterDateStoredProcedure(dataSource: DataSource) : StoredProcedure(dataSource, SPROC_NAME) {
companion object {
private const val SPROC_NAME = "TitlesAfterDate"
private const val CUTOFF_DATE_PARAM = "cutoffDate"
}
init {
declareParameter(SqlParameter(CUTOFF_DATE_PARAM, Types.DATE))
declareParameter(SqlOutParameter("titles", OracleTypes.CURSOR, TitleMapper()))
compile()
}
fun execute(cutoffDate: Date) = super.execute(
mapOf<String, Any>(CUTOFF_DATE_PARAM to cutoffDate))
}
# 3.8.参数和数据值处理的常见问题
Spring Framework 的 JDBC 支持所提供的不同方法中存在参数和数据值的常见问题。本节介绍如何解决这些问题。
# 3.8.1.为参数提供 SQL 类型信息
通常, Spring 根据传入的参数的类型来确定参数的 SQL 类型。在设置参数值时,可以显式地提供要使用的 SQL 类型。这有时是正确设置NULL
值所必需的。
你可以通过以下几种方式提供 SQL 类型信息:
JdbcTemplate
的许多更新和查询方法以int
数组的形式接受一个额外的参数。此数组用于通过使用来自java.sql.Types
类的常量值来指示相应参数的 SQL 类型。为每个参数提供一个条目。你可以使用
SqlParameterValue
类来包装需要此附加信息的参数值。为此,为每个值创建一个新实例,并在构造函数中传入 SQL 类型和参数值。你还可以为数值提供一个可选的缩放参数。对于使用命名参数的方法,可以使用
SqlParameterSource
类,BeanPropertySqlParameterSource
或MapSqlParameterSource
。它们都有为任何命名参数值注册 SQL 类型的方法。
# 3.8.2.处理 BLOB 和 CLOB 对象
你可以在数据库中存储图像、其他二进制数据和大量文本。这些大对象被称为 BLOBS(二进制大对象),用于二进制数据,而 CLOBS(字符大对象),用于字符数据。在 Spring 中,可以通过直接使用JdbcTemplate
来处理这些大型对象,也可以在使用 RDBMS 对象和SimpleJdbc
类提供的更高抽象时处理这些对象。所有这些方法都使用LobHandler
接口的实现,用于实际管理 LOB(大对象)数据。LobHandler
提供了对LobCreator
类的访问,通过getLobCreator
方法,即用于创建要插入的新的 LOB 对象。
LobCreator
和LobHandler
为 LOB 输入和输出提供以下支持:
BLOB
byte[]
:getBlobAsBytes
和setBlobAsBytes
InputStream
:getBlobAsBinaryStream
和setBlobAsBinaryStream
CLOB
String
:getClobAsString
和setClobAsString
InputStream
:getClobAsAsciiStream
和setClobAsAsciiStream
Reader
:getClobAsCharacterStream
和setClobAsCharacterStream
下一个示例展示了如何创建和插入 BLOB。稍后,我们将展示如何从数据库中读回它。
这个示例使用JdbcTemplate
和AbstractLobCreatingPreparedStatementCallback
的实现。它实现了一种方法setValues
。这个方法提供了LobCreator
,我们使用它来设置 SQL INSERT 语句中 LOB 列的值。
对于这个示例,我们假设有一个变量lobHandler
,它已经被设置为DefaultLobHandler
的实例。你通常通过依赖项注入来设置该值。
下面的示例展示了如何创建和插入 BLOB:
Java
final File blobIn = new File("spring2004.jpg");
final InputStream blobIs = new FileInputStream(blobIn);
final File clobIn = new File("large.txt");
final InputStream clobIs = new FileInputStream(clobIn);
final InputStreamReader clobReader = new InputStreamReader(clobIs);
jdbcTemplate.execute(
"INSERT INTO lob_table (id, a_clob, a_blob) VALUES (?, ?, ?)",
new AbstractLobCreatingPreparedStatementCallback(lobHandler) { (1)
protected void setValues(PreparedStatement ps, LobCreator lobCreator) throws SQLException {
ps.setLong(1, 1L);
lobCreator.setClobAsCharacterStream(ps, 2, clobReader, (int)clobIn.length()); (2)
lobCreator.setBlobAsBinaryStream(ps, 3, blobIs, (int)blobIn.length()); (3)
}
}
);
blobIs.close();
clobReader.close();
1 | 在lobHandler 中传递(在本例中)是一个普通的DefaultLobHandler 。 |
---|---|
2 | 使用setClobAsCharacterStream 方法传入 CLOB 的内容。 |
3 | 使用setBlobAsBinaryStream 方法传入 BLOB 的内容。 |
Kotlin
val blobIn = File("spring2004.jpg")
val blobIs = FileInputStream(blobIn)
val clobIn = File("large.txt")
val clobIs = FileInputStream(clobIn)
val clobReader = InputStreamReader(clobIs)
jdbcTemplate.execute(
"INSERT INTO lob_table (id, a_clob, a_blob) VALUES (?, ?, ?)",
object: AbstractLobCreatingPreparedStatementCallback(lobHandler) { (1)
override fun setValues(ps: PreparedStatement, lobCreator: LobCreator) {
ps.setLong(1, 1L)
lobCreator.setClobAsCharacterStream(ps, 2, clobReader, clobIn.length().toInt()) (2)
lobCreator.setBlobAsBinaryStream(ps, 3, blobIs, blobIn.length().toInt()) (3)
}
}
)
blobIs.close()
clobReader.close()
1 | 在lobHandler 中传递(在本例中)是一个普通的DefaultLobHandler 。 |
---|---|
2 | 使用setClobAsCharacterStream 的方法来传入 CLOB 的内容。 |
3 | 使用setBlobAsBinaryStream 方法传入 BLOB 的内容。 |
如果在LobCreator 返回的DefaultLobHandler.getLobCreator() 方法上调用setBlobAsBinaryStream 、setClobAsAsciiStream 或setClobAsCharacterStream 方法,则可以选择为contentLength 参数指定负值。如果指定的内容长度为负,则DefaultLobHandler 使用 set-stream 方法的 JDBC4.0 变体,而不使用长度参数。否则,它会将指定的长度传递给驱动程序。 查看用于验证它是否支持流 LOB 而不提供内容长度的 JDBC 驱动程序的文档。 |
---|
现在是从数据库中读取 LOB 数据的时候了。同样,使用带有相同实例变量JdbcTemplate
的lobHandler
和对DefaultLobHandler
的引用。下面的示例展示了如何做到这一点:
Java
List<Map<String, Object>> l = jdbcTemplate.query("select id, a_clob, a_blob from lob_table",
new RowMapper<Map<String, Object>>() {
public Map<String, Object> mapRow(ResultSet rs, int i) throws SQLException {
Map<String, Object> results = new HashMap<String, Object>();
String clobText = lobHandler.getClobAsString(rs, "a_clob"); (1)
results.put("CLOB", clobText);
byte[] blobBytes = lobHandler.getBlobAsBytes(rs, "a_blob"); (2)
results.put("BLOB", blobBytes);
return results;
}
});
1 | 使用方法getClobAsString 检索 CLOB 的内容。 |
---|---|
2 | 使用方法getBlobAsBytes 检索 BLOB 的内容。 |
Kotlin
val l = jdbcTemplate.query("select id, a_clob, a_blob from lob_table") { rs, _ ->
val clobText = lobHandler.getClobAsString(rs, "a_clob") (1)
val blobBytes = lobHandler.getBlobAsBytes(rs, "a_blob") (2)
mapOf("CLOB" to clobText, "BLOB" to blobBytes)
}
1 | 使用方法getClobAsString 检索 CLOB 的内容。 |
---|---|
2 | 使用方法getBlobAsBytes 检索 BLOB 的内容。 |
# 3.8.3.传入 IN 子句的值列表
SQL 标准允许基于包含变量值列表的表达式选择行。一个典型的例子是select * from T_ACTOR where id in (1, 2, 3)
。JDBC 标准对准备好的语句不直接支持此变量列表。不能声明数量可变的占位符。你需要使用所需数量的占位符来进行一些更改,或者,一旦知道需要多少个占位符,就需要动态地生成 SQL 字符串。在NamedParameterJdbcTemplate
和JdbcTemplate
中提供的命名参数支持采用后一种方法。可以将这些值作为基元对象的java.util.List
传入。这个列表用于插入所需的占位符,并在语句执行期间传入这些值。
在传递许多值时要小心。JDBC 标准并不保证 表达式列表可以使用超过 100 个值。各种数据库都超过了 的值,但是它们通常对允许的值有一个严格的限制。例如,甲骨文的 极限是 1000。 |
---|
除了 value 列表中的基本值之外,还可以创建java.util.List
的对象数组。这个列表可以支持为in
子句定义的多个表达式,例如select * from T_ACTOR where (id, last_name) in ((1, 'Johnson'), (2, 'Harrop'))
。当然,这需要你的数据库支持这种语法。
# 3.8.4.处理存储过程调用的复杂类型
当调用存储过程时,有时可以使用特定于数据库的复杂类型。为了适应这些类型, Spring 提供了一个SqlReturnType
,用于在从存储过程调用返回它们时处理它们,以及在将它们作为参数传入存储过程时处理SqlTypeValue
。
SqlReturnType
接口有一个必须实现的方法(名为getTypeValue
)。该接口用作SqlOutParameter
声明的一部分。下面的示例显示返回用户声明类型ITEM_TYPE
的 OracleSTRUCT
对象的值:
Java
public class TestItemStoredProcedure extends StoredProcedure {
public TestItemStoredProcedure(DataSource dataSource) {
// ...
declareParameter(new SqlOutParameter("item", OracleTypes.STRUCT, "ITEM_TYPE",
(CallableStatement cs, int colIndx, int sqlType, String typeName) -> {
STRUCT struct = (STRUCT) cs.getObject(colIndx);
Object[] attr = struct.getAttributes();
TestItem item = new TestItem();
item.setId(((Number) attr[0]).longValue());
item.setDescription((String) attr[1]);
item.setExpirationDate((java.util.Date) attr[2]);
return item;
}));
// ...
}
Kotlin
class TestItemStoredProcedure(dataSource: DataSource) : StoredProcedure() {
init {
// ...
declareParameter(SqlOutParameter("item", OracleTypes.STRUCT, "ITEM_TYPE") { cs, colIndx, sqlType, typeName ->
val struct = cs.getObject(colIndx) as STRUCT
val attr = struct.getAttributes()
TestItem((attr[0] as Long, attr[1] as String, attr[2] as Date)
})
// ...
}
}
可以使用SqlTypeValue
将 Java 对象的值(例如TestItem
)传递给存储过程。SqlTypeValue
接口有一个必须实现的方法(名为createTypeValue
)。活动连接被传入,你可以使用它创建特定于数据库的对象,例如StructDescriptor
实例或ArrayDescriptor
实例。下面的示例创建了StructDescriptor
实例:
Java
final TestItem testItem = new TestItem(123L, "A test item",
new SimpleDateFormat("yyyy-M-d").parse("2010-12-31"));
SqlTypeValue value = new AbstractSqlTypeValue() {
protected Object createTypeValue(Connection conn, int sqlType, String typeName) throws SQLException {
StructDescriptor itemDescriptor = new StructDescriptor(typeName, conn);
Struct item = new STRUCT(itemDescriptor, conn,
new Object[] {
testItem.getId(),
testItem.getDescription(),
new java.sql.Date(testItem.getExpirationDate().getTime())
});
return item;
}
};
Kotlin
val (id, description, expirationDate) = TestItem(123L, "A test item",
SimpleDateFormat("yyyy-M-d").parse("2010-12-31"))
val value = object : AbstractSqlTypeValue() {
override fun createTypeValue(conn: Connection, sqlType: Int, typeName: String?): Any {
val itemDescriptor = StructDescriptor(typeName, conn)
return STRUCT(itemDescriptor, conn,
arrayOf(id, description, java.sql.Date(expirationDate.time)))
}
}
现在可以将此SqlTypeValue
添加到Map
中,该参数包含存储过程的execute
调用的输入参数。
SqlTypeValue
的另一种用法是将一组值传递给 Oracle 存储过程。Oracle 有自己的内部ARRAY
类,在这种情况下必须使用它,你可以使用SqlTypeValue
来创建 OracleARRAY
的实例,并用来自 JavaARRAY
的值填充它,如下例所示:
Java
final Long[] ids = new Long[] {1L, 2L};
SqlTypeValue value = new AbstractSqlTypeValue() {
protected Object createTypeValue(Connection conn, int sqlType, String typeName) throws SQLException {
ArrayDescriptor arrayDescriptor = new ArrayDescriptor(typeName, conn);
ARRAY idArray = new ARRAY(arrayDescriptor, conn, ids);
return idArray;
}
};
Kotlin
class TestItemStoredProcedure(dataSource: DataSource) : StoredProcedure() {
init {
val ids = arrayOf(1L, 2L)
val value = object : AbstractSqlTypeValue() {
override fun createTypeValue(conn: Connection, sqlType: Int, typeName: String?): Any {
val arrayDescriptor = ArrayDescriptor(typeName, conn)
return ARRAY(arrayDescriptor, conn, ids)
}
}
}
}
# 3.9.嵌入式数据库支持
org.springframework.jdbc.datasource.embedded
包提供了对嵌入式 Java 数据库引擎的支持。本地提供了对HSQL (opens new window)、H2 (opens new window)和Derby (opens new window)的支持。你还可以使用可扩展的 API 来插入新的嵌入式数据库类型和DataSource
实现。
# 3.9.1.为什么要使用嵌入式数据库?
由于嵌入式数据库的轻量级特性,它在项目的开发阶段非常有用。好处包括易于配置、快速启动时间、可测试性以及在开发过程中快速改进 SQL 的能力。
# 3.9.2.使用 Spring XML 创建嵌入式数据库
如果希望在 Spring ApplicationContext
中以 Bean 的形式公开嵌入式数据库实例,则可以在embedded-database
名称空间中使用embedded-database
标记:
<jdbc:embedded-database id="dataSource" generate-name="true">
<jdbc:script location="classpath:schema.sql"/>
<jdbc:script location="classpath:test-data.sql"/>
</jdbc:embedded-database>
前面的配置创建了一个嵌入式 HSQL 数据库,该数据库由来自 Classpath 根中schema.sql
和test-data.sql
资源的 SQL 填充。此外,作为一种最佳实践,嵌入式数据库被分配一个唯一生成的名称。将嵌入式数据库作为类型javax.sql.DataSource
的 Bean 容器提供给 Spring 容器,然后可以根据需要将其注入到数据访问对象中。
# 3.9.3.以编程方式创建嵌入式数据库
EmbeddedDatabaseBuilder
类为以编程方式构建嵌入式数据库提供了一个 Fluent API。当你需要在独立环境或独立集成测试中创建嵌入式数据库时,可以使用此方法,如下例所示:
Java
EmbeddedDatabase db = new EmbeddedDatabaseBuilder()
.generateUniqueName(true)
.setType(H2)
.setScriptEncoding("UTF-8")
.ignoreFailedDrops(true)
.addScript("schema.sql")
.addScripts("user_data.sql", "country_data.sql")
.build();
// perform actions against the db (EmbeddedDatabase extends javax.sql.DataSource)
db.shutdown()
Kotlin
val db = EmbeddedDatabaseBuilder()
.generateUniqueName(true)
.setType(H2)
.setScriptEncoding("UTF-8")
.ignoreFailedDrops(true)
.addScript("schema.sql")
.addScripts("user_data.sql", "country_data.sql")
.build()
// perform actions against the db (EmbeddedDatabase extends javax.sql.DataSource)
db.shutdown()
有关所有支持的选项的更多详细信息,请参见[Javadoc forEmbeddedDatabaseBuilder
](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/jdbc/datasource/embedded/embeddatabasebuilder.html)。
你还可以使用EmbeddedDatabaseBuilder
通过使用 Java 配置来创建嵌入式数据库,如下例所示:
Java
@Configuration
public class DataSourceConfig {
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.generateUniqueName(true)
.setType(H2)
.setScriptEncoding("UTF-8")
.ignoreFailedDrops(true)
.addScript("schema.sql")
.addScripts("user_data.sql", "country_data.sql")
.build();
}
}
Kotlin
@Configuration
class DataSourceConfig {
@Bean
fun dataSource(): DataSource {
return EmbeddedDatabaseBuilder()
.generateUniqueName(true)
.setType(H2)
.setScriptEncoding("UTF-8")
.ignoreFailedDrops(true)
.addScript("schema.sql")
.addScripts("user_data.sql", "country_data.sql")
.build()
}
}
# 3.9.4.选择嵌入式数据库类型
本节介绍如何在 Spring 支持的三个嵌入式数据库中选择一个。它包括以下主题:
# Using HSQL
Spring 支持 HSQL1.8.0 及以上版本。如果没有显式指定类型,则 HSQL 是默认的嵌入式数据库。要显式指定 HSQL,请将embedded-database
标记的type
属性设置为HSQL
。如果使用 Builder API,则使用EmbeddedDatabaseType.HSQL
调用setType(EmbeddedDatabaseType)
方法。
# Using H2
Spring 支持 H2 数据库。要启用 H2,请将embedded-database
标记的type
属性设置为H2
。如果使用 Builder API,则使用EmbeddedDatabaseType.H2
调用setType(EmbeddedDatabaseType)
方法。
# 使用 Derby
Spring 支持 Apache Derby10.5 及以上版本。要启用 Derby,请将embedded-database
标记的type
属性设置为DERBY
。如果使用 Builder API,则使用EmbeddedDatabaseType.DERBY
调用setType(EmbeddedDatabaseType)
方法。
# 3.9.5.用嵌入式数据库测试数据访问逻辑
嵌入式数据库提供了一种轻量级的方式来测试数据访问代码。下一个示例是一个使用嵌入式数据库的数据访问集成测试模板。当嵌入式数据库不需要跨测试类重用时,使用这样的模板对于一次性操作很有用。但是,如果你希望创建一个在测试套件中共享的嵌入式数据库,可以考虑使用Spring TestContext Framework并在<91]中将嵌入式数据库配置为 Bean,如Creating an Embedded Database by Using Spring XML和以编程方式创建嵌入式数据库中所述。下面的清单显示了测试模板:
Java
public class DataAccessIntegrationTestTemplate {
private EmbeddedDatabase db;
@BeforeEach
public void setUp() {
// creates an HSQL in-memory database populated from default scripts
// classpath:schema.sql and classpath:data.sql
db = new EmbeddedDatabaseBuilder()
.generateUniqueName(true)
.addDefaultScripts()
.build();
}
@Test
public void testDataAccess() {
JdbcTemplate template = new JdbcTemplate(db);
template.query( /* ... */ );
}
@AfterEach
public void tearDown() {
db.shutdown();
}
}
Kotlin
class DataAccessIntegrationTestTemplate {
private lateinit var db: EmbeddedDatabase
@BeforeEach
fun setUp() {
// creates an HSQL in-memory database populated from default scripts
// classpath:schema.sql and classpath:data.sql
db = EmbeddedDatabaseBuilder()
.generateUniqueName(true)
.addDefaultScripts()
.build()
}
@Test
fun testDataAccess() {
val template = JdbcTemplate(db)
template.query( /* ... */)
}
@AfterEach
fun tearDown() {
db.shutdown()
}
}
# 3.9.6.为嵌入式数据库生成唯一的名称
如果开发团队的测试套件无意中试图重新创建同一数据库的其他实例,那么在使用嵌入式数据库时,开发团队经常会遇到错误。如果 XML 配置文件或@Configuration
类负责创建嵌入式数据库,并且相应的配置随后在相同的测试套件(即在相同的 JVM 流程中)的多个测试场景中重用,那么这种情况就很容易发生,例如,针对嵌入式数据库的集成测试,其ApplicationContext
配置仅在 Bean 定义配置文件是活动的方面存在差异。
此类错误的根本原因是 Spring 的EmbeddedDatabaseFactory
(用于 Java 配置的<jdbc:embedded-database>
XML 名称空间元素和EmbeddedDatabaseBuilder
内部都使用)将嵌入式数据库的名称设置为testdb
(如果没有另外指定的话)。对于<jdbc:embedded-database>
的情况,嵌入式数据库通常被分配一个等于 Bean 的id
的名称(通常,类似于dataSource
)。因此,随后创建嵌入式数据库的尝试不会产生新的数据库。相反,相同的 JDBC 连接 URL 被重用,并且试图创建新的嵌入式数据库实际上指向从相同配置创建的现有嵌入式数据库。
Spring 为了解决这个常见的问题,Framework4.2 提供了为嵌入式数据库生成唯一名称的支持。要启用生成的名称,请使用以下选项之一。
EmbeddedDatabaseFactory.setGenerateUniqueDatabaseName()
EmbeddedDatabaseBuilder.generateUniqueName()
<jdbc:embedded-database generate-name="true" … >
# 3.9.7.扩展嵌入式数据库支持
你可以通过两种方式扩展 Spring JDBC 嵌入式数据库支持:
实现
EmbeddedDatabaseConfigurer
以支持新的嵌入式数据库类型。实现
DataSourceFactory
以支持新的DataSource
实现,例如连接池来管理嵌入式数据库连接。
我们鼓励你在GitHub 问题 (opens new window)上为 Spring 社区提供扩展。
# 3.10.初始化DataSource
org.springframework.jdbc.datasource.init
包支持初始化现有的DataSource
。嵌入式数据库支持提供了一种用于为应用程序创建和初始化DataSource
的选项。然而,有时你可能需要初始化在某个服务器上运行的实例。
# 3.10.1.使用 Spring XML 初始化数据库
如果要初始化数据库,并且可以提供对DataSource
Bean 的引用,则可以在initialize-database
名称空间中使用initialize-database
标记:
<jdbc:initialize-database data-source="dataSource">
<jdbc:script location="classpath:com/foo/sql/db-schema.sql"/>
<jdbc:script location="classpath:com/foo/sql/db-test-data.sql"/>
</jdbc:initialize-database>
前面的示例针对数据库运行两个指定的脚本。第一个脚本创建一个模式,第二个脚本使用测试数据集填充表。脚本位置也可以是带有通配符的模式,该通配符具有用于 Spring 中的资源的通常的 Ant 样式(例如,classpath*:/com/foo/**/sql/*-data.sql
)。如果使用模式,那么脚本将按照其 URL 或文件名的词法顺序运行。
数据库初始化器的默认行为是无条件地运行所提供的脚本。这可能并不总是你想要的——例如,如果你针对已经包含测试数据的数据库运行脚本。通过遵循先创建表然后插入数据的常见模式(如前面所示),可以减少意外删除数据的可能性。如果表已经存在,则第一步失败。
然而,为了获得对现有数据的创建和删除的更多控制,XML 名称空间提供了一些附加选项。第一个是用来打开和关闭初始化的标志。你可以根据环境来设置这个值(例如,从系统属性或环境中提取布尔值 Bean)。下面的示例从系统属性获得一个值:
<jdbc:initialize-database data-source="dataSource"
enabled="#{systemProperties.INITIALIZE_DATABASE}"> (1)
<jdbc:script location="..."/>
</jdbc:initialize-database>
1 | 从一个名为INITIALIZE_DATABASE 的系统属性获取enabled 的值。 |
---|
控制现有数据的第二个选择是更宽容地对待失败。为此,你可以控制初始化器的能力,以忽略它从脚本运行的 SQL 中的某些错误,如下例所示:
<jdbc:initialize-database data-source="dataSource" ignore-failures="DROPS">
<jdbc:script location="..."/>
</jdbc:initialize-database>
在前面的示例中,我们认为,有时脚本是针对一个空数据库运行的,因此脚本中的一些DROP
语句将会失败。因此失败的 SQLDROP
语句将被忽略,但其他失败将导致异常。如果你的 SQL 方言不支持DROP … IF EXISTS
(或类似),但你希望在重新创建测试数据之前无条件地删除所有测试数据,那么这是非常有用的。在这种情况下,第一个脚本通常是一组DROP
语句,然后是一组CREATE
语句。
ignore-failures
选项可以设置为NONE
(默认),DROPS
(忽略失败的下降),或ALL
(忽略所有失败)。
如果脚本中根本不存在;
字符,则每条语句都应该用;
或一条新的行隔开。你可以全局控制它,也可以通过脚本控制它,如下例所示:
<jdbc:initialize-database data-source="dataSource" separator="@@"> (1)
<jdbc:script location="classpath:com/myapp/sql/db-schema.sql" separator=";"/> (2)
<jdbc:script location="classpath:com/myapp/sql/db-test-data-1.sql"/>
<jdbc:script location="classpath:com/myapp/sql/db-test-data-2.sql"/>
</jdbc:initialize-database>
1 | 将分隔符脚本设置为@@ 。 |
---|---|
2 | 将db-schema.sql 的分隔符设置为; 。 |
在这个示例中,两个test-data
脚本使用@@
作为语句分隔符,只有db-schema.sql
使用;
。此配置指定了@@
的默认分隔符,并覆盖了db-schema
脚本的默认分隔符。
如果你需要的控制比从 XML 命名空间获得的更多,那么你可以直接使用DataSourceInitializer
并将其定义为应用程序中的组件。
# 依赖于数据库的其他组件的初始化
一大类应用程序(那些在 Spring 上下文启动后才使用数据库的应用程序)可以使用数据库初始化器,而不会有更多的麻烦。如果你的应用程序不是其中之一,那么你可能需要阅读本节的其余部分。
数据库初始化器依赖于DataSource
实例,并运行其初始化回调中提供的脚本(类似于 XML Bean 定义中的init-method
、组件中的@PostConstruct
方法,或者实现InitializingBean
的组件中的afterPropertiesSet()
方法)。如果其他 bean 依赖于相同的数据源,并在初始化回调中使用数据源,则可能存在问题,因为数据尚未初始化。这方面的一个常见示例是一个缓存,它在应用程序启动时急切地初始化并从数据库加载数据。
要解决这个问题,你有两种选择:将缓存初始化策略更改到稍后的阶段,或者确保首先初始化数据库初始化器。
如果应用程序在你的控制范围内,而不是在其他情况下,更改缓存初始化策略可能很容易。关于如何实现这一点的一些建议包括:
使缓存在第一次使用时就进行惰性初始化,从而提高了应用程序的启动时间。
让你的缓存或单独的组件初始化缓存实现
Lifecycle
或SmartLifecycle
。当应用程序上下文启动时,可以通过设置其autoStartup
标志自动启动SmartLifecycle
,也可以通过在封闭的上下文上调用Lifecycle
手动启动Lifecycle
。使用 Spring
ApplicationEvent
或类似的自定义观察者机制来触发缓存初始化。ContextRefreshedEvent
总是在上下文准备好使用时发布(在所有 bean 都已初始化之后),因此这通常是一个有用的钩子(这是SmartLifecycle
默认情况下的工作方式)。
确保首先初始化数据库初始化器也很容易。关于如何实现这一点的一些建议包括:
依赖于 Spring
BeanFactory
的默认行为,即以注册顺序初始化 bean。你可以通过在 XML 配置中采用一组<import/>
元素的常见做法来轻松地进行安排,这些元素对应用程序模块进行排序,并确保首先列出数据库和数据库初始化。分离
DataSource
和使用它的业务组件,并通过将它们放在单独的ApplicationContext
实例中来控制它们的启动顺序(例如,父上下文包含DataSource
,子上下文包含业务组件)。这种结构在 Spring Web 应用程序中很常见,但可以更普遍地应用。
# 4. 使用 R2DBC 进行数据访问
R2DBC (opens new window)(“反应式关系数据库连接”)是一种社区驱动的规范工作,用于使用反应式模式标准化对 SQL 数据库的访问。
# 4.1.包层次结构
Spring 框架的 R2DBC 抽象框架由两个不同的包组成:
core
:org.springframework.r2dbc.core
包包含DatabaseClient
类和各种相关的类。见使用 R2DBC 核心类来控制基本的 R2DBC 处理和错误处理。connection
:org.springframework.r2dbc.connection
包包含一个用于 easyConnectionFactory
访问的实用程序类和各种简单的ConnectionFactory
实现,你可以使用这些实现来测试和运行未修改的 R2DBC。见控制数据库连接。
# 4.2.使用 R2DBC 核心类来控制基本的 R2DBC 处理和错误处理
本节介绍如何使用 R2DBC 核心类来控制基本的 R2DBC 处理,包括错误处理。它包括以下主题:
[使用
DatabaseClient
](#r2dbc-databaseclient)[查询(
SELECT
)](#r2dbc-databaseclient-examples-query)[用
DatabaseClient
更新(INSERT
,UPDATE
,和DELETE
)](#r2dbc-databaseclient-examples-update)
# 4.2.1.使用DatabaseClient
DatabaseClient
是 R2DBC 核心包中的中心类。它处理资源的创建和释放,这有助于避免常见的错误,例如忘记关闭连接。它执行核心 R2DBC 工作流的基本任务(例如语句创建和执行),将应用程序代码留给提供 SQL 和提取结果。DatabaseClient
类:
运行 SQL 查询
更新语句和存储过程调用
在
Result
实例上执行迭代捕获 R2DBC 异常,并将其转换为在
org.springframework.dao
包中定义的通用的、信息量更大的异常层次结构。(见一致的异常层次结构。
客户机有一个功能强大、流畅的 API,它使用了用于声明性组合的反应性类型。
当你使用DatabaseClient
作为你的代码时,你只需要实现java.util.function
接口,为它们提供一个明确定义的契约。给定一个由DatabaseClient
类提供的Connection
,一个Function
回调将创建一个Publisher
。对于提取Row
结果的映射函数也是如此。
你可以通过使用ConnectionFactory
引用的直接实例化在 DAO 实现中使用DatabaseClient
,或者可以在 Spring IoC 容器中配置它,并将其作为 Bean 引用提供给 DAO。
创建DatabaseClient
对象的最简单方法是通过静态工厂方法,如下所示:
Java
DatabaseClient client = DatabaseClient.create(connectionFactory);
Kotlin
val client = DatabaseClient.create(connectionFactory)
在 Spring IOC 容器中,应该始终将 ConnectionFactory 配置为 Bean。 |
---|
前面的方法使用默认设置创建DatabaseClient
。
你还可以从DatabaseClient.builder()
获得Builder
实例。你可以通过调用以下方法来定制客户机:
….bindMarkers(…)
:提供一个特定的BindMarkersFactory
以将命名参数配置为数据库绑定标记转换。….executeFunction(…)
:设置ExecuteFunction
如何运行Statement
对象。….namedParameters(false)
:禁用命名参数展开。默认情况下启用。
方言是由[BindMarkersFactoryResolver ](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/r2dbc/core/binding/bindmarkersfactoryresolver.html)从ConnectionFactory 解析的,通常是通过检查ConnectionFactoryMetadata 。你可以通过注册一个 实现 org.springframework.r2dbc.core.binding.BindMarkersFactoryResolver$BindMarkerFactoryProvider 的类来让 Spring 自动发现你的BindMarkersFactory 。BindMarkersFactoryResolver 从使用 Spring 的 SpringFactoriesLoader 的类路径中发现 bind 标记提供者实现。 |
---|
目前支持的数据库有:
H2
马里亚布
Microsoft SQL Server
MySQL
Postgres
这个类发出的所有 SQL 都记录在DEBUG
级别下,该类别对应于客户端实例的完全限定类名称(通常DefaultDatabaseClient
)。此外,每个执行都在响应序列中注册一个检查点,以帮助调试。
下面的部分提供了DatabaseClient
用法的一些示例。这些示例并不是DatabaseClient
公开的所有功能的详尽列表。关于这一点,请参见乘务员javadoc (opens new window)。
# 执行语句
DatabaseClient
提供了运行语句的基本功能。下面的示例展示了创建新表的最小但功能齐全的代码需要包括哪些内容:
Java
Mono<Void> completion = client.sql("CREATE TABLE person (id VARCHAR(255) PRIMARY KEY, name VARCHAR(255), age INTEGER);")
.then();
Kotlin
client.sql("CREATE TABLE person (id VARCHAR(255) PRIMARY KEY, name VARCHAR(255), age INTEGER);")
.await()
DatabaseClient
是为方便、流畅的使用而设计的。它在执行规范的每个阶段公开中间方法、延续方法和终端方法。上面的示例使用then()
返回一个补全Publisher
,该补全在查询(或查询,如果 SQL 查询包含多个语句)完成后立即完成。
execute(…) 接受 SQL 查询字符串或查询Supplier<String> ,以将实际的查询创建推迟到执行时。 |
---|
# 查询(SELECT
)
SQL 查询可以通过Row
对象或受影响行数返回值。DatabaseClient
可以返回更新行数或行本身,具体取决于发出的查询。
下面的查询从表中获取id
和name
列:
Java
Mono<Map<String, Object>> first = client.sql("SELECT id, name FROM person")
.fetch().first();
Kotlin
val first = client.sql("SELECT id, name FROM person")
.fetch().awaitSingle()
以下查询使用了一个绑定变量:
爪哇
Mono<Map<String, Object>> first = client.sql("SELECT id, name FROM person WHERE first_name = :fn")
.bind("fn", "Joe")
.fetch().first();
Kotlin
val first = client.sql("SELECT id, name FROM person WHERE WHERE first_name = :fn")
.bind("fn", "Joe")
.fetch().awaitSingle()
你可能已经注意到在上面的示例中使用了fetch()
。fetch()
是一个延续运算符,它允许你指定要消耗多少数据。
调用first()
返回结果中的第一行,并丢弃剩余的行。你可以使用以下操作符来使用数据:
first()
返回整个结果的第一行。它的 Kotlin 协程变量名为awaitSingle()
,用于表示不可空的返回值,如果该值是可选的,则名为awaitSingleOrNull()
。one()
只返回一个结果,如果结果包含更多行,则该结果将失败。使用 Kotlin 协程,awaitOne()
正好代表一个值,或者awaitOneOrNull()
如果该值可能是null
。all()
返回结果的所有行。当使用 Kotlin 协程时,使用flow()
。rowsUpdated()
返回受影响的行数(INSERT
/UPDATE
/DELETE
计数)。其 Kotlin 协程变种被命名为awaitRowsUpdated()
。
在不指定更多映射细节的情况下,查询以Map
的形式返回表式结果,其键是不区分大小写的列名,这些列名映射到它们的列值。
你可以通过提供一个Function<Row, T>
来控制结果映射,每个Row
都会被调用,这样它就可以返回任意值(奇异值、集合和映射以及对象)。
下面的示例提取name
列并发出其值:
爪哇
Flux<String> names = client.sql("SELECT name FROM person")
.map(row -> row.get("name", String.class))
.all();
Kotlin
val names = client.sql("SELECT name FROM person")
.map{ row: Row -> row.get("name", String.class) }
.flow()
那么null
呢?
关系数据库结果可以包含null
值。反应流规范禁止null
值的发射。该需求要求在提取器函数中进行适当的null
处理。虽然可以从Row
中获得null
值,但不能发出null
值。你必须对对象中的任何null
值进行包装(例如,对于奇异值,Optional
),以确保提取函数永远不会直接返回null
值。
# 更新(INSERT
,UPDATE
,和DELETE
)与DatabaseClient
####
修改语句的唯一区别是,这些语句通常不返回表格数据,因此你可以使用rowsUpdated()
来使用结果。
下面的示例显示了返回更新行数的UPDATE
语句:
爪哇
Mono<Integer> affectedRows = client.sql("UPDATE person SET first_name = :fn")
.bind("fn", "Joe")
.fetch().rowsUpdated();
Kotlin
val affectedRows = client.sql("UPDATE person SET first_name = :fn")
.bind("fn", "Joe")
.fetch().awaitRowsUpdated()
# 将值绑定到查询
典型的应用程序需要参数化的 SQL 语句来根据某些输入选择或更新行。这些语句通常是SELECT
语句,受WHERE
子句或INSERT
和UPDATE
语句的约束,这些语句接受输入参数。如果参数没有正确转义,参数化语句将承担 SQL 注入的风险。DatabaseClient
利用 R2DBC 的bind
API 来消除查询参数的 SQL 注入风险。你可以使用execute(…)
操作符提供参数化 SQL 语句,并将参数绑定到实际的Statement
。然后,你的 R2DBC 驱动程序通过使用准备好的语句和参数替换来运行该语句。
参数绑定支持两种绑定策略:
通过索引,使用零基参数索引。
按名称,使用占位符名称。
下面的示例展示了查询的参数绑定:
db.sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)")
.bind("id", "joe")
.bind("name", "Joe")
.bind("age", 34);
R2DBC 原生结合标记
R2DBC 使用依赖于实际数据库供应商的数据库本地绑定标记。例如,Postgres 使用索引标记,如$1
,$2
,$n
。另一个例子是 SQL Server,它使用带有@
前缀的命名绑定标记。
这与 JDBC 不同,后者需要?
作为绑定标记。在 JDBC 中,实际的驱动程序将?
绑定标记转换为数据库本地标记,作为其语句执行的一部分。
Spring Framework 的 R2DBC 支持允许你使用本机绑定标记或带有:name
语法的命名绑定标记。
命名参数支持利用BindMarkersFactory
实例在执行查询时将命名参数扩展为本机绑定标记,这使你在不同的数据库供应商之间具有一定程度的查询可移植性。
查询预处理程序将名为Collection
的参数展开到一系列绑定标记中,以消除基于参数数量的动态查询创建的需要。嵌套对象数组被扩展以允许使用(例如)选择列表。
考虑以下查询:
SELECT id, name, state FROM table WHERE (name, age) IN (('John', 35), ('Ann', 50))
前面的查询可以参数化并按以下方式运行:
爪哇
List<Object[]> tuples = new ArrayList<>();
tuples.add(new Object[] {"John", 35});
tuples.add(new Object[] {"Ann", 50});
client.sql("SELECT id, name, state FROM table WHERE (name, age) IN (:tuples)")
.bind("tuples", tuples);
Kotlin
val tuples: MutableList<Array<Any>> = ArrayList()
tuples.add(arrayOf("John", 35))
tuples.add(arrayOf("Ann", 50))
client.sql("SELECT id, name, state FROM table WHERE (name, age) IN (:tuples)")
.bind("tuples", tuples)
选择列表的使用依赖于供应商。 |
---|
下面的示例显示了一个使用IN
谓词的更简单的变体:
爪哇
client.sql("SELECT id, name, state FROM table WHERE age IN (:ages)")
.bind("ages", Arrays.asList(35, 50));
Kotlin
val tuples: MutableList<Array<Any>> = ArrayList()
tuples.add(arrayOf("John", 35))
tuples.add(arrayOf("Ann", 50))
client.sql("SELECT id, name, state FROM table WHERE age IN (:ages)")
.bind("tuples", arrayOf(35, 50))
R2DBC 本身不支持类集合的值。尽管如此, 在上面的示例中展开给定的 List 对于 Spring 的 R2DBC 支持中的命名参数有效,例如,如上面所示,用于 IN 子句。但是,插入或更新数组类型的列(例如在 postgres 中) 需要底层 R2DBC 驱动程序支持的数组类型: 通常是一个 爪哇 数组,例如。 String[] 以更新text[] 列。不要将 Collection<String> 等作为数组参数传递。 |
---|
# 语句过滤器
有时,你需要在运行前对实际Statement
上的选项进行微调。通过DatabaseClient
注册一个Statement
过滤器(StatementFilterFunction
)来截取和修改语句在其执行中的位置,如下例所示:
爪哇
client.sql("INSERT INTO table (name, state) VALUES(:name, :state)")
.filter((s, next) -> next.execute(s.returnGeneratedValues("id")))
.bind("name", …)
.bind("state", …);
Kotlin
client.sql("INSERT INTO table (name, state) VALUES(:name, :state)")
.filter { s: Statement, next: ExecuteFunction -> next.execute(s.returnGeneratedValues("id")) }
.bind("name", …)
.bind("state", …)
DatabaseClient
公开还简化了filter(…)
接受Function<Statement, Statement>
的过载:
爪哇
client.sql("INSERT INTO table (name, state) VALUES(:name, :state)")
.filter(statement -> s.returnGeneratedValues("id"));
client.sql("SELECT id, name, state FROM table")
.filter(statement -> s.fetchSize(25));
Kotlin
client.sql("INSERT INTO table (name, state) VALUES(:name, :state)")
.filter { statement -> s.returnGeneratedValues("id") }
client.sql("SELECT id, name, state FROM table")
.filter { statement -> s.fetchSize(25) }
StatementFilterFunction
实现方式允许对Statement
对象进行过滤和对Result
对象进行过滤。
# DatabaseClient
最佳实践
一旦配置好,DatabaseClient
类的实例是线程安全的。这很重要,因为这意味着你可以配置DatabaseClient
的单个实例,然后将这个共享引用安全地注入到多个 DAO(或存储库)中。DatabaseClient
是有状态的,因为它保持了对ConnectionFactory
的引用,但是这个状态不是会话状态。
在使用DatabaseClient
类时,一种常见的做法是在 Spring 配置文件中配置ConnectionFactory
,然后在依赖项中-将共享的ConnectionFactory
注入到 DAO 类中。DatabaseClient
是在 setter 中为ConnectionFactory
创建的。这导致 DAO 类似于以下内容:
爪哇
public class R2dbcCorporateEventDao implements CorporateEventDao {
private DatabaseClient databaseClient;
public void setConnectionFactory(ConnectionFactory connectionFactory) {
this.databaseClient = DatabaseClient.create(connectionFactory);
}
// R2DBC-backed implementations of the methods on the CorporateEventDao follow...
}
Kotlin
class R2dbcCorporateEventDao(connectionFactory: ConnectionFactory) : CorporateEventDao {
private val databaseClient = DatabaseClient.create(connectionFactory)
// R2DBC-backed implementations of the methods on the CorporateEventDao follow...
}
显式配置的一种替代方法是使用组件扫描和注释支持进行依赖注入。在这种情况下,你可以用@Component
注释类(这使它成为组件扫描的候选),并用ConnectionFactory
setter 方法注解@Autowired
。下面的示例展示了如何做到这一点:
爪哇
@Component (1)
public class R2dbcCorporateEventDao implements CorporateEventDao {
private DatabaseClient databaseClient;
@Autowired (2)
public void setConnectionFactory(ConnectionFactory connectionFactory) {
this.databaseClient = DatabaseClient.create(connectionFactory); (3)
}
// R2DBC-backed implementations of the methods on the CorporateEventDao follow...
}
1 | 用@Component 注释类。 |
---|---|
2 | 用@Autowired 注释ConnectionFactory setter 方法。 |
3 | 用ConnectionFactory 创建一个新的DatabaseClient 。 |
Kotlin
@Component (1)
class R2dbcCorporateEventDao(connectionFactory: ConnectionFactory) : CorporateEventDao { (2)
private val databaseClient = DatabaseClient(connectionFactory) (3)
// R2DBC-backed implementations of the methods on the CorporateEventDao follow...
}
1 | 用@Component 注释类。 |
---|---|
2 | ConnectionFactory 的构造函数注入。 |
3 | 用ConnectionFactory 创建一个新的DatabaseClient 。 |
无论你选择使用(或不使用)上述哪种模板初始化样式,每次想要运行 SQL 时,都很少需要创建DatabaseClient
类的新实例。一旦配置好,DatabaseClient
实例就是线程安全的。如果你的应用程序访问多个数据库,你可能需要多个DatabaseClient
实例,这需要多个ConnectionFactory
实例,然后需要多个配置不同的DatabaseClient
实例。
# 4.3.检索自动生成的密钥
INSERT
语句在向定义自动增量或标识列的表中插入行时可能会生成键。要获得对要生成的列名的完全控制,只需注册一个StatementFilterFunction
,它为所需的列请求生成的键。
爪哇
Mono<Integer> generatedId = client.sql("INSERT INTO table (name, state) VALUES(:name, :state)")
.filter(statement -> s.returnGeneratedValues("id"))
.map(row -> row.get("id", Integer.class))
.first();
// generatedId emits the generated key once the INSERT statement has finished
Kotlin
val generatedId = client.sql("INSERT INTO table (name, state) VALUES(:name, :state)")
.filter { statement -> s.returnGeneratedValues("id") }
.map { row -> row.get("id", Integer.class) }
.awaitOne()
// generatedId emits the generated key once the INSERT statement has finished
# 4.4.控制数据库连接
本节内容包括:
[使用
ConnectionFactory
](#r2dbc-connectionFactory)[使用
ConnectionFactoryUtils
](#r2dbc-connectionfactoryutils)[使用
SingleConnectionFactory
](#R2DBC-SingleConnectionFactory)[使用
TransactionAwareConnectionFactoryProxy
](#r2dbc-transactionawareConnectionFactoryProxy)[using
R2dbcTransactionManager
](#r2dbc-r2dbctransactionmanager)
# 4.4.1.使用ConnectionFactory
Spring 通过ConnectionFactory
获得到数据库的 R2DBC 连接。aConnectionFactory
是 R2DBC 规范的一部分,是驱动程序的一个常见入口点。它允许容器或框架从应用程序代码中隐藏连接池和事务管理问题。作为开发人员,你不需要了解有关如何连接到数据库的详细信息。这是设置ConnectionFactory
的管理员的责任。在开发和测试代码时,你最有可能同时填充这两个角色,但是你并不一定要知道生产数据源是如何配置的。
当你使用 Spring 的 R2DBC 层时,你可以使用第三方提供的连接池实现来配置你自己的连接池。一个流行的实现是 R2DBC 池()。 Spring 发行版中的实现仅用于测试目的,并不提供池。
要配置ConnectionFactory
:
获得与
ConnectionFactory
的连接,就像你通常获得 R2dbcConnectionFactory
一样。提供一个 R2DBC URL(请参阅驱动程序的文档以获得正确的值)。
下面的示例展示了如何配置ConnectionFactory
:
爪哇
ConnectionFactory factory = ConnectionFactories.get("r2dbc:h2:mem:///test?options=DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE");
Kotlin
val factory = ConnectionFactories.get("r2dbc:h2:mem:///test?options=DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE");
# 4.4.2.使用ConnectionFactoryUtils
ConnectionFactoryUtils
类是一个方便且功能强大的助手类,它提供static
方法来从ConnectionFactory
获得连接和紧密连接(如果需要)。
它支持订阅者Context
的绑定连接,例如R2dbcTransactionManager
。
# 4.4.3.使用SingleConnectionFactory
SingleConnectionFactory
类是DelegatingConnectionFactory
接口的一种实现,该接口封装单个Connection
,该接口在每次使用后都不会关闭。
如果任何客户机代码在假定池连接的情况下调用close
(如使用持久性工具时),则应将suppressClose
属性设置为true
。此设置返回一个封装物理连接的关闭抑制代理。请注意,你不能再将其强制转换为本机Connection
或类似的对象。
SingleConnectionFactory
主要是一个测试类,如果你的 R2DBC 驱动程序允许这样的使用,它可以用于特定的需求,例如流水线。与池ConnectionFactory
相反,它始终重用相同的连接,避免了过多地创建物理连接。
# 4.4.4.使用TransactionAwareConnectionFactoryProxy
TransactionAwareConnectionFactoryProxy
是目标ConnectionFactory
的代理。代理包装的目标是ConnectionFactory
,以添加对 Spring 管理的事务的感知。
如果你使用的 R2DBC 客户机不是集成的,则需要使用这个类,否则将支持 Spring 的 R2DBC。在这种情况下,你仍然可以使用这个客户机,并且在 的同时,让这个客户机参与 Spring 托管事务。通常情况下, 最好是将具有对 ConnectionFactoryUtils 的正确访问的 R2DBC 客户机集成在一起,以进行资源管理。 |
---|
参见[TransactionAwareConnectionFactoryProxy
](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/r2dbc/connection/transactionawareConnectionFactoryproxy.html)爪哇doc 获取更多细节。
# 4.4.5.使用R2dbcTransactionManager
R2dbcTransactionManager
类是用于单个 R2DBC 数据源的ReactiveTransactionManager
实现。它将从指定的连接工厂到订阅服务器Context
的 R2DBC 连接绑定,可能允许每个连接工厂使用一个订阅服务器连接。
需要应用程序代码来通过ConnectionFactoryUtils.getConnection(ConnectionFactory)
检索 R2DBC 连接,而不是 R2DBC 的标准ConnectionFactory.create()
。
所有框架类(如DatabaseClient
)都隐式地使用此策略。如果不与此事务管理器一起使用,则查找策略的行为与常见策略完全相同。因此,它可以在任何情况下使用。
R2dbcTransactionManager
类支持应用于连接的自定义隔离级别。
# 5. 对象关系映射数据访问
本节介绍使用对象关系映射时的数据访问。
# 5.1.ORM 简介 Spring
Spring 框架支持与 爪哇 持久性 API( JPA)的集成,并支持用于资源管理、数据访问对象实现和事务策略的本机 Hibernate。例如,对于 Hibernate,有几个方便的 IoC 特性的一流支持,这些特性解决了许多典型的 Hibernate 集成问题。你可以通过依赖项注入为 OR(对象关系)映射工具配置所有受支持的特性。它们可以参与 Spring 的资源和事务管理,并且符合 Spring 的通用事务和 DAO 异常层次结构。推荐的集成风格是针对普通的 Hibernate 或 JPA API 对 DAO 进行编码。
Spring 在创建数据访问应用程序时,为你选择的 ORM 层增加了显著的增强。你可以根据需要利用尽可能多的集成支持,并且应该将这种集成工作与内部构建类似基础设施的成本和风险进行比较。无论采用何种技术,你都可以像使用库一样使用大量的 ORM 支持,因为所有的东西都被设计为一组可重用的 爪哇Bean。 Spring IOC 容器中的 ORM 促进了配置和部署。因此,本节中的大多数示例都显示了 Spring 容器内的配置。
使用 Spring 框架创建 ORM DAO 的好处包括:
更容易的测试。 Spring 的 IOC 方法使得很容易交换 Hibernate
SessionFactory
实例、JDBCDataSource
实例、事务管理器和映射对象实现(如果需要)的实现和配置位置。这进而使得隔离地测试与持久性相关的每一段代码变得容易得多。常见的数据访问异常。 Spring 可以从你的 ORM 工具中包装异常,将它们从专有的(可能经过检查的)异常转换为公共的运行时
DataAccessException
层次结构。这个特性允许你仅在适当的层中处理大多数持久性异常(这些异常是不可恢复的),而不需要烦人的样板文件捕获、抛出和异常声明。你仍然可以根据需要捕获和处理异常。请记住,JDBC 异常(包括特定于 DB 的方言)也被转换为相同的层次结构,这意味着你可以在一致的编程模型中使用 JDBC 执行一些操作。一般资源管理。 Spring 应用程序上下文可以处理 Hibernate
SessionFactory
实例、 JPAEntityManagerFactory
实例、JDBCDataSource
实例的位置和配置,以及其他相关资源。这使得这些价值观易于管理和改变。 Spring 提供了对持久性资源的高效、简单和安全的处理。例如,使用 Hibernate 的相关代码通常需要使用相同的 HibernateSession
,以确保效率和适当的事务处理。 Spring 通过 HibernateSessionFactory
公开当前的Session
,使得创建Session
并将其透明地绑定到当前的线程变得容易。因此, Spring 解决了 Hibernate 典型使用的许多慢性问题,适用于任何本地或 JTA 事务环境。**综合事务管理。**你可以通过
@Transactional
注释或通过在 XML 配置文件中显式配置事务 AOP 通知,用声明性的、面向方面的编程( AOP)风格的方法拦截器包装你的 ORM 代码。在这两种情况下,事务语义和异常处理(回滚等)都是为你处理的。正如资源和事务管理中所讨论的,你还可以交换各种事务管理器,而不会影响与 ORM 相关的代码。例如,你可以在本地事务和 JTA 之间进行交换,在这两个场景中都可以使用相同的完整服务(例如声明式事务)。此外,与 JDBC 相关的代码可以完全集成到用于执行 ORM 的代码中。这对于不适合 ORM 的数据访问(例如批处理和 BLOB 流)很有用,但仍然需要与 ORM 操作共享公共事务。
有关更全面的 ORM 支持,包括对替代数据库 技术(如 MongoDB)的支持,你可能想查看Spring Data (opens new window)项目套件。如果你是 JPA 用户,Getting Started Accessing Data with JPA (opens new window)中的https://spring.io (opens new window)指南提供了一个很好的介绍。 |
---|
# 5.2.ORM 集成的一般考虑因素
本节重点介绍了适用于所有 ORM 技术的注意事项。Hibernate部分提供了更多细节,并在一个具体的上下文中显示了这些特性和配置。
Spring ORM 集成的主要目标是明确的应用程序分层(使用任何数据访问和事务技术),并实现应用程序对象的松耦合——不再依赖于数据访问或事务策略的业务服务,不再需要硬编码的资源查找,不再需要难以替换的单件件,没有更多的自定义服务注册中心。我们的目标是使用一种简单且一致的方法来连接应用程序对象,使它们尽可能地可重用且不依赖于容器。所有单独的数据访问功能都可以单独使用,但与 Spring 的应用程序上下文概念很好地集成在一起,提供了基于 XML 的配置和对普通 爪哇Bean 实例的交叉引用,这些实例不需要 Spring 感知。 Spring 在典型的应用程序中,许多重要的对象是 爪哇Bean:数据访问模板、数据访问对象、事务管理器、使用数据访问对象的业务服务和事务管理器、Web 视图解析器、使用业务服务的 Web 控制器,等等。
# 5.2.1.资源和事务管理
典型的业务应用程序充斥着重复的资源管理代码。许多项目试图发明自己的解决方案,有时为了编程方便而牺牲了对故障的正确处理。 Spring 提倡用于适当的资源处理的简单解决方案,即在 JDBC 的情况下通过模板化来实现 IOC,并为 ORM 技术应用 AOP 拦截器。
基础架构提供了正确的资源处理,并将特定的 API 异常适当地转换为未经检查的基础架构异常层次结构。 Spring 引入了一种 DAO 异常层次结构,适用于任何数据访问策略。对于直接 JDBC,JdbcTemplate
中提到的JdbcTemplate
类提供连接处理和SQLException
到DataAccessException
层次结构的正确转换,包括将数据库特定的 SQL 错误代码转换为有意义的异常类。对于 ORM 技术,请参见下一节,了解如何获得相同的异常转换好处。
当涉及到事务管理时,JdbcTemplate
类与 Spring 事务支持挂钩,并通过相应的 Spring 事务管理器支持 JTA 和 JDBC 事务。对于所支持的 ORM 技术, Spring 通过 Hibernate 和 JPA 事务管理器以及 JTA 支持提供 Hibernate 和 JPA 支持。有关事务支持的详细信息,请参见事务管理章节。
# 5.2.2.异常转换
在 DAO 中使用 Hibernate 或 JPA 时,必须决定如何处理持久性技术的本机异常类。根据技术的不同,DAO 抛出了HibernateException
或PersistenceException
的子类。这些异常都是运行时异常,不需要声明或捕获。你可能还需要处理IllegalArgumentException
和IllegalStateException
。这意味着调用者只能将异常视为通常是致命的,除非他们希望依赖于持久性技术自身的异常结构。如果不将调用方与实现策略绑定,就不可能捕获特定的原因(例如乐观锁定失败)。这种权衡对于基于强 ORM 或不需要任何特殊异常处理(或两者兼而有之)的应用程序来说可能是可以接受的。然而, Spring 允许通过@Repository
注释透明地应用异常翻译。下面的示例(一个用于 爪哇 配置,一个用于 XML 配置)展示了如何这样做:
爪哇
@Repository
public class ProductDaoImpl implements ProductDao {
// class body here...
}
Kotlin
@Repository
class ProductDaoImpl : ProductDao {
// class body here...
}
<beans>
<!-- Exception translation bean post processor -->
<bean class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor"/>
<bean id="myProductDao" class="product.ProductDaoImpl"/>
</beans>
后处理器会自动查找所有异常翻译器(PersistenceExceptionTranslator
接口的实现),并建议所有标有@Repository
注释的 bean,以便发现的翻译器可以拦截并在抛出的异常上应用适当的翻译。
总之,你可以基于纯持久性技术的 API 和注释实现 DAO,同时仍然受益于 Spring 管理的事务、依赖注入和透明的异常转换(如果需要的话)到 Spring 的自定义异常层次结构。
# 5.3. Hibernate
我们从在 Spring 环境中覆盖Hibernate 5 (opens new window)开始,使用它来演示 Spring 用于集成或映射的方法。本节详细介绍了许多问题,并展示了 DAO 实现和事务划分的不同变体。这些模式中的大多数可以直接转换为所有其他受支持的 ORM 工具。本章后面的部分将介绍其他 ORM 技术,并展示简要的示例。
在 Spring Framework5.3 中, Spring 对于 Spring 的HibernateJpaVendorAdapter 以及对于本机 Hibernate SessionFactory 设置,需要 Hibernate orm5.2+。,对于新启动的应用程序,强烈建议使用 Hibernate orm5.4。,用于 HibernateJpaVendorAdapter , Hibernate 搜索需要升级到 5.1 1.6。 |
---|
# 5.3.1.SessionFactory
Spring 容器中的设置
为了避免将应用程序对象绑定到硬编码的资源查找,可以将资源(例如 JDBC或 Hibernate )定义为 Spring 容器中的 bean。需要访问资源的应用程序对象通过 Bean 引用接收对此类预定义实例的引用,如下一节中的 DAO 定义所示。
以下摘自 XML 应用程序上下文定义的节选显示了如何在其上设置一个 JDBCDataSource
和一个 Hibernate SessionFactory
:
<beans>
<bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="org.hsqldb.jdbcDriver"/>
<property name="url" value="jdbc:hsqldb:hsql://localhost:9001"/>
<property name="username" value="sa"/>
<property name="password" value=""/>
</bean>
<bean id="mySessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean">
<property name="dataSource" ref="myDataSource"/>
<property name="mappingResources">
<list>
<value>product.hbm.xml</value>
</list>
</property>
<property name="hibernateProperties">
<value>
hibernate.dialect=org.hibernate.dialect.HSQLDialect
</value>
</property>
</bean>
</beans>
从本地的 Jakarta Commons DBCP切换到位于 JNDI 的(通常由应用程序服务器管理)只是一个配置问题,如下例所示:
<beans>
<jee:jndi-lookup id="myDataSource" jndi-name="java:comp/env/jdbc/myds"/>
</beans>
你还可以访问位于SessionFactory
的 JNDI,使用 Spring 的JndiObjectFactoryBean
/<jee:jndi-lookup>
来检索和公开它。然而,在 EJB 上下文之外,这通常是不常见的。
Spring 还提供了一个LocalSessionFactoryBuilder 变体,将与 @Bean 样式配置和编程设置无缝集成在一起(不涉及FactoryBean )。既 LocalSessionFactoryBean 又LocalSessionFactoryBuilder 支持后台引导, Hibernate 初始化与给定的引导程序执行器上的应用程序 引导程序线程并行运行(例如 SimpleAsyncTaskExecutor )。在 LocalSessionFactoryBean 上,这可以通过bootstrapExecutor 属性获得。在程序化的LocalSessionFactoryBuilder 上,有一个重载的buildSessionFactory 方法,它接受一个 Bootstrap 执行器参数。从 Spring Framework5.1 开始,这样的原生 Hibernate 设置还可以公开一个 JPA EntityManagerFactory 用于在原生 Hibernate 访问旁边的标准 JPA 交互的方法。详见Native Hibernate Setup for JPA。 |
---|
# 5.3.2.基于普通 Hibernate API 实现 DAOS
Hibernate 具有称为上下文会话的功能,其中 Hibernate 本身管理一个当前的Session
每个事务。这大致相当于 Spring 的每笔交易同步一次 Hibernate Session
。基于普通的 Hibernate API,相应的 DAO 实现类似于以下示例:
Java
public class ProductDaoImpl implements ProductDao {
private SessionFactory sessionFactory;
public void setSessionFactory(SessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
}
public Collection loadProductsByCategory(String category) {
return this.sessionFactory.getCurrentSession()
.createQuery("from test.Product product where product.category=?")
.setParameter(0, category)
.list();
}
}
Kotlin
class ProductDaoImpl(private val sessionFactory: SessionFactory) : ProductDao {
fun loadProductsByCategory(category: String): Collection<*> {
return sessionFactory.currentSession
.createQuery("from test.Product product where product.category=?")
.setParameter(0, category)
.list()
}
}
这种样式类似于 Hibernate 引用文档和示例的样式,只是在实例变量中持有SessionFactory
。在 Hibernate 的 CaveaTemptor 示例应用程序中的老派static``HibernateUtil
类之上,我们强烈推荐这样一种基于实例的设置。(通常,除非绝对必要,否则不要在static
变量中保留任何资源。
前面的 DAO 示例遵循依赖注入模式。它非常适合 Spring IOC 容器,就像它与 Spring 的HibernateTemplate
编码一样。你也可以在普通 Java 中设置这样的 DAO(例如,在单元测试中)。要这样做,实例化它,并使用所需的工厂引用调用setSessionFactory(..)
。作为 Spring Bean 的定义,DAO 类似于以下内容:
<beans>
<bean id="myProductDao" class="product.ProductDaoImpl">
<property name="sessionFactory" ref="mySessionFactory"/>
</bean>
</beans>
这种 DAO 样式的主要优点是它仅依赖于 Hibernate API。不需要输入任何 Spring 类。从非侵犯性的角度来看,这是有吸引力的,并且对于 Hibernate 开发人员来说可能感觉更自然。
然而,DAO 抛出了普通的HibernateException
(它未经检查,因此不必声明或捕获),这意味着调用者只能将异常视为通常是致命的——除非他们希望依赖 Hibernate 自己的异常层次结构。如果不将调用方与实现策略绑定,就不可能捕获特定的原因(例如乐观锁定失败)。这种权衡对于基于强 Hibernate 的应用程序、不需要任何特殊的异常处理,或者两者兼而有之的应用程序来说都是可以接受的。
幸运的是, Spring 的LocalSessionFactoryBean
支持 Hibernate 的SessionFactory.getCurrentSession()
方法用于任何 Spring 事务策略,返回当前 Spring-管理的事务Session
,甚至使用HibernateTransactionManager
。该方法的标准行为仍然是返回当前与正在进行的 JTA 事务关联的Session
(如果有的话)。无论你是使用 Spring 的JtaTransactionManager
、EJB 容器管理事务,还是使用 JTA,都会应用此行为。
总之,你可以基于普通的 Hibernate API 实现 DAO,同时仍然能够参与由 Spring 管理的事务。
# 5.3.3.声明式事务划分
我们建议你使用 Spring 的声明式事务支持,它允许你用 AOP 事务拦截器替换 Java 代码中的显式事务划分 API 调用。你可以使用 Java 注释或 XML 在 Spring 容器中配置此事务拦截器。这种声明性事务功能使你能够使业务服务免于重复的事务划分代码,并专注于添加业务逻辑,这是应用程序的真正价值所在。
在你继续之前,我们强烈建议你阅读声明式事务管理,如果你还没有这样做。 |
---|
你可以使用@Transactional
注释对服务层进行注释,并指示 Spring 容器查找这些注释,并为这些注释的方法提供事务语义。下面的示例展示了如何做到这一点:
Java
public class ProductServiceImpl implements ProductService {
private ProductDao productDao;
public void setProductDao(ProductDao productDao) {
this.productDao = productDao;
}
@Transactional
public void increasePriceOfAllProductsInCategory(final String category) {
List productsToChange = this.productDao.loadProductsByCategory(category);
// ...
}
@Transactional(readOnly = true)
public List<Product> findAllProducts() {
return this.productDao.findAllProducts();
}
}
Kotlin
class ProductServiceImpl(private val productDao: ProductDao) : ProductService {
@Transactional
fun increasePriceOfAllProductsInCategory(category: String) {
val productsToChange = productDao.loadProductsByCategory(category)
// ...
}
@Transactional(readOnly = true)
fun findAllProducts() = productDao.findAllProducts()
}
在容器中,你需要设置PlatformTransactionManager
实现(作为 Bean)和<tx:annotation-driven/>
条目,在运行时选择进入@Transactional
处理。下面的示例展示了如何做到这一点:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
https://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- SessionFactory, DataSource, etc. omitted -->
<bean id="transactionManager"
class="org.springframework.orm.hibernate5.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory"/>
</bean>
<tx:annotation-driven/>
<bean id="myProductService" class="product.SimpleProductService">
<property name="productDao" ref="myProductDao"/>
</bean>
</beans>
# 5.3.4.程序化事务划分
你可以在应用程序的较高级别上,在跨越任意数量操作的较低级别的数据访问服务之上,对事务进行划分。对周围业务服务的实现也不存在限制。它只需要一个 Spring PlatformTransactionManager
。同样,后者可以来自任何地方,但最好是通过setTransactionManager(..)
方法作为 Bean 引用。同样,productDAO
应该由setProductDao(..)
方法设置。以下一对片段显示了 Spring 应用程序上下文中的事务管理器和业务服务定义,以及业务方法实现的示例:
<beans>
<bean id="myTxManager" class="org.springframework.orm.hibernate5.HibernateTransactionManager">
<property name="sessionFactory" ref="mySessionFactory"/>
</bean>
<bean id="myProductService" class="product.ProductServiceImpl">
<property name="transactionManager" ref="myTxManager"/>
<property name="productDao" ref="myProductDao"/>
</bean>
</beans>
Java
public class ProductServiceImpl implements ProductService {
private TransactionTemplate transactionTemplate;
private ProductDao productDao;
public void setTransactionManager(PlatformTransactionManager transactionManager) {
this.transactionTemplate = new TransactionTemplate(transactionManager);
}
public void setProductDao(ProductDao productDao) {
this.productDao = productDao;
}
public void increasePriceOfAllProductsInCategory(final String category) {
this.transactionTemplate.execute(new TransactionCallbackWithoutResult() {
public void doInTransactionWithoutResult(TransactionStatus status) {
List productsToChange = this.productDao.loadProductsByCategory(category);
// do the price increase...
}
});
}
}
Kotlin
class ProductServiceImpl(transactionManager: PlatformTransactionManager,
private val productDao: ProductDao) : ProductService {
private val transactionTemplate = TransactionTemplate(transactionManager)
fun increasePriceOfAllProductsInCategory(category: String) {
transactionTemplate.execute {
val productsToChange = productDao.loadProductsByCategory(category)
// do the price increase...
}
}
}
Spring 的TransactionInterceptor
允许使用回调码抛出任何选中的应用程序异常,而TransactionTemplate
仅限于回调中未选中的异常。TransactionTemplate
在未检查的应用程序异常的情况下,或者如果事务被应用程序标记为只回滚(通过设置TransactionStatus
),则触发回滚。默认情况下,TransactionInterceptor
的行为方式相同,但每个方法都允许可配置的回滚策略。
# 5.3.5.事务管理策略
都TransactionTemplate
并且TransactionInterceptor
将实际事务处理委托给PlatformTransactionManager
实例(对于单个 Hibernate SessionFactory
,可以是HibernateTransactionManager
(对于单个 Hibernate SessionFactory
)),方法是使用ThreadLocal
下的遮蔽物)或用于 Hibernate 应用程序的JtaTransactionManager
(委托给容器的 JTA 子系统)。你甚至可以使用自定义PlatformTransactionManager
实现。从本机 Hibernate 事务管理切换到 JTA(例如,当你的应用程序的某些部署面临分布式事务需求时)只是一个配置问题。你可以用 Spring 的 JTA 事务实现替换 Hibernate 事务管理器。事务划分和数据访问代码都不做任何更改,因为它们使用通用的事务管理 API。
对于跨多个 Hibernate 会话工厂的分布式事务,你可以将JtaTransactionManager
合并为具有多个LocalSessionFactoryBean
定义的事务策略。然后,每个 DAO 获得一个特定的SessionFactory
引用,该引用被传递到其相应的 Bean 属性中。如果所有底层 JDBC 数据源都是事务容器数据源,那么业务服务可以在不需要特别注意的情况下在任意数量的 DAO 和任意数量的会话工厂之间划分事务,只要它使用JtaTransactionManager
作为策略。
HibernateTransactionManager
和JtaTransactionManager
都允许使用 Hibernate 进行适当的 JVM 级缓存处理,而不需要容器特定的事务管理器查找或 JCA 连接器(如果不使用 EJB 发起事务)。
HibernateTransactionManager
可以将 Hibernate JDBCConnection
导出为特定DataSource
的普通 JDBC 访问代码。如果只访问一个数据库,这种能力允许在完全没有 JTA 的情况下使用混合 Hibernate 和 JDBC 数据访问的高级事务划分。HibernateTransactionManager
如果你通过LocalSessionFactoryBean
类的dataSource
属性设置了带DataSource
的传入-inSessionFactory
事务,则自动将 Hibernate 事务公开为 JDBC 事务。或者,你可以显式地指定DataSource
,其中事务应该通过HibernateTransactionManager
类的dataSource
属性公开。
# 5.3.6.比较容器管理的资源和本地定义的资源
你可以在容器管理的 JNDISessionFactory
和本地定义的 JNDI 之间进行切换,而无需更改一行应用程序代码。是将资源定义保留在容器中,还是保留在应用程序的本地中,主要取决于你使用的事务策略。与 Spring 定义的本地SessionFactory
相比,手动注册的 JNDISessionFactory
不提供任何好处。通过 Hibernate 的 JCA 连接器部署SessionFactory
提供了参与 Java EE 服务器的管理基础架构的附加价值,但不会增加更多的实际价值。
Spring 的事务支持不绑定到容器。当使用 JTA 以外的任何策略进行配置时,事务支持也可以在独立或测试环境中工作。特别是在单数据库事务的典型情况下, Spring 的单资源本地事务支持是 JTA 的一种轻量级和强大的替代方案。当使用本地 EJB 无状态会话 bean 来驱动事务时,你既依赖于 EJB 容器,也依赖于 JTA,即使你只访问一个数据库,并且仅使用无状态会话 bean 通过容器管理的事务来提供声明性事务。以编程方式直接使用 JTA 还需要一个 Java EE 环境。JTA 并不只涉及 JTA 本身和 JNDIDataSource
实例方面的容器依赖关系。对于非 Spring、JTA 驱动的 Hibernate 事务,你必须使用 Hibernate JCA 连接器或带有TransactionManagerLookup
的额外 Hibernate 事务代码,该代码被配置为适当的 JVM 级缓存。
Spring-驱动的事务可以与本地定义的 Hibernate SessionFactory
一起工作,就像它们与本地 JDBCDataSource
一起工作一样,只要它们访问单个数据库。因此,当你有分布式事务需求时,你只需要使用 Spring 的 JTA 事务策略。JCA 连接器需要特定于容器的部署步骤,并且(显然)首先需要 JCA 支持。这种配置需要更多的工作,而不是部署具有本地资源定义和 Spring 驱动事务的简单 Web 应用程序。此外,如果你使用 WebLogic Express(它不提供 JCA),则通常需要容器的 Enterprise 版本。 Spring 具有跨越单个数据库的本地资源和事务的应用程序在任何 Java EE Web 容器(不包括 JTA、JCA 或 EJB)中工作,例如 Tomcat、Resin,甚至是普通的 Jetty。此外,你可以轻松地在桌面应用程序或测试套件中重用这样的中间层。
综合考虑,如果不使用 EJB,请坚持使用本地SessionFactory
设置和 Spring 的HibernateTransactionManager
或JtaTransactionManager
。你获得了所有的好处,包括适当的事务 JVM 级缓存和分布式事务,而不会带来容器部署的不便。 Hibernate SessionFactory
通过 JCA 连接器进行的 JNDI 注册只有在与 EJB 结合使用时才会增加价值。
# 5.3.7.带有 Hibernate 的虚假应用程序服务器警告
在一些具有非常严格的XADataSource
实现的 JTA 环境中(目前是一些 WebLogic Server 和 WebSphere 版本),当 Hibernate 在不考虑该环境的 JTA 事务管理器的情况下进行配置时,可能会在应用程序服务器日志中显示虚假的警告或异常。这些警告或异常表示正在访问的连接不再有效或 JDBC 访问不再有效,这可能是因为事务不再活动。举个例子,这里有一个来自 WebLogic 的实际例外:
java.sql.SQLException: The transaction is no longer active - status: 'Committed'. No
further JDBC access is allowed within this transaction.
另一个常见的问题是 JTA 事务之后的连接泄漏, Hibernate 会话(以及潜在的底层 JDBC 连接)无法正确关闭。
你可以通过使 Hibernate 了解 JTA 事务管理器来解决此类问题,JTA 事务管理器与 Spring 同步到该事务管理器。你有两个选择:
将你的 Spring
JtaTransactionManager
Bean 传递到你的 Hibernate 设置中。最简单的方法是 Bean 引用jtaTransactionManager
属性中的LocalSessionFactoryBean
Bean(参见Hibernate Transaction Setup)。 Spring 然后使相应的 JTA 策略可用于 Hibernate。你还可以在
LocalSessionFactoryBean
上的“HibernateProperties”中显式地配置 Hibernate 的 JTA 相关属性,特别是“ Hibernate.transaction.coordinator_class”、“ Hibernate.connection.handling_mode”和潜在的“ Hibernate.transaction.jta.platform”(有关这些属性的详细信息,请参见 Hibernate 的手册)。
本节的其余部分描述了在 Hibernate 意识到 JTA的情况下和不意识到 JTA的情况下发生的事件的顺序。
Hibernate 未配置任何对 JTA 事务管理器的了解时,当 JTA 事务提交时发生以下事件:
提交 JTA 事务。
Spring 的
JtaTransactionManager
与 JTA 事务同步,因此 JTA 事务管理器通过afterCompletion
回调回调它。在其他活动中,这种同步可以通过 Hibernate 的
afterTransactionCompletion
回调(用于清除 Hibernate 缓存)触发 Spring 到 Hibernate 的回调,然后在 Hibernate 会话上执行显式的close()
调用,这会导致 Hibernate 尝试close()
JDBC 连接。在某些环境中,这个
Connection.close()
调用会触发警告或错误,因为应用程序服务器不再认为Connection
是可用的,因为事务已经提交。
Hibernate 配置了对 JTA 事务管理器的感知时,当 JTA 事务提交时会发生以下事件:
JTA 事务已准备好提交。
Spring 的
JtaTransactionManager
与 JTA 事务同步,因此 JTA 事务管理器通过beforeCompletion
回调回调事务。Spring 意识到 Hibernate 本身与 JTA 事务同步,并且其行为与在前面的场景中不同。特别是,它与 Hibernate 的事务性资源管理保持一致。
提交 JTA 事务。
Hibernate 是同步到 JTA 事务的,因此该事务通过 JTA 事务管理器的
afterCompletion
回调被调回,并且可以正确地清除其缓存。
# 5.4. JPA
Spring JPA,在包下可用,以类似于与 Hibernate 集成的方式提供对的全面支持,同时意识到底层实现,以便提供额外的特征。
# 5.4.1.在 Spring 环境中用于 JPA 设置的三个选项
Spring JPA 支持提供了设置 JPA 的三种方式,其被应用程序用于获得实体管理器。
[using
LocalEntityManagerFactoryBean
](#orm- JPA-setup-lemfb)[using
LocalContainerEntityManagerFactoryBean
](#orm- JPA-setup-lcemfb)
# 使用LocalEntityManagerFactoryBean
你只能在简单的部署环境中使用此选项,例如独立应用程序和集成测试。
LocalEntityManagerFactoryBean
创建了一个EntityManagerFactory
,适用于应用程序仅使用 JPA 进行数据访问的简单部署环境。工厂 Bean 使用 JPA PersistenceProvider
自动检测机制(根据 JPA 的 Java SE 引导),并且在大多数情况下,只需要指定持久性单元名称。下面的 XML 示例配置了这样的 Bean:
<beans>
<bean id="myEmf" class="org.springframework.orm.jpa.LocalEntityManagerFactoryBean">
<property name="persistenceUnitName" value="myPersistenceUnit"/>
</bean>
</beans>
JPA 部署的这种形式是最简单和最有限的。你无法引用现有的 JDBCDataSource
Bean 定义,并且不存在对全局事务的支持。此外,持久类的编织(字节代码转换)是特定于提供者的,通常需要在启动时指定特定的 JVM 代理。此选项仅适用于 JPA 规范为其设计的独立应用程序和测试环境。
# 从 JNDI 获得 EntityManagerFactory
在部署到 Java EE 服务器时可以使用此选项。检查你的服务器的文档,了解如何将自定义 JPA 提供程序部署到你的服务器中,从而允许使用不同于服务器默认值的提供程序。
从 JNDI(例如在 Java EE 环境中)获得EntityManagerFactory
是更改 XML 配置的问题,如下例所示:
<beans>
<jee:jndi-lookup id="myEmf" jndi-name="persistence/myPersistenceUnit"/>
</beans>
此操作假定了标准的 Java EE 引导。Java EE 服务器自动检测持久化单元(实际上是应用程序 JAR 中的META-INF/persistence.xml
文件)和persistence-unit-ref
Java EE 部署描述符中的条目(例如,web.xml
),并为这些持久化单元定义环境命名上下文位置。
在这样的场景中,整个持久性单元部署,包括持久性类的编织(字节代码转换),由 Java EE 服务器决定。JDBCDataSource
是通过META-INF/persistence.xml
文件中的一个 JNDI 位置定义的。EntityManager
事务与服务器的 JTA 子系统集成在一起。 Spring 仅使用获得的EntityManagerFactory
,通过依赖注入将其传递给应用程序对象,并为持久性单元管理事务(通常通过JtaTransactionManager
)。
如果在同一个应用程序中使用多个持久化单元,则此类 JNDI 检索的持久化单元的 Bean 名称应该与应用程序用于引用它们的持久化单元名称相匹配(例如,在@PersistenceUnit
和@PersistenceContext
注释中)。
# 使用LocalContainerEntityManagerFactoryBean
你可以在基于 Spring 的应用程序环境中使用此选项以获得完整的 JPA 功能。这包括诸如 Tomcat 之类的 Web 容器、独立的应用程序以及具有复杂持久性需求的集成测试。
如果你想专门配置一个 Hibernate 设置,那么一个直接的替代方案 是设置一个本机 Hibernate LocalSessionFactoryBean ,而不是一个普通的 JPA LocalContainerEntityManagerFactoryBean ,让它与 JPA 访问代码以及本机 Hibernate 访问代码进行交互。 详情请参见Native Hibernate setup for JPA interaction。 |
---|
LocalContainerEntityManagerFactoryBean
提供了对EntityManagerFactory
配置的完全控制,并且适合于需要细粒度定制的环境。LocalContainerEntityManagerFactoryBean
基于persistence.xml
文件、提供的dataSourceLookup
策略和指定的loadTimeWeaver
创建一个PersistenceUnitInfo
实例。因此,可以使用 JNDI 之外的自定义数据源并控制编织过程。下面的示例显示了LocalContainerEntityManagerFactoryBean
的典型 Bean 定义:
<beans>
<bean id="myEmf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="dataSource" ref="someDataSource"/>
<property name="loadTimeWeaver">
<bean class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver"/>
</property>
</bean>
</beans>
下面的示例显示了一个典型的persistence.xml
文件:
<persistence xmlns="http://java.sun.com/xml/ns/persistence" version="1.0">
<persistence-unit name="myUnit" transaction-type="RESOURCE_LOCAL">
<mapping-file>META-INF/orm.xml</mapping-file>
<exclude-unlisted-classes/>
</persistence-unit>
</persistence>
<exclude-unlisted-classes/> 快捷方式表示不应该对带注释的实体类进行扫描。显式的’true’值 ( <exclude-unlisted-classes>true</exclude-unlisted-classes/> )也表示没有扫描。<exclude-unlisted-classes>false</exclude-unlisted-classes/> 确实会触发扫描。但是,如果你希望发生实体类扫描,我们建议省略 exclude-unlisted-classes 元素。 |
---|
JPA 使用LocalContainerEntityManagerFactoryBean
是最强大的设置选项,允许在应用程序中进行灵活的本地配置。它支持到现有 JDBCDataSource
的链接,支持本地和全局事务,以此类推。然而,它也对运行时环境提出了要求,例如,如果持久性提供程序要求进行字节码转换,则需要一个具有编织能力的类装入器。
此选项可能与 Java EE 服务器的内置 JPA 功能相冲突。在完整的 Java EE 环境中,考虑从 JNDI 获得EntityManagerFactory
。或者,在你的LocalContainerEntityManagerFactoryBean
定义(例如,meta-inf/my-persistence.xml)上指定一个自定义的persistenceXmlLocation
,并在应用程序 jar 文件中仅包含带有该名称的描述符。因为 Java EE 服务器只查找默认的META-INF/persistence.xml
文件,所以它忽略了这些自定义的持久性单元,从而避免了与 Spring 驱动的 JPA 预先设置的冲突。(例如,这适用于树脂 3.1)
什么时候需要进行负载-时间编织?
并非所有 JPA 提供者都需要 JVM 代理。 Hibernate 是一个不这样做的例子。如果你的提供者不需要代理,或者你有其他的选择,例如在构建时通过自定义编译器或 Ant 任务应用增强,那么你不应该使用加载时 Weaver。
LoadTimeWeaver
接口是 Spring 提供的类,它允许以特定的方式插入 JPA ClassTransformer
实例,这取决于环境是 Web 容器还是应用程序服务器。通过agent (opens new window)来挂钩ClassTransformers
通常是无效的。代理针对整个虚拟机工作,并检查加载的每个类,这在生产服务器环境中通常是不希望的。
Spring 为各种环境提供了许多LoadTimeWeaver
实现,使得ClassTransformer
实例仅适用于每个类装入器,而不适用于每个 VM。
有关LoadTimeWeaver
实现及其设置的更多了解,请参见 AOP 章中的Spring configuration,这些实现及其设置可以是通用的,也可以是针对各种平台(例如 Tomcat、JBoss 和 WebSphere)定制的。
如Spring configuration中所述,你可以通过使用@EnableLoadTimeWeaving
注释或context:load-time-weaver
XML 元素来配置上下文范围的LoadTimeWeaver
。所有 JPA LocalContainerEntityManagerFactoryBean
实例都会自动拾取这样的全局 weaver。下面的示例展示了设置加载时 Weaver、交付平台的自动检测(例如 Tomcat 的可编织的类装入器或 Spring 的 JVM 代理)以及将 Weaver 自动传播到所有可感知 Weaver 的 bean 的首选方法:
<context:load-time-weaver/>
<bean id="emf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
...
</bean>
但是,如果需要,你可以通过loadTimeWeaver
属性手动指定一个专用的 Weaver,如下例所示:
<bean id="emf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="loadTimeWeaver">
<bean class="org.springframework.instrument.classloading.ReflectiveLoadTimeWeaver"/>
</property>
</bean>
无论 LTW 是如何配置的,通过使用该技术, JPA 依赖于检测的应用程序可以在目标平台(例如 Tomcat)中运行,而不需要代理。当托管应用程序依赖于不同的实现方式时,这一点尤其重要,因为 JPA 变压器仅在类装入器级别上应用,并且因此彼此隔离。
# 处理多个持久性单元
对于依赖于多个持久性单元位置的应用程序(例如,存储在 Classpath 中的各种 JAR 中), Spring 提供了PersistenceUnitManager
作为中心存储库并避免持久性单元发现过程,这可能是昂贵的。默认实现允许指定多个位置。这些位置会被解析,然后通过持久性单元名称检索。(默认情况下,在 Classpath 中搜索META-INF/persistence.xml
文件。)以下示例配置了多个位置:
<bean id="pum" class="org.springframework.orm.jpa.persistenceunit.DefaultPersistenceUnitManager">
<property name="persistenceXmlLocations">
<list>
<value>org/springframework/orm/jpa/domain/persistence-multi.xml</value>
<value>classpath:/my/package/**/custom-persistence.xml</value>
<value>classpath*:META-INF/persistence.xml</value>
</list>
</property>
<property name="dataSources">
<map>
<entry key="localDataSource" value-ref="local-db"/>
<entry key="remoteDataSource" value-ref="remote-db"/>
</map>
</property>
<!-- if no datasource is specified, use this one -->
<property name="defaultDataSource" ref="remoteDataSource"/>
</bean>
<bean id="emf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="persistenceUnitManager" ref="pum"/>
<property name="persistenceUnitName" value="myCustomUnit"/>
</bean>
默认实现允许对PersistenceUnitInfo
实例(在它们被馈送到 JPA 提供程序之前)进行定制,可以是声明式的(通过其属性,这会影响所有托管单元),也可以是程序化的(通过PersistenceUnitPostProcessor
,这允许持久性单元选择)。如果指定了 noPersistenceUnitManager
,则由LocalContainerEntityManagerFactoryBean
在内部创建和使用一个。
# 背景引导
LocalContainerEntityManagerFactoryBean
支持通过bootstrapExecutor
属性进行后台引导,如下例所示:
<bean id="emf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="bootstrapExecutor">
<bean class="org.springframework.core.task.SimpleAsyncTaskExecutor"/>
</property>
</bean>
JPA 提供程序的实际引导将交给指定的执行器,然后并行地运行到应用程序引导线程。公开的EntityManagerFactory
代理可以被注入到其他应用程序组件中,甚至能够响应EntityManagerFactoryInfo
配置检查。然而,一旦实际的 JPA 提供程序被其他组件访问(例如,调用createEntityManager
),这些调用就会阻塞,直到后台引导完成为止。特别是,当你使用 Spring 数据 JPA 时,请确保也为其存储库设置延迟引导。
# 5.4.2.基于 JPA 实现 DAO:EntityManagerFactory
和EntityManager
虽然EntityManagerFactory 实例是线程安全的,但EntityManager 实例不是。注入的 JPA EntityManager 的行为类似于从应用服务器的 JNDI 环境中获取的 EntityManager ,如 JPA 规范所定义的。它委托对当前事务 EntityManager 的所有调用(如果有的话)。否则,它将返回到每个操作新创建的 EntityManager ,实际上使其使用线程安全。 |
---|
通过使用注入的EntityManagerFactory
或EntityManager
,可以在没有任何 Spring 依赖关系的情况下针对普通 JPA 编写代码。 Spring 如果启用了PersistenceAnnotationBeanPostProcessor
和@PersistenceContext
注释,则可以在字段和方法级别上同时理解@PersistenceUnit
注释。下面的示例显示了使用@PersistenceUnit
注释的普通 JPA DAO 实现:
Java
public class ProductDaoImpl implements ProductDao {
private EntityManagerFactory emf;
@PersistenceUnit
public void setEntityManagerFactory(EntityManagerFactory emf) {
this.emf = emf;
}
public Collection loadProductsByCategory(String category) {
EntityManager em = this.emf.createEntityManager();
try {
Query query = em.createQuery("from Product as p where p.category = ?1");
query.setParameter(1, category);
return query.getResultList();
}
finally {
if (em != null) {
em.close();
}
}
}
}
Kotlin
class ProductDaoImpl : ProductDao {
private lateinit var emf: EntityManagerFactory
@PersistenceUnit
fun setEntityManagerFactory(emf: EntityManagerFactory) {
this.emf = emf
}
fun loadProductsByCategory(category: String): Collection<*> {
val em = this.emf.createEntityManager()
val query = em.createQuery("from Product as p where p.category = ?1");
query.setParameter(1, category);
return query.resultList;
}
}
前面的 DAO 对 Spring 没有依赖性,并且仍然很好地适合 Spring 应用程序上下文。此外,DAO 利用注释的优势要求注入缺省的EntityManagerFactory
,如下例 Bean 的定义所示:
<beans>
<!-- bean post-processor for JPA annotations -->
<bean class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor"/>
<bean id="myProductDao" class="product.ProductDaoImpl"/>
</beans>
作为显式定义PersistenceAnnotationBeanPostProcessor
的替代方法,可以考虑在应用程序上下文配置中使用 Spring context:annotation-config
XML 元素。这样做会自动注册用于基于注释的配置的所有 Spring 标准后处理器,包括CommonAnnotationBeanPostProcessor
等。
考虑以下示例:
<beans>
<!-- post-processors for all standard config annotations -->
<context:annotation-config/>
<bean id="myProductDao" class="product.ProductDaoImpl"/>
</beans>
这种 DAO 的主要问题是,它总是通过工厂创建一个新的EntityManager
。可以通过请求注入事务EntityManager
(也称为“共享 EntityManager”,因为它是实际事务 EntityManager 的共享、线程安全代理)而不是工厂来避免这种情况。下面的示例展示了如何做到这一点:
Java
public class ProductDaoImpl implements ProductDao {
@PersistenceContext
private EntityManager em;
public Collection loadProductsByCategory(String category) {
Query query = em.createQuery("from Product as p where p.category = :category");
query.setParameter("category", category);
return query.getResultList();
}
}
Kotlin
class ProductDaoImpl : ProductDao {
@PersistenceContext
private lateinit var em: EntityManager
fun loadProductsByCategory(category: String): Collection<*> {
val query = em.createQuery("from Product as p where p.category = :category")
query.setParameter("category", category)
return query.resultList
}
}
@PersistenceContext
注释具有一个名为type
的可选属性,该属性缺省为PersistenceContextType.TRANSACTION
。你可以使用此默认值来接收共享的EntityManager
代理。另一种选择,PersistenceContextType.EXTENDED
,则是完全不同的事情。这导致所谓的扩展EntityManager
,它不是线程安全的,因此,不能在并发访问的组件中使用,例如 Spring 管理的单例 Bean。扩展的EntityManager
实例仅应在有状态组件中使用,例如,这些组件驻留在会话中,而EntityManager
的生命周期不与当前事务绑定,而是完全由应用程序决定。
方法和场级注入
你可以在类中的字段或方法上应用指示依赖注入的注释(例如@PersistenceUnit
和@PersistenceContext
),因此产生了表达式“方法级注入”和“字段级注入”。字段级别的注释简洁且易于使用,而方法级别的注释允许对注入的依赖项进行进一步处理。在这两种情况下,成员可见性(公共的、受保护的或私有的)并不重要。
那么类级别的注释呢?
在 Java EE 平台上,它们用于依赖性声明,而不是用于资源注入。
注入的EntityManager
是 Spring-管理的(了解正在进行的事务)。尽管新的 DAO 实现使用了方法级注入EntityManager
而不是EntityManagerFactory
,但由于注释的使用,不需要对应用程序上下文 XML 进行更改。
这种 DAO 风格的主要优点是它仅依赖于 Java 持久性 API。不需要任何 Spring 类的进口。此外,如 JPA 注释所理解的,注入由 Spring 容器自动应用。从非侵犯性的角度来看,这是很有吸引力的,并且对于 JPA 开发人员来说可能会感觉更自然。
# 5.4.3. Spring-驱动的 JPA 交易
我们强烈建议你阅读声明式事务管理,如果你还没有 ,以获得 Spring 的声明性事务支持的更详细的覆盖。 |
---|
JPA 的推荐策略是通过 JPA 的本机事务支持进行本地事务。 Spring 的JpaTransactionManager
针对任何常规的 JDBC 连接池(不需要 XA),提供了从本地 JDBC 事务中已知的许多功能(例如特定于事务的隔离级别和资源级别的只读优化)。
Spring JPA 还允许配置的JpaTransactionManager
向访问相同DataSource
的 JDBC 访问代码公开 JPA 事务,前提是已注册的JpaDialect
支持对底层 JDBCConnection
的检索。 Spring 为 EclipseLink 和 Hibernate JPA 实现方式提供了方言。有关下一节机制的详细信息,请参见下一节。
作为一种直接的替代方案, Spring 的本机HibernateTransactionManager 能够与 JPA 访问代码进行交互,适应多个 Hibernate 细节并提供JDBC 交互。这与 LocalSessionFactoryBean 设置结合在一起特别有意义。详见Native Hibernate Setup for JPA Interaction。 |
---|
# 5.4.4.理解JpaDialect
和JpaVendorAdapter
作为一个高级特性,JpaTransactionManager
和AbstractEntityManagerFactoryBean
的子类允许将自定义JpaDialect
传递到jpaDialect
Bean 属性中。 Spring 支持的JpaDialect
实现通常以特定于供应商的方式实现以下高级特性:
应用特定的事务语义(例如自定义隔离级别或事务超时)
检索事务性 JDBC
Connection
(用于暴露于基于 JDBC 的 DAO)将
PersistenceExceptions
高级翻译为 SpringDataAccessExceptions
这对于特殊的事务语义和异常的高级转换特别有价值。默认实现(DefaultJpaDialect
)不提供任何特殊功能,如果需要前面列出的功能,则必须指定适当的方言。
作为主要针对 Spring 的全功能LocalContainerEntityManagerFactoryBean 设置的更广泛的提供者适配工具,JpaVendorAdapter 将的 功能与其他提供者特定的默认值结合在一起。指定 HibernateJpaVendorAdapter 或EclipseLinkJpaVendorAdapter 是分别为 Hibernate 或 EclipseLink、自动配置 EntityManagerFactory 设置的最方便的方式。请注意,这些提供程序适配器主要设计用于 Spring 驱动的事务管理(即用于 JpaTransactionManager )。 |
---|
参见[JpaDialect
](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/orm/ JPA/jpavendoric.html)和[JpaVendorAdapter
](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/ork/orm/[pavendorkframework]/jpavendoradoc),以了解其操作的更多细节,以及如何在[306
# 5.4.5.使用 JTA 事务管理设置 JPA
作为JpaTransactionManager
的替代方案, Spring 还允许通过 JTA 进行多资源事务协调,可以是在 Java EE 环境中,也可以是使用独立的事务协调器,例如 Atomikos。除了选择 Spring 的JtaTransactionManager
而不是JpaTransactionManager
之外,还需要采取一些进一步的步骤:
底层的 JDBC 连接池需要具有 XA 功能,并与事务协调器集成在一起。在 Java EE 环境中,这通常很简单,通过 JNDI 公开不同类型的
DataSource
。有关详细信息,请参阅你的应用程序服务器文档。类似地,独立的事务协调器通常带有特殊的 XA 集成DataSource
变体。再一次,检查它的文档。需要为 JTA 配置 JPA
EntityManagerFactory
设置。这是特定于提供者的,通常通过在LocalContainerEntityManagerFactoryBean
上指定为jpaProperties
的特殊属性。在 Hibernate 的情况下,这些属性甚至是版本特定的。有关详细信息,请参阅你的 Hibernate 文档。Spring 的
HibernateJpaVendorAdapter
强制执行某些面向 Spring 的默认值,例如连接释放模式,on-close
,该模式与 Hibernate 自己在 Hibernate 5.0 中的默认值相匹配,但在 Hibernate 5.1+ 中不再匹配。对于 JTA 设置,请确保将你的持久性单元事务类型声明为“JTA”。或者,将 Hibernate 5.2 的hibernate.connection.handling_mode
属性设置为DELAYED_ACQUISITION_AND_RELEASE_AFTER_STATEMENT
,以恢复 Hibernate 自己的默认值。相关注解见Spurious Application Server Warnings with Hibernate。或者,考虑从应用程序服务器本身获得
EntityManagerFactory
(即通过 JNDI 查找,而不是本地声明的LocalContainerEntityManagerFactoryBean
)。服务器提供的EntityManagerFactory
在你的服务器配置中可能需要特殊的定义(使得部署不那么可移植),但它是为服务器的 JTA 环境设置的。
# 5.4.6.用于 JPA 交互的本机 Hibernate 设置和本机 Hibernate 事务
结合LocalSessionFactoryBean
的本机HibernateTransactionManager
设置允许与@PersistenceContext
和其他 JPA 访问代码进行交互。一个 Hibernate SessionFactory
本地实现了 JPA 的EntityManagerFactory
接口,而一个 Hibernate Session
句柄本地实现了一个 JPA EntityManager
。 Spring 的 JPA 支持设施自动检测本机 Hibernate 会话。
因此,这样的本机 Hibernate 设置可以在许多场景中作为标准 JPA LocalContainerEntityManagerFactoryBean
和JpaTransactionManager
组合的替代,允许在相同的本地事务中与SessionFactory.getCurrentSession()
(以及HibernateTemplate
)旁边的@PersistenceContext EntityManager
进行交互。这样的设置还提供了更强的 Hibernate 集成和更多的配置灵活性,因为它不受 JPA 引导程序契约的约束。
在这样的场景中,你不需要HibernateJpaVendorAdapter
配置,因为 Spring 的本机 Hibernate 设置提供了更多的功能(例如,自定义 Hibernate Integrator 设置, Hibernate 5.3 Bean 容器集成,以及针对只读事务的更强优化)。最后但并非最不重要的是,你还可以通过LocalSessionFactoryBuilder
表示本机 Hibernate 设置,与@Bean
样式配置无缝集成(不涉及FactoryBean
)。
LocalSessionFactoryBean 和LocalSessionFactoryBuilder 支持后台引导,就像 JPA LocalContainerEntityManagerFactoryBean 所做的那样。参见背景引导介绍。 在 LocalSessionFactoryBean 上,这可以通过bootstrapExecutor 属性获得。在程序化的LocalSessionFactoryBuilder 上,重载的buildSessionFactory 方法接受一个 BootStrap 执行器参数。 |
---|
# 6. 使用对象-XML 映射器编组 XML
# 6.1.导言
本章介绍 Spring 的对象-XML 映射支持。对象-XML 映射(Object-XML mapping,简称 O-X 映射)是将 XML 文档转换为对象和从对象转换的行为。这种转换过程也称为 XML 编组,或 XML 序列化。本章交替使用这些术语。
在 O-X 映射领域中,编组器负责将对象(图)序列化为 XML。以类似的方式,反编组器将 XML 反序列化为一个对象图。这个 XML 可以采取 DOM 文档、输入或输出流或 SAX 处理程序的形式。
针对你的 O/X 映射需求使用 Spring 的一些好处是:
# 6.1.1.易于配置
Spring 的 Bean 工厂使得很容易配置编组器,而不需要构建 JAXB 上下文、JiBX 绑定工厂等。你可以像在你的应用程序上下文中的任何其他 Bean 那样配置编组器。此外,基于 XML 名称空间的配置可用于许多编组器,这使得配置更加简单。
# 6.1.2.一致的接口
Spring 的 O-X 映射通过两个全球接口进行操作:[Marshaller
](https://DOCS. Spring.io/ Spring-Framework/DOCS/5.3.16/javadoc-api/org/SpringFramework/OXM/Marshaller.html)和[Unmarshaller
(https://DOCS. Spring.io/ Spring/ Spring-framework/DOCS/5.3.16/javadoc-springframework/OXM/org/unmarshaller.html)。这些抽象允许你相对轻松地切换 O-X 映射框架,只需对进行编组的类进行很少或根本不需要更改。这种方法还有一个额外的好处,那就是可以使用混合匹配的方法(例如,使用 JAXB 和 XStream 执行一些编组)以非侵入性的方式进行 XML 编组,从而使你能够利用每种技术的优势。
# 6.1.3.一致的异常层次结构
Spring 提供了从底层 O-X 映射工具的异常到其自身的异常层次结构的转换,并将XmlMappingException
作为根异常。这些运行时异常会对原始异常进行包装,这样就不会丢失任何信息。
# 6.2.Marshaller
和Unmarshaller
正如导言中所述,编组器将对象序列化为 XML,而解组器将 XML 流反序列化为对象。本节描述了用于此目的的两个 Spring 接口。
# 6.2.1.理解Marshaller
Spring 抽象org.springframework.oxm.Marshaller
接口后面的所有编组操作,其主要方法如下:
public interface Marshaller {
/**
* Marshal the object graph with the given root into the provided Result.
*/
void marshal(Object graph, Result result) throws XmlMappingException, IOException;
}
Marshaller
接口有一个主要方法,它将给定对象封送到给定的javax.xml.transform.Result
。其结果是一个标记接口,它基本上表示一个 XML 输出抽象。具体的实现封装了各种 XML 表示,如下表所示:
Result implementation | 包装 XML 表示 |
---|---|
DOMResult | org.w3c.dom.Node |
SAXResult | org.xml.sax.ContentHandler |
StreamResult | java.io.File ,java.io.OutputStream ,或java.io.Writer |
尽管marshal() 方法接受一个普通对象作为其第一个参数,但大多数Marshaller 实现不能处理任意对象。相反,对象类必须映射到映射文件中,用注释标记,用 编组器注册,或者有一个公共的基类。请参阅本章 中后面的部分,以确定你的 O-X 技术如何管理这一点。 |
---|
# 6.2.2.理解Unmarshaller
与Marshaller
类似,我们有org.springframework.oxm.Unmarshaller
接口,下面的清单显示了该接口:
public interface Unmarshaller {
/**
* Unmarshal the given provided Source into an object graph.
*/
Object unmarshal(Source source) throws XmlMappingException, IOException;
}
这个接口也有一个方法,它从给定的javax.xml.transform.Source
(一个 XML 输入抽象)读取并返回读取的对象。与Result
一样,Source
是一个标记接口,具有三个具体的实现。每一种都包装了不同的 XML 表示形式,如下表所示:
Source implementation | 包装 XML 表示 |
---|---|
DOMSource | org.w3c.dom.Node |
SAXSource | org.xml.sax.InputSource ,和org.xml.sax.XMLReader |
StreamSource | java.io.File ,java.io.InputStream ,或java.io.Reader |
尽管有两个独立的编组接口(Marshaller
和Unmarshaller
), Spring-WS 中的所有实现都在一个类中实现这两个接口。这意味着你可以连接一个 Marshaller 类,并在applicationContext.xml
中同时将其称为 Marshaller 和 unshaller。
# 6.2.3.理解XmlMappingException
Spring 将来自底层 O-X 映射工具的异常转换为其自身的异常层次结构,并将XmlMappingException
作为根异常。这些运行时异常将包装原始异常,这样就不会丢失任何信息。
此外,MarshallingFailureException
和UnmarshallingFailureException
提供了编组和解组操作之间的区别,即使底层的 O-X 映射工具不这样做。
O-X 映射异常层次结构如下图所示:
# 6.3.使用Marshaller
和Unmarshaller
你可以在各种各样的情况下使用 Spring 的 OXM。在下面的示例中,我们使用它将 Spring 管理的应用程序的设置编组为 XML 文件。在下面的示例中,我们使用一个简单的 JavaBean 来表示设置:
Java
public class Settings {
private boolean fooEnabled;
public boolean isFooEnabled() {
return fooEnabled;
}
public void setFooEnabled(boolean fooEnabled) {
this.fooEnabled = fooEnabled;
}
}
Kotlin
class Settings {
var isFooEnabled: Boolean = false
}
应用程序类使用此 Bean 来存储其设置。除了一个主方法之外,这个类还有两个方法:saveSettings()
将设置保存到一个名为settings.xml
的文件中,loadSettings()
再次加载这些设置。下面的main()
方法构造了一个 Spring 应用程序上下文,并调用这两个方法:
Java
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.oxm.Marshaller;
import org.springframework.oxm.Unmarshaller;
public class Application {
private static final String FILE_NAME = "settings.xml";
private Settings settings = new Settings();
private Marshaller marshaller;
private Unmarshaller unmarshaller;
public void setMarshaller(Marshaller marshaller) {
this.marshaller = marshaller;
}
public void setUnmarshaller(Unmarshaller unmarshaller) {
this.unmarshaller = unmarshaller;
}
public void saveSettings() throws IOException {
try (FileOutputStream os = new FileOutputStream(FILE_NAME)) {
this.marshaller.marshal(settings, new StreamResult(os));
}
}
public void loadSettings() throws IOException {
try (FileInputStream is = new FileInputStream(FILE_NAME)) {
this.settings = (Settings) this.unmarshaller.unmarshal(new StreamSource(is));
}
}
public static void main(String[] args) throws IOException {
ApplicationContext appContext =
new ClassPathXmlApplicationContext("applicationContext.xml");
Application application = (Application) appContext.getBean("application");
application.saveSettings();
application.loadSettings();
}
}
Kotlin
class Application {
lateinit var marshaller: Marshaller
lateinit var unmarshaller: Unmarshaller
fun saveSettings() {
FileOutputStream(FILE_NAME).use { outputStream -> marshaller.marshal(settings, StreamResult(outputStream)) }
}
fun loadSettings() {
FileInputStream(FILE_NAME).use { inputStream -> settings = unmarshaller.unmarshal(StreamSource(inputStream)) as Settings }
}
}
private const val FILE_NAME = "settings.xml"
fun main(args: Array<String>) {
val appContext = ClassPathXmlApplicationContext("applicationContext.xml")
val application = appContext.getBean("application") as Application
application.saveSettings()
application.loadSettings()
}
Application
需要同时设置marshaller
和unmarshaller
属性。我们可以通过使用以下applicationContext.xml
来做到这一点:
<beans>
<bean id="application" class="Application">
<property name="marshaller" ref="xstreamMarshaller" />
<property name="unmarshaller" ref="xstreamMarshaller" />
</bean>
<bean id="xstreamMarshaller" class="org.springframework.oxm.xstream.XStreamMarshaller"/>
</beans>
这个应用程序上下文使用 XStream,但我们可以使用本章后面描述的任何其他 Marshaller 实例。请注意,默认情况下,XStream 不需要任何进一步的配置,因此 Bean 的定义相当简单。还要注意,XStreamMarshaller
同时实现了Marshaller
和Unmarshaller
,因此我们可以在xstreamMarshaller
和unmarshaller
这两个应用程序的属性中引用xstreamMarshaller
Bean。
此示例应用程序生成以下settings.xml
文件:
<?xml version="1.0" encoding="UTF-8"?>
<settings foo-enabled="false"/>
# 6.4.XML 配置命名空间
通过使用 OXM 名称空间中的标记,可以更简洁地配置编组器。要使这些标记可用,你必须首先在 XML 配置文件的前导码中引用适当的模式。下面的示例展示了如何做到这一点:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:oxm="http://www.springframework.org/schema/oxm" (1)
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/oxm https://www.springframework.org/schema/oxm/spring-oxm.xsd"> (2)
1 | 引用oxm 模式。 |
---|---|
2 | 指定oxm 模式位置。 |
该模式使以下元素可用:
[
jaxb2-marshaller
](#OXM-JAXB2-XSD)[
jibx-marshaller
](#OXM-JiBX-XSD)
每个标记在其各自的编组器部分中进行了解释。但是,作为示例,JAXB2 封送编组器的配置可能类似于以下内容:
<oxm:jaxb2-marshaller id="marshaller" contextPath="org.springframework.ws.samples.airline.schema"/>
# 6.5.JAXB
JAXB 绑定编译器将 W3C XML 模式转换为一个或多个 Java 类、jaxb.properties
文件,可能还有一些资源文件。JAXB 还提供了一种从带注释的 Java 类生成模式的方法。
Spring 支持 JAXB2.0API 作为 XML 编组策略,遵循[Marshaller
和Unmarshaller
中描述的接口[Marshaller
和Unmarshaller
](#OXM-marshaller-unmarshaller)。相应的集成类驻留在org.springframework.oxm.jaxb
包中。
# 6.5.1.使用Jaxb2Marshaller
Jaxb2Marshaller
类实现了 Spring 的Marshaller
和Unmarshaller
接口。它需要一个上下文路径来操作。你可以通过设置contextPath
属性来设置上下文路径。上下文路径是包含模式派生类的冒号分隔的 Java 包名称的列表。它还提供了classesToBeBound
属性,它允许你设置编组器支持的类数组。模式验证是通过向 Bean 指定一个或多个模式资源来执行的,如下例所示:
<beans>
<bean id="jaxb2Marshaller" class="org.springframework.oxm.jaxb.Jaxb2Marshaller">
<property name="classesToBeBound">
<list>
<value>org.springframework.oxm.jaxb.Flight</value>
<value>org.springframework.oxm.jaxb.Flights</value>
</list>
</property>
<property name="schema" value="classpath:org/springframework/oxm/schema.xsd"/>
</bean>
...
</beans>
# XML 配置命名空间
jaxb2-marshaller
元素配置了org.springframework.oxm.jaxb.Jaxb2Marshaller
,如下例所示:
<oxm:jaxb2-marshaller id="marshaller" contextPath="org.springframework.ws.samples.airline.schema"/>
或者,你可以通过使用class-to-be-bound
子元素提供要绑定到编组器的类的列表:
<oxm:jaxb2-marshaller id="marshaller">
<oxm:class-to-be-bound name="org.springframework.ws.samples.airline.schema.Airport"/>
<oxm:class-to-be-bound name="org.springframework.ws.samples.airline.schema.Flight"/>
...
</oxm:jaxb2-marshaller>
下表描述了可用的属性:
Attribute | 说明 | Required |
---|---|---|
id | 编组员的身份 | No |
contextPath | JAXB 上下文路径 | No |
# 6.6.JiBX
JiBX 框架提供了类似于 Hibernate 为 ORM 提供的解决方案:绑定定义定义了如何将 Java 对象转换为 XML 或从 XML 转换的规则。在准备好绑定和编译类之后,JiBX 绑定编译器将增强类文件,并添加代码来处理将类的实例从 XML 转换为 XML。
有关 JiBX 的更多信息,请参见JiBX 网站 (opens new window)。 Spring 集成类驻留在org.springframework.oxm.jibx
包中。
# 6.6.1.使用JibxMarshaller
JibxMarshaller
类实现了Marshaller
和Unmarshaller
接口。要进行操作,它需要封送类的名称,你可以使用targetClass
属性对其进行设置。还可以通过设置bindingName
属性来设置绑定名称。在下面的示例中,我们绑定Flights
类:
<beans>
<bean id="jibxFlightsMarshaller" class="org.springframework.oxm.jibx.JibxMarshaller">
<property name="targetClass">org.springframework.oxm.jibx.Flights</property>
</bean>
...
</beans>
为单个类配置了JibxMarshaller
。如果要封送多个类,则必须配置具有不同的JibxMarshaller
属性值的多个targetClass
实例。
# XML 配置命名空间
jibx-marshaller
标记配置了org.springframework.oxm.jibx.JibxMarshaller
,如下例所示:
<oxm:jibx-marshaller id="marshaller" target-class="org.springframework.ws.samples.airline.schema.Flight"/>
下表描述了可用的属性:
Attribute | 说明 | Required |
---|---|---|
id | 编组员的身份 | No |
target-class | 这个编组器的目标类 | Yes |
bindingName | 这个编组器使用的绑定名称 | No |
# 6.7.XStream
XStream 是一个简单的库,用于将对象序列化为 XML 并返回。它不需要任何映射,并生成干净的 XML。
有关 XStream 的更多信息,请参见XStream 网站 (opens new window)。 Spring 集成类驻留在org.springframework.oxm.xstream
包中。
# 6.7.1.使用XStreamMarshaller
XStreamMarshaller
不需要任何配置,可以直接在应用程序上下文中进行配置。为了进一步定制 XML,你可以设置一个别名映射,它由映射到类的字符串别名组成,如下例所示:
<beans>
<bean id="xstreamMarshaller" class="org.springframework.oxm.xstream.XStreamMarshaller">
<property name="aliases">
<props>
<prop key="Flight">org.springframework.oxm.xstream.Flight</prop>
</props>
</property>
</bean>
...
</beans>
默认情况下,XStream 允许对任意类进行解编,这可能导致 不安全的 Java 序列化效果。因此,我们不建议使用 XStreamMarshaller 从外部源(即 Web)解编 XML,因为这可能会导致安全漏洞。 如果你选择使用 XStreamMarshaller 从外部源解压缩 XML,在 supportedClasses 上设置supportedClasses 属性,如下例所示:<br/><bean id="xstreamMarshaller" class="org.springframework.oxm.xstream.XStreamMarshaller"><br/> <property name="supportedClasses" value="org.springframework.oxm.xstream.Flight"/><br/> ...<br/></bean><br/> 这样做可以确保只有已注册的类才有资格进行解组。 另外,你可以注册[custom converters](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/oxm/xstream/XStreamMarshaller.html#setConverters(com.thoughtworks.xstream.converters.ConverterMatcher…)),以确保只有你支持的类可以进行解组。除了显式支持应该支持的域类的 转换器之外,你可能希望在列表中添加一个 CatchAllConverter 作为最后一个转换器。作为结果,不会调用具有较低优先级和可能的安全性 漏洞的默认 XStream 转换器。 |
---|
请注意,XStream 是一个 XML 序列化库,而不是一个数据绑定库。 因此,它对名称空间的支持有限。因此,它非常不适合在 Web 服务中使用 。 |
---|
# 7. 附录
# 7.1.XML 模式
附录的这一部分列出了用于数据访问的 XML 模式,包括以下内容:
[the
tx
schema](#XSD-schemas-tx)[the
jdbc
schema](#XSD-schemas-jdbc)
# 7.1.1.tx
模式
在 Spring 的事务全面支持中,tx
标记处理配置所有这些 bean。这些标记在标题为事务管理的章节中进行了介绍。
我们强烈建议你查看带有 Spring 分布的 'spring-tx.xsd' 文件。该文件包含 Spring 事务配置的 XML 模式,并涵盖 tx 命名空间中的所有不同元素,包括属性默认值和类似信息。这个文件是内联记录的,因此, 这里不重复信息是为了遵守 dry(不要 重复自己)原则。 |
---|
为了完整起见,要使用tx
模式中的元素,你需要在 Spring XML 配置文件的顶部具有以下前导码。以下代码片段中的文本引用了正确的模式,因此tx
名称空间中的标记对你是可用的:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx" (1)
xsi:schemaLocation="
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx https://www.springframework.org/schema/tx/spring-tx.xsd (2)
http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- bean definitions here -->
</beans>
1 | 声明tx 名称空间的用法。 |
---|---|
2 | 指定位置(与其他架构位置一起)。 |
通常,当你使用tx 命名空间中的元素时,你还使用来自aop 命名空间中的元素(因为 Spring 中的声明性事务支持是通过使用 AOP 实现的 )。前面的 XML 片段包含引用 aop 模式所需的相关行,以便aop 名称空间中的元素对你可用。 |
---|
# 7.1.2.jdbc
模式
jdbc
元素允许你快速配置嵌入式数据库或初始化现有数据源。这些元素分别记录在嵌入式数据库支持和初始化数据源中。
要使用jdbc
模式中的元素,你需要在 Spring XML 配置文件的顶部具有以下前导码。以下代码片段中的文本引用了正确的模式,因此jdbc
名称空间中的元素对你是可用的:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jdbc="http://www.springframework.org/schema/jdbc" (1)
xsi:schemaLocation="
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/jdbc https://www.springframework.org/schema/jdbc/spring-jdbc.xsd"> (2)
<!-- bean definitions here -->
</beans>
← 测试 Servlet 堆栈上的 Web →