参考文档的这一部分与数据访问以及数据访问层和业务或服务层之间的交互有关。
本文详细介绍了对Spring的全面事务管理支持,然后全面介绍了各种数据访问框架以及与Spring框架集成的技术。
1. Transaction Management
全面的事务支持是使用Spring框架的最有说服力的理由之一。Spring框架为事务管理提供了一致的抽象,具有以下优势:
以下各节描述了Spring框架的事务功能和技术:
-
Spring框架的事务支持模型的优点描述了为什么您要使用Spring框架的事务抽象而不是EJB容器管理的事务(CMT),或者选择通过专有的API(如Hibernate)来驱动本地事务。
-
理解Spring框架事务抽象概述了核心类,并描述了如何配置和从各种来源获取
DataSource
实例。 -
将资源与事务同步描述了应用程序代码如何确保正确创建、重用和清理资源。
-
声明性事务管理描述了对声明性事务管理的支持。
-
程序性事务管理涵盖了对程序性(即显式编码)事务管理的支持。
-
事务绑定事件描述了如何在事务中使用应用程序事件。
1.1. Advantages of the Spring Framework’s Transaction Support Model
传统上,EE应用程序开发人员有两种事务管理选择:全局事务或本地事务,这两种事务都有很大的局限性。在接下来的两节中,我们将回顾全局和本地事务管理,然后讨论Spring框架的事务管理支持如何解决全局和本地事务模型的限制。
1.1.1. Global Transactions
全局事务允许您使用多个事务性资源,通常是关系数据库和消息队列。应用服务器通过JTA管理全局事务,这是一个繁琐的API(部分原因是它的异常模型)。此外,JTAUserTransaction
通常需要源自JNDI,这意味着您还需要使用JNDI才能使用JTA。全局事务的使用限制了应用程序代码的任何潜在重用,因为JTA通常只在应用程序服务器环境中可用。
以前,使用全局事务的首选方式是通过EJB CMT(容器管理事务)。CMT是声明性事务管理的一种形式(区别于程序性事务管理)。Ejb CMT消除了与事务相关的JNDI查找的需要,尽管使用EJB本身就需要使用JNDI。它消除了大部分(但不是全部)编写Java代码来控制事务的需要。CMT的主要缺点是与JTA和应用服务器环境捆绑在一起。此外,只有在选择在EJB中(或至少在事务性的EJB外观之后)实现业务逻辑时,它才可用。总的来说,EJB的缺点是如此之大,以至于这不是一个有吸引力的命题,特别是在面对声明性事务管理的引人注目的替代方案的情况下。
1.1.2. Local Transactions
本地事务是特定于资源的,例如与JDBC连接相关联的事务。本地事务可能更易于使用,但有一个明显的缺点:它们不能跨多个事务资源工作。例如,使用JDBC连接管理事务的代码不能在全局JTA事务内运行。由于应用程序服务器不参与事务管理,因此它无法帮助确保跨多个资源的正确性。(值得注意的是,大多数应用程序使用单个事务资源。)另一个缺点是本地事务对编程模型具有侵入性。
1.1.3. Spring Framework’s Consistent Programming Model
Spring解决了全局事务和本地事务的缺点。它允许应用程序开发人员在任何环境中使用一致的编程模型。只需编写一次代码,它就可以从不同环境中的不同事务管理策略中受益。Spring框架同时提供声明性和程序性事务管理。大多数用户更喜欢声明式事务管理,这是我们在大多数情况下推荐的。
通过程序化事务管理,开发人员可以使用Spring框架事务抽象,它可以运行在任何底层事务基础设施上。使用首选的声明性模型,开发人员通常很少或根本不编写与事务管理相关的代码,因此不依赖于Spring框架事务API或任何其他事务API。
1.2. Understanding the Spring Framework Transaction Abstraction
Spring事务抽象的关键是事务策略的概念。事务策略由TransactionManager
定义,特别是用于命令性事务管理的org.springframework.transaction.PlatformTransactionManager
接口和用于反应性事务管理的org.springframework.transaction.ReactiveTransactionManager
接口。下面的清单显示了PlatformTransactionManager
接口的定义:
public interface PlatformTransactionManager extends TransactionManager {
TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;
void commit(TransactionStatus status) throws TransactionException;
void rollback(TransactionStatus status) throws TransactionException;
}
这主要是一个服务提供者接口(SPI),尽管您可以在您的应用程序代码中以编程方式使用它。因为PlatformTransactionManager
是一个接口,所以可以根据需要轻松地对其进行模拟或清除。它不依赖于查找策略,如JNDI。PlatformTransactionManager
实现的定义与Spring框架IOC容器中的任何其他对象(或Bean)类似。仅此好处就使Spring框架事务成为一个有价值的抽象,即使在使用JTA时也是如此。与直接使用JTA相比,测试事务性代码要容易得多。
同样,为了与Spring的理念保持一致,可以由PlatformTransactionManager
接口的任何方法抛出的TransactionException
是未检查的(即,它扩展了java.lang.RunmeException
类)。交易基础设施故障几乎总是致命的。在极少数情况下,应用程序代码实际上可以从事务失败中恢复,应用程序开发人员仍然可以选择捕获和处理TransactionException
。突出的一点是,开发人员不是被强迫这样做的。
getTransaction(..)
方法返回一个TransactionStatus
对象,具体取决于TransactionDefinition
参数。如果当前调用堆栈中存在匹配的事务,则返回的TransactionStatus
可能表示新事务,也可以表示现有事务。后一种情况的含义是,与Jakarta EE事务上下文一样,TransactionStatus
与执行线程相关联。
从Spring Framework5.2开始,Spring还为使用反应式类型或Kotlin协程的反应式应用程序提供了事务管理抽象。下面的清单显示了由org.springframework.transaction.ReactiveTransactionManager
:定义的事务策略
public interface ReactiveTransactionManager extends TransactionManager {
Mono<ReactiveTransaction> getReactiveTransaction(TransactionDefinition definition) throws TransactionException;
Mono<Void> commit(ReactiveTransaction status) throws TransactionException;
Mono<Void> rollback(ReactiveTransaction status) throws TransactionException;
}
反应式事务管理器主要是一个服务提供者接口(SPI),尽管您可以在应用程序代码中以编程方式使用它。因为Reactive TransactionManager
是一个接口,所以可以根据需要轻松地对其进行模拟或清除。
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();
}
无论您在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>
如果您在Jakarta 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
(或任何其他特定资源),因为它使用容器的全局事务管理基础结构。
The preceding definition of the dataSource bean uses the <jndi-lookup/> tag from the jee namespace. For more information see The JEE Schema. |
If you use JTA, your transaction manager definition should look the same, regardless of what data access technology you use, be it JDBC, Hibernate JPA, or any other supported technology. This is due to the fact that JTA transactions are global transactions, which can enlist any transactional resource. |
在所有的Spring事务设置中,应用程序代码不需要更改。您只需更改配置即可更改管理事务的方式,即使该更改意味着从本地事务转移到全局事务或从全局事务转移到本地事务。
1.2.1. Hibernate Transaction Setup
您还可以轻松地使用Hibernate本地事务,如以下示例所示。在这种情况下,您需要定义一个HibernateLocalSessionFactoryBean
,您的应用程序代码可以使用它来获取Hibernate会话
实例。
DataSource
Bean定义类似于前面显示的本地JDBC示例,因此在下面的示例中没有显示。
If the DataSource (used by any non-JTA transaction manager) is looked up through JNDI and managed by a Jakarta EE container, it should be non-transactional, because the Spring Framework (rather than the Jakarta EE container) manages the transactions. |
本例中的txManager
Bean属于HibernateTransactionManager
类型。与DataSourceTransactionManager
需要引用DataSource
一样,HibernateTransactionManager
也需要引用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和Jakarta EE容器管理的JTA事务,则应该使用与前面的JDBC JTA示例中相同的JtaTransactionManager
,如下面的示例所示。此外,建议Hibernate通过其事务协调器以及可能的连接释放模式配置来了解JTA:
<bean id="sessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="mappingResources">
<list>
<value>org/springframework/samples/petclinic/hibernate/petclinic.hbm.xml</value>
</list>
</property>
<property name="hibernateProperties">
<value>
hibernate.dialect=${hibernate.dialect}
hibernate.transaction.coordinator_class=jta
hibernate.connection.handling_mode=DELAYED_ACQUISITION_AND_RELEASE_AFTER_STATEMENT
</value>
</property>
</bean>
<bean id="txManager" class="org.springframework.transaction.jta.JtaTransactionManager"/>
或者,您也可以将JtaTransactionManager
传递到您的LocalSessionFactoryBean
中,以强制实施相同的默认设置:
<bean id="sessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="mappingResources">
<list>
<value>org/springframework/samples/petclinic/hibernate/petclinic.hbm.xml</value>
</list>
</property>
<property name="hibernateProperties">
<value>
hibernate.dialect=${hibernate.dialect}
</value>
</property>
<property name="jtaTransactionManager" ref="txManager"/>
</bean>
<bean id="txManager" class="org.springframework.transaction.jta.JtaTransactionManager"/>
1.3. Synchronizing Resources with Transactions
如何创建不同的事务管理器以及它们如何链接到需要与事务同步的相关资源(例如DataSourceTransactionManager
到JDBC数据源
,HibernateTransactionManager
到HibernateSessionFactory
,等等)现在应该很清楚了。本节描述应用程序代码如何(直接或间接地,通过使用持久化API,如JDBC、Hibernate或JPA)确保正确地创建、重用和清理这些资源。本节还讨论如何(可选)通过相关的TransactionManager
触发事务同步。
1.3.1. High-level Synchronization Approach
首选的方法是使用Spring最高级别的基于模板的持久化集成API,或者将本机ORMAPI与支持事务的工厂Bean或代理一起使用来管理本机资源工厂。这些事务感知解决方案在内部处理资源创建和重用、清理、资源的可选事务同步以及异常映射。因此,用户数据访问代码不必处理这些任务,但可以完全专注于非样板持久性逻辑。通常,您可以使用原生ORM API,或者通过使用JdbcTemplate
来采用模板方法进行JDBC访问。本参考文档的后续部分将详细介绍这些解决方案。
1.3.2. Low-level Synchronization Approach
DataSourceUtils
(对于JDBC)、EntityManagerFactoryUtils
(对于JPA)、SessionFactoryUtils
(对于Hibernate)等类存在于较低级别。当您希望应用程序代码直接处理本机持久性API的资源类型时,可以使用这些类来确保获得正确的由Spring框架管理的实例,(可选)同步事务,并将流程中发生的异常正确地映射到一致的API。
例如,对于jdbc,您可以使用Spring的org.springframework.jdbc.datasource.DataSourceUtils
类,而不是在DataSource
上调用getConnection()
方法的传统jdbc方法:
Connection conn = DataSourceUtils.getConnection(dataSource);
如果现有事务已有同步(链接)的连接,则返回该实例。否则,方法调用将触发新连接的创建,该连接(可选)与任何现有事务同步,并可用于在同一事务中的后续重用。正如前面提到的,任何<CannotGetJdbcConnectionException
,>SQLException都包装在一个Spring框架代码中,这是Spring框架中未检查的DataAccessException
类型的层次结构之一。这种方法为您提供了比从SQLException
轻松获得的更多的信息,并确保了跨数据库甚至跨不同持久性技术的可移植性。
这种方法在没有Spring事务管理的情况下也可以使用(事务同步是可选的),所以无论您是否使用Spring进行事务管理,都可以使用它。
当然,一旦您使用了Spring的JDBC支持、JPA支持或Hibernate支持,您通常不喜欢使用DataSourceUtils
或其他助手类,因为与直接使用相关API相比,您更喜欢使用Spring抽象。例如,如果您使用SpringJdbcTemplate
或jdbc.Object
包来简化您对JDBC的使用,那么正确的连接检索将在幕后进行,您不需要编写任何特殊的代码。
1.4. Declarative Transaction Management
Most Spring Framework users choose declarative transaction management. This option has the least impact on application code and, hence, is most consistent with the ideals of a non-invasive lightweight container. |
Spring框架的声明性事务管理是通过Spring面向方面编程(AOP)实现的。然而,由于事务方面代码随Spring框架发行版一起提供,并且可以以样板方式使用,因此通常不需要理解AOP概念就可以有效地使用该代码。
Spring框架的声明性事务管理类似于ejb CMT,因为您可以指定事务行为(或缺少它),直到各个方法级别。如有必要,您可以在事务上下文中进行setRollback Only()
调用。这两种类型的事务管理之间的区别是:
-
与绑定到JTA的EJB CMT不同,Spring框架的声明性事务管理可以在任何环境中工作。通过调整配置文件,它可以使用JDBC、JPA或Hibernate处理JTA事务或本地事务。
-
您可以将Spring框架声明性事务管理应用于任何类,而不仅仅是特殊的类,如EJB。
-
Spring框架提供了声明性回滚规则,这是一个没有与ejb等价的特性。提供了对回滚规则的编程和声明性支持。
-
Spring框架允许您使用AOP定制事务行为。例如,您可以在事务回滚的情况下插入自定义行为。您还可以添加任意建议,以及事务性建议。使用ejb CMT,您不能影响容器的事务管理,除非使用
setRollbackOnly()
。 -
与高端应用服务器不同,Spring框架不支持跨远程调用传播事务上下文。如果您需要此功能,我们建议您使用EJB。但是,在使用此类功能之前请仔细考虑,因为通常情况下,不希望事务跨越远程调用。
回滚规则的概念很重要。它们允许您指定哪些异常(和可抛出的异常)应该导致自动回滚。您可以在配置中声明性地指定它,而不是在Java代码中。因此,尽管您仍然可以在TransactionStatus
对象上调用setRollackOnly()
来回滚当前事务,但最常见的情况是您可以指定MyApplicationException
必须始终导致回滚的规则。此选项的显著优势是业务对象不依赖于事务基础设施。例如,它们通常不需要导入Spring Transaction API或其他SpringAPI。
尽管ejb容器的默认行为会在系统异常(通常是运行时异常)上自动回滚事务,但ejb CMT不会在应用程序异常(即,除java.rmi.RemoteException
以外的已检查异常)上自动回滚事务。虽然声明性事务管理的Spring默认行为遵循EJB约定(只有在未检查的异常上才自动回滚),但自定义此行为通常很有用。
1.4.1. Understanding the Spring Framework’s Declarative Transaction Implementation
仅仅告诉您用@Transaction
注释类,将@EnableTransactionManagement
添加到您的配置中,并期望您了解它们的工作原理是不够的。为了提供更深层次的理解,本节解释了Spring框架的声明性事务基础设施在事务相关问题的上下文中的内部工作原理。
关于Spring框架的声明性事务支持,需要掌握的最重要的概念是,该支持是通过AOP代理启用的,并且事务性建议是由元数据(当前基于XML或注释)驱动的。AOP与事务性元数据的组合产生了一个AOP代理,该代理使用TransactionInterceptor
和适当的TransactionManager
实现来驱动方法调用周围的事务。
Spring AOP is covered in the AOP section. |
Spring框架的TransactionInterceptor
为命令式和反应式编程模型提供事务管理。拦截器通过检查方法返回类型来检测所需的事务管理风格。返回反应类型(如Publisher
或Kotlinflow
(或其子类型))的方法有资格进行反应事务管理。包括void
在内的所有其他返回类型都使用代码路径进行命令性事务管理。
事务管理风格会影响需要哪个事务管理器。命令性事务需要PlatformTransactionManager
,而反应性事务使用Reactive TransactionManager
实现。
由 |
下图显示了在事务代理上调用方法的概念视图:
1.4.2. Example of Declarative Transaction Implementation
请考虑以下接口及其附带的实现。本例使用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);
}
下面的示例显示了上述接口的实现:
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) {
// ...
}
}
假设FooService
接口的前两个方法,getFoo(字符串)
和getFoo(字符串,字符串)
必须在具有只读语义的事务的上下文中运行,而其他方法intertFoo(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:Advisment/>;
定义中。<;tx:Consultion/>;
定义为“所有以get
开头的方法都将在只读事务的上下文中运行,所有其他方法都将使用默认的事务语义运行”。<;tx:通知/>;
标记的Transaction-Manager
属性被设置为将驱动事务的TransactionManager
Bean的名称(在本例中为txManager
Bean)。
You can omit the transaction-manager attribute in the transactional advice (<tx:advice/> ) if the bean name of the TransactionManager that you want to wire in has the name transactionManager . If the TransactionManager bean that you want to wire in has any other name, you must use the transaction-manager attribute explicitly, as in the preceding example. |
<;aop:config/>;
定义确保txAdviceBean
定义的事务性通知在程序中的适当位置运行。首先,定义一个与FooService
接口(fooServiceOperation
)中定义的任何操作的执行相匹配的切入点。然后使用Advisor将切入点与txAdvice
关联起来。结果表明,在执行fooServiceOperation
时,运行了txAdvice
定义的通知。
<;aop:PointCut/>;
元素中定义的表达式是一个AspectJ切入点表达式。有关Spring中的切入点表达式的更多详细信息,请参阅AOP部分。
一个常见的要求是使整个服务层成为事务性的。要做到这一点,最好的方法是更改切入点表达式以匹配服务层中的任何操作。以下示例显示了如何执行此操作:
<aop:config>
<aop:pointcut id="fooServiceMethods" expression="execution(* x.y.service.*.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="fooServiceMethods"/>
</aop:config>
In the preceding example, it is assumed that all your service interfaces are defined in the x.y.service package. See the AOP section for more details. |
现在我们已经分析了配置,您可能会问自己,“所有这些配置实际上是做什么的?”
前面显示的配置用于围绕从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());
}
}
运行前面程序的输出应该类似于以下内容(为清楚起见,已截断了由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 [org.apache.commons.dbcp.PoolableConnection@a53de4] for JDBC transaction
<!-- the insertFoo(..) method from DefaultFooService throws an exception... -->
[RuleBasedTransactionAttribute] - Applying rules to determine whether transaction should rollback on java.lang.UnsupportedOperationException
[TransactionInterceptor] - Invoking rollback for transaction on x.y.service.FooService.insertFoo due to throwable [java.lang.UnsupportedOperationException]
<!-- and the transaction is rolled back (by default, RuntimeException instances cause rollback) -->
[DataSourceTransactionManager] - Rolling back JDBC transaction on Connection [org.apache.commons.dbcp.PoolableConnection@a53de4]
[DataSourceTransactionManager] - Releasing JDBC Connection after transaction
[DataSourceUtils] - Returning JDBC Connection to DataSource
Exception in thread "main" java.lang.UnsupportedOperationException at x.y.service.DefaultFooService.insertFoo(DefaultFooService.java:14)
<!-- AOP infrastructure stack trace elements removed for clarity -->
at $Proxy0.insertFoo(Unknown Source)
at Boot.main(Boot.java:11)
要使用反应式事务管理,代码必须使用反应式类型。
Spring Framework uses the ReactiveAdapterRegistry to determine whether a method return type is reactive. |
下面的清单显示了以前使用的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);
}
下面的示例显示了上述接口的实现:
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) {
// ...
}
}
命令式和反应式事务管理对于事务边界和事务属性定义共享相同的语义。命令性交易和反应性交易之间的主要区别在于后者的延迟性。TransactionInterceptor
使用事务性运算符修饰返回的反应类型,以开始和清理事务。因此,调用事务性反应性方法将实际事务管理推迟到激活反应性类型的处理的订阅类型。
反应式事务管理的另一个方面与数据转义有关,这是编程模型的自然结果。
命令性事务的方法返回值在方法成功终止时从事务性方法返回,因此部分计算的结果不会逃脱方法闭包。
反应式事务方法返回一个反应式包装器类型,该类型表示一个计算序列以及开始和完成计算的承诺。
发布者
可以在事务正在进行但不一定已完成时发出数据。因此,依赖于成功完成整个事务的方法需要确保完成并缓冲调用代码中的结果。
1.4.3. Rolling Back a Declarative Transaction
上一节概述了如何在应用程序中以声明方式指定类(通常是服务层类)的事务设置的基本知识。本节介绍如何在XML配置中以简单的声明性方式控制事务的回滚。有关使用@Transaction
注释以声明方式控制回滚语义的详细信息,请参阅@Transaction
设置。
要向Spring框架的事务基础设施指示要回滚事务的工作,推荐的方法是从当前在事务上下文中执行的代码抛出异常
。Spring框架的事务基础结构代码捕获任何未处理的异常
,因为它在调用堆栈中冒泡,并决定是否将事务标记为回滚。
在其缺省配置中,Spring框架的事务基础设施代码仅在发生运行时未检查异常的情况下标记要回滚的事务。也就是说,当抛出的异常是RunmeException
的实例或子类时。(错误
实例默认情况下也会导致回滚)。从事务性方法引发的检查异常不会在默认配置中导致回滚。
您可以准确配置将事务标记为回滚的异常
类型,包括通过指定回滚规则检查的异常。
Rollback rules
回滚规则确定在引发给定异常时是否应回滚事务,这些规则基于异常类型或异常模式。 回滚规则可以通过 当使用异常类型定义回滚规则时,该类型将用于匹配引发的异常的类型及其超类型,从而提供类型安全并避免在使用模式时可能发生的任何无意匹配。例如, 回档规则定义异常模式时,模式可以是异常类型的全限定类名,也可以是异常类型的全限定类名的子串(必须是
|
以下XML代码片段演示了如何通过Rollback-for
属性提供异常模式,为选中的特定于应用程序的异常
类型配置回滚:
<tx:advice id="txAdvice" transaction-manager="txManager">
<tx:attributes>
<tx:method name="get*" read-only="true" rollback-for="NoProductInStockException"/>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
如果不希望在引发异常时回滚事务,还可以指定“无回滚”规则。下面的示例告诉Spring框架的事务基础结构,即使面对未处理的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();
}
}
如果可能,强烈建议您使用声明性方法进行回滚。如果您绝对需要的话,程序化回滚是可用的,但是它的使用与实现一个干净的基于POJO的架构相冲突。
1.4.4. Configuring Different Transactional Semantics for Different Beans
考虑这样一种场景,其中您有许多服务层对象,并且您想要对每个对象应用完全不同的事务配置。为此,您可以使用不同的切入点
和Advisor-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/> Settings
本节总结了可以使用<;tx:通知/>;
标记指定的各种事务设置。默认<;tx:通知/&>
设置为:
-
传播设置是必需的。
-
隔离级别为
默认。
-
该事务是读写的。
-
事务超时默认为基础事务系统的默认超时,如果不支持超时,则为无。
-
任何
运行异常
都会触发回滚,而任何选中的异常
都不会。
您可以更改这些默认设置。下表汇总了嵌套在<;tx:通知/>;
和<;tx:Attributes/>;
标记中的<;tx:method/>;
标记的各种属性:
Attribute | Required? | Default | Description |
---|---|---|---|
|
是 |
要与事务属性关联的方法名称。通配符(*)可用于将相同的事务属性设置与许多方法(例如, |
|
|
不是 |
|
事务传播行为。 |
|
不是 |
|
事务隔离级别。仅适用于 |
|
不是 |
-1 |
事务超时(秒)。仅适用于传播 |
|
不是 |
错误 |
读写事务与只读事务。仅适用于 |
|
不是 |
以逗号分隔的触发回滚的 |
|
|
不是 |
不触发回滚的 |
1.4.6. Using @Transactional
除了基于XML的声明性事务配置方法外,您还可以使用基于注释的方法。直接在Java源代码中声明事务语义使声明更接近受影响的代码。过度耦合没有太大危险,因为本来要以事务方式使用的代码几乎总是以这种方式部署的。
The standard jakarta.transaction.Transactional annotation is also supported as a drop-in replacement to Spring’s own annotation. Please refer to the JTA documentation for more details. |
使用@Transaction
注释所提供的易用性通过一个示例得到了最好的说明,下面的文本对此进行了解释。考虑以下类定义:
// 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) {
// ...
}
}
1 | The line that makes the bean instance transactional. |
如上所述,注释在类级别使用,表示声明类(及其子类)的所有方法的默认设置。或者,每种方法都可以单独进行注释。有关Spring认为哪些方法是事务性的,请参阅方法可见性和@Transaction
。请注意,类级别的注释不适用于类层次结构中向上的祖先类;在这种情况下,继承的方法需要在本地重新声明才能参与子类级别的注释。
当像上面这样的POJO类被定义为Spring上下文中的Bean时,您可以通过@Configuration
类中的@EnableTransactionManagement
注释使Bean实例成为事务性的。有关详细信息,请参阅javadoc。
在XML配置中,<;tx:Annotation-Driven/>;
标签提供了类似的便利:
<!-- from the file 'context.xml' -->
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation=" http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/tx https://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- this is the service object that we want to make transactional -->
<bean id="fooService" class="x.y.service.DefaultFooService"/>
<!-- enable the configuration of transactional behavior based on annotations -->
<!-- a TransactionManager is still required -->
<tx:annotation-driven transaction-manager="txManager"/> (1)
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- (this dependency is defined somewhere else) -->
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- other <bean/> definitions here -->
</beans>
You can omit the transaction-manager attribute in the <tx:annotation-driven/> tag if the bean name of the TransactionManager that you want to wire in has the name transactionManager . If the TransactionManager bean that you want to dependency-inject has any other name, you have to use the transaction-manager attribute, as in the preceding example. |
反应式事务方法使用反应式返回类型,与命令式编程安排形成对比,如下面的清单所示:
// 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) {
// ...
}
}
请注意,对于返回的发布者
,对于反应式流取消信号有特殊的注意事项。有关详细信息,请参阅“使用TransactionalOperator”下的取消信号一节。
Method visibility and
@Transactional
当您在Spring的标准配置中使用事务代理时,您应该只将 当在
Spring TestContext框架默认支持非私有 |
您可以将@Transaction
注释应用于接口定义、接口上的方法、类定义或类上的方法。然而,仅仅存在@Transaction
注释并不足以激活事务性行为。@Transaction
注释只是一些运行时基础设施可以使用的元数据,这些运行时基础设施可以识别@Transaction
,并且可以使用元数据来配置具有事务性行为的适当Bean。在前面的示例中,<;tx:Annotation-Driven/>;
元素打开了事务行为。
The Spring team recommends that you annotate only concrete classes (and methods of concrete classes) with the @Transactional annotation, as opposed to annotating interfaces. You certainly can place the @Transactional annotation on an interface (or an interface method), but this works only as you would expect it to if you use interface-based proxies. The fact that Java annotations are not inherited from interfaces means that, if you use class-based proxies (proxy-target-class="true" ) or the weaving-based aspect (mode="aspectj" ), the transaction settings are not recognized by the proxying and weaving infrastructure, and the object is not wrapped in a transactional proxy. |
In proxy mode (which is the default), only external method calls coming in through the proxy are intercepted. This means that self-invocation (in effect, a method within the target object calling another method of the target object) does not lead to an actual transaction at runtime even if the invoked method is marked with @Transactional . Also, the proxy must be fully initialized to provide the expected behavior, so you should not rely on this feature in your initialization code — for example, in a @PostConstruct method. |
如果您希望自调用也与事务一起包装,请考虑使用AspectJ模式(请参阅下表中的模式
属性)。在这种情况下,首先没有代理。相反,目标类被编织(即其字节代码被修改)以支持任何类型方法上的@Transaction
运行时行为。
XML Attribute | Annotation Attribute | Default | Description |
---|---|---|---|
|
不适用(参见 |
|
要使用的事务管理器的名称。仅当事务管理器的名称不是 |
|
|
|
默认模式( |
|
|
|
仅适用于 |
|
|
|
定义应用于使用 |
The default advice mode for processing @Transactional annotations is proxy , which allows for interception of calls through the proxy only. Local calls within the same class cannot get intercepted that way. For a more advanced mode of interception, consider switching to aspectj mode in combination with compile-time or load-time weaving. |
The proxy-target-class attribute controls what type of transactional proxies are created for classes annotated with the @Transactional annotation. If proxy-target-class is set to true , class-based proxies are created. If proxy-target-class is false or if the attribute is omitted, standard JDK interface-based proxies are created. (See Proxying Mechanisms for a discussion of the different proxy types.) |
@EnableTransactionManagement and <tx:annotation-driven/> look for @Transactional only on beans in the same application context in which they are defined. This means that, if you put annotation-driven configuration in a WebApplicationContext for a DispatcherServlet , it checks for @Transactional beans only in your controllers and not in your services. See MVC for more information. |
在评估方法的事务设置时,派生最多的位置优先。在下面的示例中,DefaultFooService
类在类级别使用只读事务的设置进行批注,但同一个类中的updateFoo(Foo)
方法上的@Transaction
批注优先于在类级别定义的事务设置。
@Transactional(readOnly = true)
public class DefaultFooService implements FooService {
public Foo getFoo(String fooName) {
// ...
}
// these settings have precedence for this method
@Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW)
public void updateFoo(Foo foo) {
// ...
}
}
@Transactional
Settings
@Transaction
注释是元数据,它指定接口、类或方法必须具有事务性语义(例如,“当调用此方法时启动一个全新的只读事务,挂起任何现有的事务”)。默认的@Transaction
设置如下:
-
传播设置为
PROPACTION_REQUIRED。
-
隔离级别为
Isolation_Default。
-
该事务是读写的。
-
事务超时默认为基础事务系统的默认超时,如果不支持超时,则默认为无。
-
任何
运行异常
或错误
都会触发回滚,而任何选中的异常
不会触发回滚。
您可以更改这些默认设置。下表总结了@Transaction
批注的各种属性:
Property | Type | Description |
---|---|---|
|
可选限定符,指定要使用的事务管理器。 |
|
|
|
|
|
|
标签可以由事务管理器进行评估,以将特定于实现的行为与实际事务相关联。 |
|
可选传播设置。 |
|
|
|
可选的隔离级别。仅适用于 |
|
|
可选的事务超时。仅适用于 |
|
|
将 |
|
|
读写事务与只读事务。仅适用于 |
|
|
必须导致回滚的异常类型的可选数组。 |
|
异常名称模式数组。 |
必须导致回滚的异常名称模式的可选数组。 |
|
|
不能导致回滚的异常类型的可选数组。 |
|
异常名称模式数组。 |
不能导致回滚的异常名称模式的可选数组。 |
See Rollback rules for further details on rollback rule semantics, patterns, and warnings regarding possible unintentional matches for pattern-based rollback rules. |
目前,您不能显式控制事务的名称,其中‘name’表示出现在事务监视器(如WebLogic的事务监视器)和日志记录输出中的事务名称。对于声明性事务,事务名始终是完全限定的类名+.
+事务建议类的方法名。例如,如果BusinessService
类的handlePayment(..)
方法启动了一个事务,则该事务的名称将为:com.example.BusinessService.handlePayment
.
Multiple Transaction Managers with @Transactional
大多数Spring应用程序只需要一个事务管理器,但在某些情况下,您可能希望在单个应用程序中有多个独立的事务管理器。您可以使用@Transaction
注释的值
或TransactionManager
属性来选择性地指定要使用的TransactionManager
的标识。这可以是Bean名称,也可以是事务管理器Bean的限定符值。例如,使用限定符表示法,您可以在应用程序上下文中将以下Java代码与以下事务管理器Bean声明相结合:
public class TransactionalService {
@Transactional("order")
public void setSomething(String name) { ... }
@Transactional("account")
public void doSomething() { ... }
@Transactional("reactive-account")
public Mono<Void> doSomethingReactive() { ... }
}
下面的清单显示了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
上的各个方法在单独的事务管理器下运行,由顺序
、帐户
和反应帐户
限定符区分。如果没有找到特别限定的TransactionManager
Bean,则仍然使用默认的<;tx:Annotation-Driven>;
目标Bean名称TransactionManager
。
Custom Composed Annotations
如果您发现在许多不同的方法上重复使用相同的属性和@Transaction
,Spring元批注支持允许您为特定用例定义定制的合成批注。例如,考虑以下注释定义:
@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 {
}
前面的注释让我们可以编写上一节中的示例,如下所示:
public class TransactionalService {
@OrderTx
public void setSomething(String name) {
// ...
}
@AccountTx
public void doSomething() {
// ...
}
}
在前面的示例中,我们使用语法来定义事务管理器限定符和事务标签,但我们还可以包括传播行为、回滚规则、超时和其他功能。
1.4.7. Transaction Propagation
本节描述了Spring中事务传播的一些语义。请注意,本节不是对事务传播的适当介绍。相反,它详细介绍了与Spring中的事务传播有关的一些语义。
在Spring管理的事务中,请注意物理事务和逻辑事务之间的差异,以及传播设置如何应用于这种差异。
Understanding PROPAGATION_REQUIRED
PROPACTION_REQUIRED
强制执行物理事务,如果尚不存在事务,则在本地为当前作用域强制执行,或者参与为更大作用域定义的现有“外部”事务。这是同一线程(例如,委托给多个存储库方法的服务外观,其中所有底层资源都必须参与服务级别事务)内的常见调用堆栈安排中的一个很好的缺省设置。
By default, a participating transaction joins the characteristics of the outer scope, silently ignoring the local isolation level, timeout value, or read-only flag (if any). Consider switching the validateExistingTransactions flag to true on your transaction manager if you want isolation level declarations to be rejected when participating in an existing transaction with a different isolation level. This non-lenient mode also rejects read-only mismatches (that is, an inner read-write transaction that tries to participate in a read-only outer scope). |
当传播设置为PROPACTION_REQUIRED
时,将为应用该设置的每个方法创建一个逻辑事务作用域。每个这样的逻辑事务作用域都可以单独确定仅回滚状态,外部事务作用域在逻辑上独立于内部事务作用域。在标准Proportation_Required
行为的情况下,所有这些作用域都映射到相同的物理事务。因此,内部事务作用域中设置的仅回滚标记确实会影响外部事务实际提交的机会。
但是,如果内部事务作用域设置了ROLLBACK-ONLY标记,则外部事务本身尚未决定回滚,因此回滚(由内部事务作用域静默触发)是意外的。此时将抛出相应的UnexpectedRollbackException
异常。这是预期的行为,因此事务的调用者永远不会被误导,以为执行了提交,而实际上并没有执行。因此,如果内部事务(外部调用方不知道)默默地将事务标记为仅回滚,外部调用方仍会调用COMMIT。外部调用方需要接收UnexpectedRollackException
,以明确指示已执行回滚。
Understanding PROPAGATION_REQUIRES_NEW
PROPACTION_REQUIRED_NEW
与PROPACTION_REQUIRED
不同,它始终对每个受影响的事务作用域使用独立的物理事务,从不参与外部作用域的现有事务。在这种安排中,底层资源事务是不同的,因此可以独立地提交或回滚,其中外部事务不受内部事务的回滚状态的影响,并且内部事务的锁在其完成后立即被释放。这种独立的内部事务还可以声明其自己的隔离级别、超时和只读设置,而不继承外部事务的特征。
Understanding PROPAGATION_NESTED
PROPACTION_NESTED
使用具有多个保存点的单个物理事务,它可以回滚到这些保存点。这种部分回滚允许内部事务作用域触发其作用域的回滚,而外部事务能够继续物理事务,尽管已回滚了一些操作。此设置通常映射到JDBC保存点,因此它仅适用于JDBC资源事务。参见Spring‘sDataSourceTransactionManager
.
1.4.8. Advising Transactional Operations
假设您想同时运行事务性操作和一些基本的分析建议。在<;tx:Annotation-Driven/>;
的上下文中如何实现这一点?
当您调用updateFoo(Foo)
方法时,您希望看到以下操作:
-
配置的评测方面启动。
-
事务性建议将运行。
-
建议对象上的方法运行。
-
事务提交。
-
分析方面报告整个事务性方法调用的确切持续时间。
This chapter is not concerned with explaining AOP in any great detail (except as it applies to transactions). See AOP for detailed coverage of the AOP configuration and AOP in general. |
下面的代码显示了前面讨论的简单分析方面:
package x.y; 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; } }
通知的排序是通过Ordered
接口控制的。有关建议排序的详细信息,请参阅建议排序。
以下配置创建了一个fooService
Bean,该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
属性的值,使其高于事务性建议的Order值。
您可以用类似的方式配置其他方面。
1.4.9. Using @Transactional
with AspectJ
您还可以通过AspectJ方面在Spring容器外部使用Spring框架的@Transaction
支持。为此,首先使用@Transaction
注释注释您的类(以及您的类的方法),然后将您的应用程序与
文件中定义的代码链接(编织)。您还必须使用事务管理器配置方面。您可以使用Spring框架的IoC容器来处理注入方面的依赖项。配置事务管理方面的最简单方法是使用org.springframework.transaction.aspectj.AnnotationTransactionAspect
-aspects.jar<;tx:Annotation-Driven/>;
元素,并将模式
属性指定给AspectJ
,如Using@Transaction
中所述。因为这里我们关注的是在Spring容器之外运行的应用程序,所以我们将向您展示如何通过编程来实现这一点。
Prior to continuing, you may want to read Using @Transactional and AOP respectively. |
以下示例显示如何创建事务管理器并配置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);
When you use this aspect, you must annotate the implementation class (or the methods within that class or both), not the interface (if any) that the class implements. AspectJ follows Java’s rule that annotations on interfaces are not inherited. |
类上的@Transaction
注释为类中任何公共方法的执行指定默认事务语义。
类中方法上的@Transaction
注释覆盖了类注释(如果存在)给出的默认事务语义。您可以注释任何方法,而不管其可见性如何。
要使用AnnotationTransactionAspect
构建应用程序,必须使用AspectJ构建应用程序(请参阅AspectJ开发指南)或使用加载时编织。有关使用AspectJ进行加载时编织的讨论,请参阅Spring框架中的使用AspectJ进行加载时编织。
1.5. Programmatic Transaction Management
Spring框架提供了两种编程事务管理方法,通过使用:
-
TransactionTemplate
或TransactionalOperator
。 -
直接实现
TransactionManager
。
Spring团队通常建议将TransactionTemplate
用于命令性流程中的程序性事务管理,并将TransactionalOperator
用于反应性代码。第二种方法类似于使用JTAUserTransaction
API,尽管异常处理不那么麻烦。
1.5.1. Using the TransactionTemplate
TransactionTemplate
采用与其他Spring模板相同的方法,如JdbcTemplate
。它使用回调方法(将应用程序代码从必须执行的样板获取和释放事务资源中解放出来),并生成意图驱动的代码,因为您的代码只关注您想要做的事情。
As the examples that follow show, using the TransactionTemplate absolutely couples you to Spring’s transaction infrastructure and APIs. Whether or not programmatic transaction management is suitable for your development needs is a decision that you have to make yourself. |
必须在事务上下文中运行并显式使用TransactionTemplate
的应用程序代码类似于下一个示例。作为应用程序开发人员,您可以编写TransactionCallback
实现(通常表示为匿名内部类),其中包含在事务上下文中运行所需的代码。然后,您可以将自定义TransactionCallback
的实例传递给TransactionTemplate
上公开的Execute(..)
方法。以下示例显示了如何执行此操作:
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();
}
});
}
}
如果没有返回值,可以将方便的TransactionCallback WithoutResult
类用于匿名类,如下所示:
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
protected void doInTransactionWithoutResult(TransactionStatus status) {
updateOperation1();
updateOperation2();
}
});
回调中的代码可以通过对提供的TransactionStatus
对象调用setRollackOnly()
方法回滚事务,如下所示:
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
protected void doInTransactionWithoutResult(TransactionStatus status) {
try {
updateOperation1();
updateOperation2();
} catch (SomeBusinessException ex) {
status.setRollbackOnly();
}
}
});
Specifying Transaction Settings
您可以通过编程或在配置中在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...
}
}
下面的示例使用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. Using the TransactionalOperator
TransactionalOperator
遵循与其他反应式运算符类似的运算符设计。它使用回调方法(将应用程序代码从必须执行的样板获取和释放事务资源中解放出来),并生成意图驱动的代码,因为您的代码只关注您想要做的事情。
As the examples that follow show, using the TransactionalOperator absolutely couples you to Spring’s transaction infrastructure and APIs. Whether or not programmatic transaction management is suitable for your development needs is a decision that you have to make yourself. |
必须在事务上下文中运行并显式使用TransactionalOperator
的应用程序代码类似于下一个示例:
public class SimpleService implements Service {
// single TransactionalOperator shared amongst all methods in this instance
private final TransactionalOperator transactionalOperator;
// use constructor-injection to supply the ReactiveTransactionManager
public SimpleService(ReactiveTransactionManager transactionManager) {
this.transactionalOperator = 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);
}
}
TransactionalOperator
有两种使用方式:
-
使用项目反应堆类型(
mono.as(transactionalOperator::transactional)
)的操作员风格 -
所有其他Case(
transactionalOperator.execute(TransactionCallback<;T>;)
)的回调样式
回调中的代码可以通过对提供的Reactive Transaction
对象调用setRollackOnly()
方法回滚事务,如下所示:
transactionalOperator.execute(new TransactionCallback<>() {
public Mono<Object> doInTransaction(ReactiveTransaction status) {
return updateOperation1().then(updateOperation2)
.doOnError(SomeBusinessException.class, e -> status.setRollbackOnly());
}
}
});
Cancel Signals
在反应流中,订阅者
可以取消其订阅
并停止其发布者
。项目反应器中的运算符,以及其他库中的运算符,如Next()
、Take(Long)
、Timeout(持续时间)
以及其他库中的运算符可以发出取消。没有办法知道取消的原因,无论是由于错误还是仅仅是对进一步消费缺乏兴趣。从5.3版开始,取消信号会导致回滚。因此,重要的是要考虑在事务发布者
下游使用的运算符。尤其是在Flux
或其他多值发布者
的情况下,必须使用全部输出才能完成事务。
Specifying Transaction Settings
您可以为TransactionalOperator
指定事务设置(如传播模式、隔离级别、超时等)。默认情况下,TransactionalOperator
实例具有默认事务设置。以下示例显示特定TransactionalOperator:
的事务设置的自定义
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);
}
}
1.5.3. Using the TransactionManager
以下各节解释命令式事务管理器和反应式事务管理器的编程用法。
Using the PlatformTransactionManager
对于命令性事务,您可以直接使用org.springframework.transaction.PlatformTransactionManager
来管理事务。为此,请通过Bean引用将您使用的PlatformTransactionManager
的实现传递给您的Bean。然后,通过使用TransactionDefinition
和TransactionStatus
对象,您可以启动事务、回滚和提交。以下示例显示了如何执行此操作:
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);
Using the ReactiveTransactionManager
在处理被动事务时,您可以直接使用org.springframework.transaction.ReactiveTransactionManager
来管理您的事务。为此,请通过Bean引用将您使用的Reactive TransactionManager
的实现传递给您的Bean。然后,通过使用TransactionDefinition
和Reactive Transaction
对象,您可以启动事务、回滚和提交。以下示例显示了如何执行此操作:
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)));
});
1.6. Choosing Between Programmatic and Declarative Transaction Management
只有当您有少量的事务性操作时,程序化事务管理通常才是一个好主意。例如,如果您的Web应用程序只需要某些更新操作的事务,则可能不希望使用Spring或任何其他技术来设置事务代理。在这种情况下,使用TransactionTemplate
可能是一个很好的方法。能够显式设置事务名称也是只有通过使用事务管理的编程方法才能完成的事情。
另一方面,如果您的应用程序有许多事务性操作,声明性事务管理通常是值得的。它将事务管理排除在业务逻辑之外,并且配置起来并不困难。当使用Spring框架而不是EJB CMT时,声明性事务管理的配置成本大大降低。
1.7. Transaction-bound Events
从Spring4.2开始,事件的侦听器可以绑定到事务的一个阶段。典型的例子是在事务成功完成时处理事件。这样做可以在当前事务的结果对侦听器真正重要时更灵活地使用事件。
您可以使用@EventListener
注释注册常规事件监听器。如果需要将其绑定到事务,请使用@TransactionalEventListener
。这样做时,默认情况下,侦听器将绑定到事务的提交阶段。
下一个例子展示了这个概念。假设一个组件发布了一个由订单创建的事件,并且我们希望定义一个侦听器,该侦听器应该只在发布它的事务成功提交后才处理该事件。下面的示例设置这样的事件侦听器:
@Component
public class MyComponent {
@TransactionalEventListener
public void handleOrderCreatedEvent(CreationEvent<Order> creationEvent) {
// ...
}
}
@TransactionalEventListener
注释公开了一个阶段
属性,允许您自定义侦听器应该绑定到的事务的阶段。有效阶段是BEFORE_COMMIT
、AFTER_COMMIT
(默认)、AFTER_ROLLBACK
以及聚合事务完成后(无论是提交还是回滚)的AFTER_COMPLETION
。
如果没有正在运行的事务,则根本不会调用侦听器,因为我们无法遵守所需的语义。但是,您可以通过将批注的FallackExecution
属性设置为true
来覆盖该行为。
|
1.8. Application server-specific integration
Spring的事务抽象通常是与应用服务器无关的。此外,Spring的JtaTransactionManager
类(它可以选择性地对JTAUserTransaction
和TransactionManager
对象执行JNDI查找)自动检测后一个对象的位置,该位置因应用程序服务器而异。访问JTA< >TransactionManager尤其允许增强的事务语义代码- ,支持事务挂起。有关详细信息,请参阅JtaTransactionManager
javadoc。
Spring的JtaTransactionManager
是在Jakarta EE应用服务器上运行的标准选择,众所周知可以在所有常见服务器上运行。高级功能,如事务挂起,也可以在许多服务器(包括GlassFish、JBoss和Geronimo)上运行,而不需要任何特殊配置。然而,为了完全支持事务挂起和进一步的高级集成,Spring为WebLogic Server和WebSphere提供了特殊的适配器。这些适配器将在以下各节中讨论。
对于标准场景,包括WebLogic Server和WebSphereTM,可以考虑使用方便的<;tx:jta-transaction-manager/>;
配置元素。配置后,该元素会自动检测底层服务器,并选择可用于平台的最佳事务管理器。这意味着您不需要显式配置特定于服务器的适配器类(如以下各节所述)。相反,它们是自动选择的,标准的JtaTransactionManager
是默认的后备选项。
1.9. Solutions to Common Problems
本节介绍一些常见问题的解决方案。
1.9.1. Using the Wrong Transaction Manager for a Specific DataSource
根据您选择的事务技术和要求,使用正确的PlatformTransactionManager
实现。如果使用得当,Spring框架只提供了一个简单且可移植的抽象。如果使用全局事务,则必须为所有事务操作使用org.springframework.transaction.jta.JtaTransactionManager
类(或其特定于应用服务器的子类)。否则,事务基础结构会尝试在容器DataSource
实例等资源上执行本地事务。这样的本地事务没有意义,一个好的应用程序服务器会将它们视为错误。
1.10. Further Resources
有关Spring框架的事务支持的更多信息,请参阅:
-
Spring中的分布式事务,使用和不使用XA是一个Java World演示文稿,其中Spring的David Syer向您介绍了七种用于Spring应用程序中的分布式事务的模式,其中三种使用XA,四种不使用XA。
-
Java事务设计策略是InfoQ提供的一本介绍Java事务的书。它还包括如何在Spring框架和EJB3中配置和使用事务的并列示例。
2. DAO Support
Spring中的数据访问对象(DAO)支持旨在使以一致的方式轻松地使用数据访问技术(如JDBC、Hibernate或JPA)。这使您可以相当轻松地在上述持久性技术之间进行切换,并且还允许您编写代码,而无需担心捕获特定于每种技术的异常。
2.1. Consistent Exception Hierarchy
Spring提供了从特定于技术的异常(如SQLException
)到其自己的异常类层次结构(将DataAccessException
作为根异常)的方便转换。这些异常包装了原始异常,因此您永远不会丢失有关可能出错的任何信息的任何风险。
除了JDBC异常,Spring还可以包装特定于JPA和Hibernate的异常,将它们转换为一组集中的运行时异常。这使您可以只在适当的层中处理大多数不可恢复的持久性异常,而不需要在DAO中使用恼人的样板捕获抛出块和异常声明。(不过,您仍然可以在需要的任何地方捕获和处理异常。)如上所述,JDBC异常(包括特定于数据库的方言)也被转换为相同的层次结构,这意味着您可以在一致的编程模型中使用JDBC执行一些操作。
前面的讨论适用于Spring对各种ORM框架的支持中的各种模板类。如果使用基于拦截器的类,应用程序必须注意处理<代码>HibernateExceptions
和<代码>PersistenceExceptions本身,最好是分别委托给<代码>SessionFactoryUtils的convertHibernateAccessException(..)
或ConvertJpaAccessException(..)
方法。这些方法将异常转换为与org.springFrawork.ao
异常层次结构中的异常兼容的异常。由于PersistenceExceptions
未被选中,它们也可能被抛出(不过,就异常而言,牺牲了泛型DAO抽象)。
下图显示了Spring提供的异常层次结构。(请注意,图像中详细的类层次结构只显示了整个DataAccessException
层次结构的一个子集。)
2.2. Annotations Used to Configure DAO or Repository Classes
保证数据访问对象(DAO)或存储库提供异常转换的最好方法是使用@Repository
注释。该注释还允许组件扫描支持查找和配置DAO和存储库,而不必为它们提供XML配置条目。以下示例显示如何使用@Repository
批注:
@Repository (1)
public class SomeMovieFinder implements MovieFinder {
// ...
}
1 | The @Repository annotation. |
任何DAO或存储库实现都需要访问持久性资源,具体取决于使用的持久性技术。例如,基于JDBC的存储库需要访问JDBCDataSource
,而基于JPA的存储库需要访问EntityManager
。要实现这一点,最简单的方法是使用@AuTower
、@Inject
、@Resource
或@PersistenceContext
注释之一注入此资源依赖项。以下示例适用于JPA存储库:
@Repository
public class JpaMovieFinder implements MovieFinder {
@PersistenceContext
private EntityManager entityManager;
// ...
}
如果您使用经典的Hibernate API,则可以注入SessionFactory
,如下例所示:
@Repository
public class HibernateMovieFinder implements MovieFinder {
private SessionFactory sessionFactory;
@Autowired
public void setSessionFactory(SessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
}
// ...
}
我们在这里展示的最后一个示例是针对典型的JDBC支持。您可以将DataSource
注入到初始化方法或构造函数中,在其中您将使用DataSource
创建JdbcTemplate
和其他数据访问支持类(如SimpleJdbcCall
和其他类)。下面的示例自动生成一个DataSource
:
@Repository
public class JdbcMovieFinder implements MovieFinder {
private JdbcTemplate jdbcTemplate;
@Autowired
public void init(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
// ...
}
See the specific coverage of each persistence technology for details on how to configure the application context to take advantage of these annotations. |
3. Data Access with JDBC
下表中列出的一系列操作可能最能体现Spring框架JDBC抽象提供的价值。该表显示了Spring负责哪些操作,哪些操作是您的责任。
Action | Spring | You |
---|---|---|
定义连接参数。 |
X |
|
打开连接。 |
X |
|
指定SQL语句。 |
X |
|
声明参数并提供参数值 |
X |
|
准备并运行该语句。 |
X |
|
设置循环以迭代结果(如果有的话)。 |
X |
|
完成每个迭代的工作。 |
X |
|
处理任何异常。 |
X |
|
处理交易。 |
X |
|
关闭连接、语句和结果集。 |
X |
Spring框架负责处理使JDBC成为如此乏味的API的所有底层细节。
3.1. Choosing an Approach for JDBC Database Access
您可以在几种方法中进行选择,以形成JDBC数据库访问的基础。除了三种风格的JdbcTemplate
之外,一个新的SimpleJdbcInsert
和SimpleJdbcCall
方法优化了数据库元数据,而RDBMS对象样式采用了一种更面向对象的方法,类似于JDO查询设计。一旦您开始使用这些方法中的一种,您仍然可以混合匹配以包含来自不同方法的功能。所有方法都需要与JDBC 2.0兼容的驱动程序,一些高级功能需要JDBC 3.0驱动程序。
-
JdbcTemplate
是经典且最流行的Spring JDBC方法。这种“最低级别”的方法和所有其他方法都在幕后使用JdbcTemplate。 -
Named参数JdbcTemplate
包装JdbcTemplate
以提供命名参数,而不是传统的JDBC?
占位符。当一条SQL语句有多个参数时,这种方法提供了更好的文档和易用性。 -
SimpleJdbcInsert
和SimpleJdbcCall
优化数据库元数据以限制必要的配置量。这种方法简化了编码,因此您只需要提供表或过程的名称,并提供与列名匹配的参数映射。只有当数据库提供足够的元数据时,这才起作用。如果数据库不提供此元数据,则必须提供参数的显式配置。 -
RDBMS对象--包括
MappingSqlQuery
、SqlUpdate
和StoredProcedure
--要求您在数据访问层的初始化过程中创建可重用且线程安全的对象。这种方法仿效JDO查询,您可以定义查询字符串、声明参数并编译查询。完成此操作后,<代码>将执行(…) ,<代码>更新(…)和查找对象(…)
方法可以使用不同的参数值多次调用。
3.2. Package Hierarchy
Spring框架的JDBC抽象框架由四个不同的包组成:
-
core
:org.springFrawork.jdbc.core
包包含JdbcTemplate
类及其各种回调接口,以及各种相关类。名为org.springframework.jdbc.core.simple
的子包包含SimpleJdbcInsert
和SimpleJdbcCall
类。另一个名为org.springframework.jdbc.core.namedparam
的子包包含Named参数JdbcTemplate
类和相关的支持类。请参阅使用JDBC核心类控制基本的JDBC处理和错误处理、JDBC批处理和使用SimpleJdbc类简化JDBC操作。 -
数据源
:org.springframework.jdbc.datasource
包包含一个实用程序类,用于轻松访问数据源
和各种简单的数据源
实现,您可以使用这些实现在Jakarta EE容器外部测试和运行未经修改的JDBC代码。一个名为org.springfamework.jdbc.datasource.embedded
的子包支持使用Java数据库引擎创建嵌入式数据库,比如HSQL、H2和Derby。请参阅控制数据库连接和嵌入式数据库支持。 -
对象
:org.springFrawork.jdbc.Object
包包含将RDBMS查询、更新和存储过程表示为线程安全、可重用对象的类。请参阅将JDBC操作建模为Java对象。这种方法是由JDO建模的,尽管查询返回的对象自然与数据库断开连接。这种较高级别的JDBC抽象依赖于org.springFrawork.jdbc.core
包中的较低级别抽象。 -
支持
:org.springFrawork.jdbc.Support
包提供了SQLException
转换功能和一些实用程序类。JDBC处理过程中抛出的异常被转换为org.springFrawork.ao
包中定义的异常。这意味着使用Spring JDBC抽象层的代码不需要实现特定于JDBC或RDBMS的错误处理。所有已转换的异常都未选中,这使您可以选择捕获可从中恢复的异常,同时允许将其他异常传播到调用方。请参阅UsingSQLExceptionTranslator
。
3.3. Using the JDBC Core Classes to Control Basic JDBC Processing and Error Handling
本节介绍如何使用JDBC核心类来控制基本的JDBC处理,包括错误处理。它包括以下主题:
3.3.1. Using JdbcTemplate
JdbcTemplate
是JDBC核心包的核心类。它处理资源的创建和释放,帮助您避免常见错误,如忘记关闭连接。它执行核心JDBC工作流的基本任务(如语句创建和执行),让应用程序代码提供SQL和提取结果。JdbcTemplate
类:
-
运行SQL查询
-
更新语句和存储过程调用
-
对
ResultSet
实例执行迭代并提取返回的参数值。 -
捕获JDBC异常并将其转换为
org.springFrawork.ao
包中定义的通用的、信息量更大的异常层次结构。(请参阅一致异常层次结构。)
当您对代码使用JdbcTemplate
时,您只需要实现回调接口,为它们提供一个明确定义的约定。给定JdbcTemplate
类提供的连接
,PreparedStatementCreator
回调接口创建一条准备好的语句,提供SQL和任何必要的参数。CallableStatementCreator
接口也是如此,它创建可调用的语句。RowCallback Handler
接口从ResultSet
的每一行提取值。
您可以通过使用DataSource
引用的直接实例化在DAO实现中使用JdbcTemplate
,也可以在Spring IOC容器中配置它并将其作为Bean引用提供给DAO。
The DataSource should always be configured as a bean in the Spring IoC container. In the first case the bean is given to the service directly; in the second case it is given to the prepared template. |
此类发出的所有SQL都记录在与模板实例的完全限定类名对应的类别下的调试
级别(通常为JdbcTemplate
,但如果使用JdbcTemplate
类的自定义子类,则可能会有所不同)。
以下各节提供了一些JdbcTemplate
用法的示例。这些示例并不是JdbcTemplate
公开的所有功能的详尽列表。有关这一点,请参阅服务员javadoc。
Querying (SELECT
)
以下查询获取关系中的行数:
int rowCount = this.jdbcTemplate.queryForObject("select count(*) from t_actor", Integer.class);
以下查询使用绑定变量:
int countOfActorsNamedJoe = this.jdbcTemplate.queryForObject(
"select count(*) from t_actor where first_name = ?", Integer.class, "Joe");
以下查询查找字符串
:
String lastName = this.jdbcTemplate.queryForObject(
"select last_name from t_actor where id = ?",
String.class, 1212L);
以下查询查找并填充单个域对象:
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);
以下查询查找并填充域对象列表:
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;
});
如果最后两个代码片段实际存在于同一应用程序中,那么删除两个RowMapper
lambda表达式中的重复项并将其提取到单个字段中将是有意义的,然后DAO方法可以根据需要引用该字段。例如,编写前面的代码片段可能更好,如下所示:
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);
}
Updating (INSERT
, UPDATE
, and DELETE
) with JdbcTemplate
您可以使用UPDATE(..)
方法执行插入、更新和删除操作。参数值通常作为变量参数提供,或者作为对象数组提供。
下面的示例插入一个新条目:
this.jdbcTemplate.update(
"insert into t_actor (first_name, last_name) values (?, ?)",
"Leonor", "Watling");
以下示例更新现有条目:
this.jdbcTemplate.update(
"update t_actor set last_name = ? where id = ?",
"Banjo", 5276L);
以下示例删除一个条目:
this.jdbcTemplate.update(
"delete from t_actor where id = ?",
Long.valueOf(actorId));
Other JdbcTemplate
Operations
您可以使用Execute(..)
方法来运行任意SQL。因此,该方法通常用于DDL语句。它被接受回调接口、绑定变量数组等的变量严重重载。下面的示例创建一个表:
this.jdbcTemplate.execute("create table mytable (id integer, name varchar(100))");
下面的示例调用一个存储过程:
this.jdbcTemplate.update(
"call SUPPORT.REFRESH_ACTORS_SUMMARY(?)",
Long.valueOf(unionId));
JdbcTemplate
Best Practices
配置后,JdbcTemplate
类的实例是线程安全的。这一点很重要,因为它意味着您可以配置JdbcTemplate
的单个实例,然后安全地将该共享引用注入多个DAO(或存储库)。JdbcTemplate
是有状态的,因为它维护对DataSource
的引用,但此状态不是会话状态。
在使用
NamedParameterJdbcTemplate
>类(以及相关的<代码>类)时,通常的做法是在您的Spring配置文件中配置一个<代码>数据源
,然后将该共享的<代码>数据源
Bean依赖注入到您的DAO类中。
JdbcTemplate
是在
DataSource
的setter中创建的。这将导致类似以下内容的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...
}
1 | Annotate the class with @Repository . |
2 | Annotate the DataSource setter method with @Autowired . |
3 | Create a new JdbcTemplate with the DataSource . |
以下示例显示了相应的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
来注释该类(这使它成为组件扫描的候选对象),并使用@Autwire
来注释DataSource
setter方法。以下示例显示了如何执行此操作:
@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 | Annotate the class with @Repository . |
2 | Constructor injection of the DataSource . |
3 | Create a new JdbcTemplate with the DataSource . |
以下示例显示了相应的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类都是从它扩展而来的,那么您的子类将从JdbcDaoSupport
类继承一个setDataSource(..)
方法。您可以选择是否从此类继承。JdbcDaoSupport
类仅为方便起见而提供。
无论您选择使用(或不使用)上述哪种模板初始化样式,很少需要在每次希望运行SQL时创建JdbcTemplate
类的新实例。配置好后,JdbcTemplate
实例就是线程安全的。如果您的应用程序访问多个数据库,您可能需要多个JdbcTemplate
实例,这需要多个DataSources
以及随后多个以不同方式配置的JdbcTemplate
实例。
3.3.2. Using NamedParameterJdbcTemplate
与仅使用经典占位符(‘?’
)参数编程JDBC语句相反,Named参数JdbcTemplate
类添加了对使用命名参数编程JDBC语句的支持。Named参数JdbcTemplate
类包装了一个JdbcTemplate
,并委托包装的JdbcTemplate
来完成其大部分工作。本节仅描述<代码>Named参数JdbcTemplate
类中不同于<代码>JdbcTemplate本身的 - 的那些区域,即使用命名参数编程jdbc语句。下面的示例说明如何使用Named参数JdbcTemplate
:
// 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);
}
请注意,在分配给SQL
变量的值中使用了命名参数表示法,并将相应的值插入named参数
变量(类型为MapSql参数源
)。
或者,您也可以使用基于Map
的样式将命名参数及其相应值传递给Named参数JdbcTemplate
实例。由Named参数JdbcOperations
公开并由Named参数JdbcTemplate
类实现的其余方法遵循类似的模式,这里不做介绍。
下面的示例显示基于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);
}
与Named参数JdbcTemplate
(存在于同一个Java包中)相关的一个很好的特性是Sql参数源
接口。您已经在前面的代码片段(MapSql参数源
类)中看到了该接口的实现示例。Sql参数源
是Named参数JdbcTemplate
的命名参数值的源。MapSql参数源
类是一个简单的实现,它是java.util.Map
周围的适配器,其中键是参数名称,值是参数值。
另一个Sql参数源
实现是BeanPropertySql参数源
类。这个类包装一个任意的JavaBean(即,遵守JavaBean约定的类的实例),并使用包装的JavaBean的属性作为命名参数值的来源。
下面的示例显示了一个典型的Java 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...
}
下面的示例使用Named参数JdbcTemplate
返回上一个示例中显示的类的成员计数:
// 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);
}
请记住,Named参数JdbcTemplate
类包装了一个经典的JdbcTemplate
模板。如果您需要访问包装的JdbcTemplate
实例来访问仅存在于JdbcTemplate
类中的功能,则可以使用getJdbcOperations()
方法通过JdbcOperations
接口访问包装的JdbcTemplate
。
另请参阅JdbcTemplate
最佳实践,了解在应用程序上下文中使用Named参数JdbcTemplate
类的指导原则。
3.3.3. Using SQLExceptionTranslator
SQLExceptionTranslator
org.springframework.dao.DataAccessException
,>是一个由类实现的接口,它可以在SQLException
和Spring自己的SQLExceptionTranslator之间进行转换,后者在数据访问策略方面是不可知的。实现可以是通用的(例如,使用JDBC的SQLState代码),也可以是专有的(例如,使用Oracle错误代码),以获得更高的精度。
SQLErrorCodeSQLExceptionTranslator
是默认使用的SQLExceptionTranslator
的实现。此实施使用特定的供应商代码。它比SQLState
实现更精确。错误代码转换基于名为SQLErrorCodes
的JavaBean类型类中保存的代码。此类由SQLErrorCodesFactory
创建和填充,顾名思义,它是一个工厂,用于根据名为SQL-Error-codes.xml
的配置文件的内容创建SQLErrorCodes
。此文件使用供应商代码填充,并基于取自DatabaseMetaData
的DatabaseProductName
。将使用您正在使用的实际数据库的代码。
SQLErrorCodeSQLExceptionTranslator
按以下顺序应用匹配规则:
-
由子类实现的任何自定义转换。通常,使用提供的具体
SQLErrorCodeSQLExceptionTranslator
,因此此规则不适用。只有当您实际提供了一个子类实现时,它才适用。 -
作为
SQLErrorCodes
类的stomSqlExceptionTranslator
属性提供的SQLExceptionTranslator
接口的任何自定义实现。 -
搜索
CustomSQLErrorCodesTransaction
类的实例列表(为SQLErrorCodes
类的CustomTranslations
属性提供)以查找匹配项。 -
应用了错误代码匹配。
-
使用备用转换器。
SQLExceptionSubClassTranslator
是默认的后备转换器。如果此转换不可用,则下一个备用转换器是SQLStateSQLExceptionTranslator
。
The SQLErrorCodesFactory is used by default to define Error codes and custom exception translations. They are looked up in a file named sql-error-codes.xml from the classpath, and the matching SQLErrorCodes instance is located based on the database name from the database metadata of the database in use. |
您可以扩展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;
}
}
在前面的示例中,转换了特定的错误代码(-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);
}
向定制翻译器传递数据源,以便在SQL-Error-codes.xml
中查找错误代码。
3.3.4. Running Statements
运行一条SQL语句只需要很少的代码。您需要一个DataSource
和一个JdbcTemplate
,包括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))"); } }
3.3.5. Running Queries
某些查询方法返回单个值。要从一行中检索计数或特定值,请使用queryForObject(..)
。后者将返回的JDBCType
转换为作为参数传入的Java类。如果类型转换无效,则引发InvalidDataAccessApiUsageException
。下面的示例包含两个查询方法,一个用于int
,另一个用于查询字符串
:
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); } }
除了单个结果查询方法外,还有几个方法返回一个列表,其中包含查询返回的每一行的条目。最通用的方法是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");
}
返回的列表如下所示:
[{name=Bob, id=1}, {name=Mary, id=2}]
3.3.6. Updating the Database
下面的示例更新某个主键的列:
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); } }
在前面的示例中,SQL语句具有行参数的占位符。您可以将参数值作为varargs传递,也可以作为对象数组传递。因此,您应该显式地将基元包装在基元包装类中,或者应该使用自动装箱。
3.3.7. Retrieving Auto-generated Keys
一个UPDATE()
方便的方法支持检索数据库生成的主键。此支持是JDBC 3.0标准的一部分。有关详细信息,请参阅规范的第13.6章。该方法将PreparedStatementCreator
作为其第一个参数,这是指定所需INSERT语句的方式。另一个参数是密钥持有者
,它包含从更新成功返回时生成的密钥。没有标准的单一方法来创建适当的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
3.4. Controlling Database Connections
本部分内容包括:
-
使用
TransactionAwareDataSourceProxy
3.4.1. Using DataSource
Spring通过DataSource
获得到数据库的连接。DataSource
是JDBC规范的一部分,是一个通用的连接工厂。它允许容器或框架对应用程序代码隐藏连接池和事务管理问题。作为开发人员,您不需要了解有关如何连接到数据库的详细信息。这是设置数据源的管理员的责任。在开发和测试代码时,您很可能同时担任这两个角色,但您不必知道生产数据源是如何配置的。
当您使用Spring的JDBC层时,您可以从JNDI获取数据源,也可以使用第三方提供的连接池实现来配置您自己的数据源。传统的选择是带有Bean风格的DataSource
类的ApacheCommons DBCP和C3P0;对于现代的JDBC连接池,可以考虑使用HikariCP的构建器风格的API。
You should use the DriverManagerDataSource and SimpleDriverDataSource classes (as included in the Spring distribution) only for testing purposes! Those variants do not provide pooling and perform poorly when multiple requests for a connection are made. |
以下部分使用Spring的DriverManagerDataSource
实现。后面还将介绍其他几个DataSource
变体。
要配置DriverManager数据源
:
-
使用
DriverManagerDataSource
获取连接,就像您通常获得的JDBC连接一样。 -
指定JDBC驱动程序的完全限定类名,以便
DriverManager
可以加载驱动程序类。 -
提供一个因JDBC驱动程序而异的URL。(有关正确的值,请参阅驱动程序的文档。)
-
提供用户名和密码以连接到数据库。
以下示例显示如何在Java中配置DriverManagerDataSource
:
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("org.hsqldb.jdbcDriver");
dataSource.setUrl("jdbc:hsqldb:hsql://localhost:");
dataSource.setUsername("sa");
dataSource.setPassword("");
以下示例显示了相应的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. Using DataSourceUtils
DataSourceUtils
类是一个方便且功能强大的帮助器类,它提供了静态
方法以从JNDI获取连接并在需要时关闭连接。它支持线程绑定连接,例如,DataSourceTransactionManager
。
3.4.3. Implementing SmartDataSource
SmartDataSource
接口应该由能够提供到关系数据库的连接的类实现。它扩展了DataSource
接口,让使用它的类查询在给定操作后是否应该关闭连接。当您知道需要重用连接时,此用法非常有效。
3.4.4. Extending AbstractDataSource
AbstractDataSource
是Spring的DataSource
实现的抽象
基类。它实现所有DataSource
实现通用的代码。如果您编写自己的DataSource
实现,则应该扩展AbstractDataSource
类。
3.4.5. Using SingleConnectionDataSource
SingleConnectionDataSource
类是SmartDataSource
接口的实现,该接口包装单个连接
,该连接在每次使用后都不会关闭。这不支持多线程。
如果任何客户端代码在假定池连接的情况下调用Close
(如使用持久性工具时),则应将suppressClose
属性设置为true
。此设置返回包装物理连接的关闭抑制代理。请注意,您不能再将其强制转换为本机Oracle连接
或类似的对象。
SingleConnectionDataSource
主要是一个测试类。它通常支持在应用程序服务器之外轻松测试代码,并结合简单的JNDI环境。与DriverManagerDataSource
相比,它始终重用相同的连接,避免过多地创建物理连接。
3.4.6. Using DriverManagerDataSource
DriverManagerDataSource
类是标准DataSource
接口的实现,该接口通过Bean属性配置普通的JDBC驱动程序,并每次都返回一个新的连接
。
该实现对于Jakarta EE容器外部的测试和独立环境非常有用,可以作为Spring IOC容器中的DataSource
Bean,也可以与简单的JNDI环境结合使用。池-假设Connection.lose()
调用关闭连接,因此任何DataSource
感知的持久性代码都应该可以工作。然而,使用JavaBean风格的连接池(如Commons-DBCP
)非常简单,即使在测试环境中也是如此,因此使用这样的连接池几乎总是比使用DriverManager erDataSource
更可取。
3.4.7. Using TransactionAwareDataSourceProxy
TransactionAwareDataSourceProxy
是目标数据源
的代理。代理包装目标DataSource
,以增加对Spring管理事务的感知。在这方面,它类似于Jakarta EE服务器提供的事务性JNDIDataSource
。
It is rarely desirable to use this class, except when already existing code must be called and passed a standard JDBC DataSource interface implementation. In this case, you can still have this code be usable and, at the same time, have this code participating in Spring managed transactions. It is generally preferable to write your own new code by using the higher level abstractions for resource management, such as JdbcTemplate or DataSourceUtils . |
有关更多详细信息,请参阅TransactionAwareDataSourceProxy
javadoc。
3.4.8. Using DataSourceTransactionManager
DataSourceTransactionManager
类是单个JDBC数据源的PlatformTransactionManager
实现。它将指定数据源的JDBC连接绑定到当前执行的线程,从而潜在地允许每个数据源有一个线程连接。
通过DataSourceUtils.getConnection(DataSource)
而不是Jakarta EE的标准DataSource.getConnection
检索JDBC连接需要应用程序代码。它抛出未检查的org.springfrawork.ao
异常,而不是已检查的SQLExceptions
。所有框架类(如JdbcTemplate
)都隐式使用此策略。如果不与此事务管理器一起使用,则查找策略的行为与常见策略完全相同。因此,它可以在任何情况下使用。
DataSourceTransactionManager
类支持作为适当的JDBC语句查询超时应用的自定义隔离级别和超时。要支持后者,应用程序代码必须使用<代码>JdbcTemplate
,或者为每个创建的语句调用DataSourceUtils.applyTransactionTimeout(..)
方法。
在单资源的情况下,您可以使用该实现来代替JtaTransactionManager
,因为它不需要容器支持JTA。如果您坚持使用所需的连接查找模式,则在两者之间进行切换只是一个配置问题。JTA不支持自定义隔离级别。
3.5. JDBC Batch Operations
如果您批处理对同一预准备语句的多个调用,则大多数JDBC驱动程序可以提高性能。通过将更新分组为批处理,可以限制数据库的往返次数。
3.5.1. Basic Batch Operations with JdbcTemplate
通过实现特殊接口的两个方法BatchPreparedStatementSetter
,并将该实现作为BatchUpdate
方法调用中的第二个参数传入,可以完成JdbcTemplate
批处理。您可以使用getBatchSize
方法提供当前批次的大小。您可以使用setValues
方法来设置预准备语句的参数值。此方法的调用次数是您在getBatchSize
调用中指定的次数。下面的示例根据列表中的条目更新t_act
表,并将整个列表用作批处理:
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
}
如果您处理更新流或从文件读取,您可能有一个首选的批处理大小,但最后一批可能没有该数量的条目。在这种情况下,您可以使用InterruptibleBatchPreparedStatementSetter
接口,该接口允许您在输入源耗尽后中断批处理。isBatchExhausted
方法允许您发出批次结束的信号。
3.5.2. Batch Operations with a List of Objects
JdbcTemplate
和Named参数JdbcTemplate
都提供了另一种提供批处理更新的方法。不是实现特殊的批处理接口,而是以列表的形式提供调用中的所有参数值。该框架循环遍历这些值并使用内部预准备语句设置器。根据是否使用命名参数,API会有所不同。对于命名参数,您需要提供一个Sql参数源
数组,批处理的每个成员都有一个条目。您可以使用SqlParameterSourceUtils.createBatch
方便的方法来创建这个数组,传入一组Bean样式的对象(带有对应于参数的Getter方法)、字符串
键控映射
实例(将相应的参数作为值包含),或者两者的组合。
以下示例显示了使用命名参数的批处理更新:
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
}
对于使用经典?
占位符的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
}
我们前面描述的所有批处理更新方法都返回一个int
数组,其中包含每个批处理条目受影响的行数。此计数由JDBC驱动程序报告。如果计数不可用,则JDBC驱动程序返回值-2
。
在这种情况下,通过自动设置底层 或者,您可以考虑显式指定相应的JDBC类型,可以通过 |
3.5.3. Batch Operations with Multiple Batches
前面的批处理更新示例处理的批太大,以至于您想要将它们拆分成几个较小的批。您可以通过多次调用BatchUpdate
方法来使用前面提到的方法来实现这一点,但现在有了一个更方便的方法。除SQL语句外,此方法还获取一个<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
}
此调用的批处理更新方法返回一个int
数组数组,其中包含每个批次的数组条目,以及每个更新受影响的行数的数组。顶级数组的长度表示运行的批处理数,第二级数组的长度表示该批处理中的更新数。每个批次中的更新数量应为为所有批次提供的批次大小(最后一个批次可能较少),具体取决于提供的更新对象总数。每个UPDATE语句的更新计数是由JDBC驱动程序报告的更新计数。如果计数不可用,则JDBC驱动程序返回值-2
。
3.6. Simplifying JDBC Operations with the SimpleJdbc
Classes
SimpleJdbcInsert
和SimpleJdbcCall
类通过利用可以通过JDBC驱动程序检索的数据库元数据来提供简化的配置。这意味着您需要预先配置的较少,尽管如果您希望在代码中提供所有细节,则可以覆盖或关闭元数据处理。
3.6.1. Inserting Data by Using SimpleJdbcInsert
我们首先查看具有最少配置选项的SimpleJdbcInsert
类。您应该实例化数据访问层的初始化方法中的SimpleJdbcInsert
。对于本例,初始化方法是setDataSource
方法。您不需要为SimpleJdbcInsert
类创建子类。相反,您可以使用with TableName
方法创建一个新实例并设置表名。该类的配置方法遵循流体
样式,该样式返回SimpleJdbcInsert
的实例,该实例允许您链接所有配置方法。以下示例仅使用一种配置方法(我们稍后将展示多个方法的示例):
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
}
这里使用的Execute
方法使用一个普通的java.util.Map
作为其唯一的参数。这里需要注意的重要一点是,Map
使用的键必须与数据库中定义的表的列名匹配。这是因为我们读取元数据来构造实际的INSERT语句。
3.6.2. Retrieving Auto-generated Keys by Using SimpleJdbcInsert
下一个示例使用与上一个示例相同的INSERT,但是它没有传入id
,而是检索自动生成的键并将其设置在新的Actor
对象上。当它创建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
}
使用第二种方法运行INSERT时,主要区别在于您不会将id
添加到Map
,而是调用ecuteAndReturnKey
方法。这将返回一个java.lang.Number
对象,您可以使用该对象创建域类中使用的Numerical类型的实例。在这里,您不能依赖所有数据库来返回特定的Java类。java.lang.Number
是您可以依赖的基类。如果有多个自动生成的列,或者生成的值不是数字,则可以使用从ecuteAndReturnKeyHolder
方法返回的密钥持有者
。
3.6.3. Specifying Columns for a SimpleJdbcInsert
您可以通过使用usingColumns
方法指定列名列表来限制INSERT的列,如下面的示例所示:
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
}
插入的执行过程与依赖于元数据来确定要使用哪些列的过程相同。
3.6.4. Using SqlParameterSource
to Provide Parameter Values
使用Map
提供参数值很好,但它不是最方便使用的类。Spring提供了两个Sql参数源
接口的实现,您可以使用它们来替代。第一个是BeanPropertySql参数源
,如果您有一个与JavaBean兼容的包含您的值的类,那么它是一个非常方便的类。它使用相应的getter方法来提取参数值。下面的示例展示了如何使用BeanPropertySql参数源
:
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
}
另一个选项是MapSql参数源
,它类似于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
}
如您所见,配置是相同的。只有执行代码必须更改才能使用这些替代输入类。
3.6.5. Calling a Stored Procedure with SimpleJdbcCall
SimpleJdbcCall
类使用数据库中的元数据来查找in
和out
参数的名称,这样您就不必显式声明它们。如果您愿意这样做,或者如果您的参数(如数组
或STRUCT
)没有自动映射到Java类,则可以声明参数。第一个示例显示了一个简单的过程,该过程仅从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
。out
参数返回从表中读取的数据。
您可以用与声明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
}
您为执行调用编写的代码涉及创建一个包含IN参数的Sql参数源
。必须将为输入值提供的名称与存储过程中声明的参数名称匹配。大小写不必匹配,因为您使用元数据来确定应该如何在存储过程中引用数据库对象。在存储过程的源中指定的内容不一定是它在数据库中的存储方式。一些数据库将名称全部转换为大写,而另一些数据库则使用小写或指定的大小写。
Execute
方法接受IN参数,并返回一个Map
,其中包含存储过程中指定的以名称为键的任何out
参数。在本例中,它们是out_first_name
、out_last_name
和out_Birth_Date
。
Execute
方法的最后一部分创建了一个Actor
实例,用于返回检索到的数据。同样,使用存储过程中声明的out
参数的名称也很重要。此外,存储在结果映射中的out
参数名称的大小写与数据库中out
参数名称的大小写匹配,这在不同的数据库中可能有所不同。为了使您的代码更可移植,您应该执行不区分大小写的查找,或者指示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
}
通过执行此操作,您可以避免用于返回的out
参数名称的大小写冲突。
3.6.6. Explicitly Declaring Parameters to Use for a SimpleJdbcCall
在本章的前面部分,我们描述了如何从元数据推导出参数,但如果愿意,您可以显式声明它们。为此,您可以使用声明参数
方法创建和配置SimpleJdbcCall
,该方法接受数量可变的SqlParameter
对象作为输入。有关如何定义SqlParameter
的详细信息,请参阅下一节。
Explicit declarations are necessary if the database you use is not a Spring-supported database. Currently, Spring supports metadata lookup of stored procedure calls for the following databases: Apache Derby, DB2, MySQL, Microsoft SQL Server, Oracle, and Sybase. We also support metadata lookup of stored functions for MySQL, Microsoft SQL Server, and Oracle. |
您可以选择显式声明一个、部分或所有参数。在未显式声明参数的情况下,仍使用参数元数据。要绕过对潜在参数的元数据查找的所有处理,而只使用已声明的参数,可以将方法withoutProcedureColumnMetaDataAccess
作为声明的一部分进行调用。假设您为一个数据库函数声明了两个或多个不同的调用签名。在本例中,您可以调用useIn参数名称
来为给定签名指定要包含的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
}
这两个示例的执行和最终结果是相同的。第二个示例显式指定所有详细信息,而不是依赖于元数据。
3.6.7. How to Define SqlParameters
要为SimpleJdbc
类和RDBMS操作类(在将JDBC操作建模为Java对象中介绍)定义参数,您可以使用SqlParameter
或其一个子类。为此,通常需要在构造函数中指定参数名称和SQL类型。SQL类型是使用java.sql.Types
常量指定的。在本章的前面部分,我们看到了类似以下内容的声明:
new SqlParameter("in_id", Types.NUMERIC),
new SqlOutParameter("out_first_name", Types.VARCHAR),
SqlParameter
的第一行声明了一个IN参数。通过使用SqlQuery
及其子类(参见了解SqlQuery
),可以将IN参数用于存储过程调用和查询。
第二行(带有SqlOutParameter
)声明了要在存储过程调用中使用的out
参数。对于InOut
参数(向过程提供IN值并且也返回值的参数),还有一个SqlInOutParameter
。
Only parameters declared as SqlParameter and SqlInOutParameter are used to provide input values. This is different from the StoredProcedure class, which (for backwards compatibility reasons) lets input values be provided for parameters declared as SqlOutParameter . |
对于IN参数,除了名称和SQL类型之外,还可以为数字数据指定小数位数或为自定义数据库类型指定类型名称。对于out
参数,您可以提供RowMapper
来处理从ref
游标返回的行的映射。另一个选项是指定一个SqlReturnType
,它提供了定义返回值的自定义处理的机会。
3.6.8. Calling a Stored Function by Using SimpleJdbcCall
调用存储函数的方式与调用存储过程的方式几乎相同,只是提供的是函数名而不是过程名。您使用with FunctionName
方法作为配置的一部分,以指示您想要调用一个函数,并为函数调用生成相应的字符串。使用专门的调用(ecuteFunction
)来运行函数,它将函数返回值作为指定类型的对象返回,这意味着您不必从结果映射中检索返回值。对于只有一个out
参数的存储过程,也可以使用类似的方便方法(名为ecuteObject
)。下面的示例(针对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
}
使用的ecuteFunction
方法返回一个字符串
,其中包含函数调用的返回值。
3.6.9. Returning a ResultSet
or REF Cursor from a SimpleJdbcCall
调用返回结果集的存储过程或函数有点棘手。一些数据库在JDBC结果处理期间返回结果集,而另一些数据库需要显式注册的特定类型的out
参数。这两种方法都需要额外的处理来循环结果集并处理返回的行。通过SimpleJdbcCall
,您可以使用reuringResultSet
方法并声明要用于特定参数的RowMapper
实现。如果在结果处理期间返回结果集,则没有定义名称,因此返回的结果必须与声明RowMapper
实现的顺序匹配。指定的名称仍用于存储从Execute
语句返回的结果映射中已处理的结果列表。
下一个示例(针对MySQL)使用了一个不带IN参数的存储过程,并返回t_act
表中的所有行:
CREATE PROCEDURE read_all_actors()
BEGIN
SELECT a.id, a.first_name, a.last_name, a.birth_date FROM t_actor a;
END;
要调用此过程,可以声明RowMapper
。因为要映射到的类遵循Java 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
}
Execute
调用传入一个空的Map
,因为此调用不带任何参数。然后从结果映射中检索参与者列表,并将其返回给调用者。
3.7. Modeling JDBC Operations as Java Objects
org.springFrawork.jdbc.Object
包包含允许您以更面向对象的方式访问数据库的类。例如,您可以运行查询并以列表的形式返回结果,该列表包含业务对象,并将关系列数据映射到业务对象的属性。您还可以运行存储过程以及运行UPDATE、DELETE和INSERT语句。
许多Spring开发人员认为,下面描述的各种RDBMS操作类( 但是,如果您从使用RDBMS操作类中获得了可测量的价值,则应该继续使用这些类。 |
3.7.1. Understanding SqlQuery
SqlQuery
是封装SQL查询的可重用线程安全类。子类必须实现newRowMapper(..)
方法以提供RowMapper
实例,该实例可以为每行创建一个对象,该对象是通过迭代在查询执行期间创建的ResultSet
获得的。SqlQuery
类很少直接使用,因为MappingSqlQuery
子类为将行映射到Java类提供了更方便的实现。扩展SqlQuery
的其他实现是MappingSqlQueryWith参数
和UpdatableSqlQuery
。
3.7.2. Using MappingSqlQuery
MappingSqlQuery
是一个可重复使用的查询,其中具体的子类必须实现抽象mapRow(..)
方法,以将提供的ResultSet
的每一行转换为指定类型的对象。下面的示例显示一个自定义查询,该查询将数据从t_act
关系映射到Actor
类的一个实例:
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;
}
}
该类扩展了使用Actor
类型参数化的MappingSqlQuery
。此客户查询的构造函数使用DataSource
作为唯一参数。在此构造函数中,您可以使用DataSource
和应该运行以检索该查询的行的SQL来调用超类上的构造函数。此SQL用于创建PreparedStatement
,因此它可以包含在执行过程中传入的任何参数的占位符。您必须使用传入SqlParameter
的decreParameter
方法来声明每个参数。SqlParameter
接受一个名称,并接受java.sql.Types
中定义的JDBC类型。在定义了所有参数之后,您可以调用Compile()
方法,以便可以准备语句并在以后运行。此类在编译后是线程安全的,因此,只要这些实例是在初始化DAO时创建的,它们就可以作为实例变量保留并重复使用。以下示例显示如何定义此类类:
private ActorMappingQuery actorMappingQuery;
@Autowired
public void setDataSource(DataSource dataSource) {
this.actorMappingQuery = new ActorMappingQuery(dataSource);
}
public Customer getCustomer(Long id) {
return actorMappingQuery.findObject(id);
}
上例中的方法使用作为唯一参数传入的id
检索客户。因为我们只想返回一个对象,所以我们使用id
作为参数来调用findObject
方便方法。如果我们有一个返回对象列表并接受其他参数的查询,我们将使用Execute
方法之一,该方法接受作为varargs传递的参数值数组。下面的示例显示了这样一种方法:
public List<Actor> searchForActors(int age, String namePattern) {
List<Actor> actors = actorSearchMappingQuery.execute(age, namePattern);
return actors;
}
3.7.3. Using SqlUpdate
SqlUpdate
类封装SQL更新。与查询一样,UPDATE对象是可重用的,并且与所有RdbmsOperation
类一样,UPDATE可以有参数,并且是在SQL中定义的。该类提供了许多类似于Query对象的Execute(..)
方法的更新(..)
方法。SqlUpdate
类是具体的。例如,它可以子类为 - ,以添加自定义更新方法。但是,您不必将SqlUpdate
类派生为子类,因为通过设置SQL和声明参数可以很容易地将其参数化。下面的示例创建名为Execute
的自定义更新方法:
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); } }
3.7.4. Using StoredProcedure
StoredProcedure
类是RDBMS存储过程的对象抽象的抽象
超类。
继承的SQL
属性是RDBMS中存储过程的名称。
若要为StoredProcedure
类定义参数,可以使用SqlParameter
或其子类之一。您必须在构造函数中指定参数名称和SQL类型,如以下代码片段所示:
new SqlParameter("in_id", Types.NUMERIC),
new SqlOutParameter("out_first_name", Types.VARCHAR),
使用java.sql.Types
常量指定SQL类型。
第一行(带有SqlParameter
)声明了一个IN参数。IN参数既可以用于存储过程调用,也可以用于使用SqlQuery
及其子类(在了解SqlQuery
中介绍)的查询。
第二行(带有SqlOutParameter
)声明了要在存储过程调用中使用的out
参数。对于InOut
参数(向过程提供in
值并且也返回值的参数),还有一个SqlInOutParameter
。
对于in
参数,除了名称和SQL类型之外,您还可以为数字数据指定小数位数或为自定义数据库类型指定类型名称。对于out
参数,您可以提供RowMapper
来处理从ref
游标返回的行的映射。另一个选项是指定一个SqlReturnType
,它允许您定义对返回值的自定义处理。
下一个简单DAO示例使用StoredProcedure
调用任何Oracle数据库附带的函数(sysdate()
)。要使用存储过程功能,您必须创建一个扩展StoredProcedure
的类。在本例中,StoredProcedure
类是一个内部类。但是,如果需要重用StoredProcedure
,则可以将其声明为顶级类。此示例没有输入参数,但使用SqlOutParameter
类将输出参数声明为Date类型。Execute()
方法运行该过程,并从结果Map
中提取返回日期。通过使用参数名作为键,结果Map
为每个声明的输出参数(在本例中只有一个)有一个条目。下面的清单显示了我们的自定义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; } } }
下面的StoredProcedure
示例有两个输出参数(在本例中为Oracle引用游标):
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>()); } }
注意已在TitlesAndGenresStoredProcedure
构造函数中使用的declreParameter(..)
方法的重载变量是如何传递给RowMapper
实现实例的。这是重用现有功能的一种非常方便和强大的方式。接下来的两个示例提供了两个RowMapper
实现的代码。
TitleMapper
类将ResultSet
映射到所提供的ResultSet
中每一行的标题
域对象,如下所示:
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; } }
GenreMapper
类将ResultSet
映射到所提供的ResultSet
中每一行的类型
域对象,如下所示:
public final class GenreMapper implements RowMapper<Genre> { public Genre mapRow(ResultSet rs, int rowNum) throws SQLException { return new Genre(rs.getString("name")); } }
要将参数传递给在RDBMS的定义中具有一个或多个输入参数的存储过程,可以编写一个强类型的Execute(..)
方法,该方法将委托给超类中的非类型化的Execute(Map)
方法,如下面的示例所示:
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); } }
3.8. Common Problems with Parameter and Data Value Handling
参数和数据值的常见问题存在于Spring框架的JDBC支持提供的不同方法中。本节介绍如何解决这些问题。
3.8.1. Providing SQL Type Information for Parameters
通常,Spring根据传入的参数类型确定参数的SQL类型。可以显式提供设置参数值时要使用的SQL类型。这有时是正确设置NULL
值所必需的。
您可以通过多种方式提供SQL类型信息:
-
JdbcTemplate
的许多更新和查询方法都以int
数组的形式接受额外的参数。该数组用于通过使用java.sql.Types
类中的常量值来指示相应参数的SQL类型。为每个参数提供一个条目。 -
您可以使用
SqlParameterValue
类包装需要此附加信息的参数值。为此,请为每个值创建一个新实例,并在构造函数中传递SQL类型和参数值。您还可以为数值提供可选的比例参数。 -
对于使用命名参数的方法,您可以使用
Sql参数源
类、BeanPropertySql参数源
或MapSql参数源
类。它们都有为任何命名参数值注册SQL类型的方法。
3.8.2. Handling BLOB and CLOB objects
您可以在数据库中存储图像、其他二进制数据和大量文本。这些大对象对于二进制数据被称为BLOB(二进制大对象),对于字符数据被称为CLOB(字符大对象)。在Spring中,您可以通过直接使用JdbcTemplate
以及使用RDBMS对象和SimpleJdbc
类提供的更高抽象来处理这些大型对象。所有这些方法都使用LobHandler
接口的实现来实际管理LOB(大对象)数据。LobHandler
通过getLobCreator
方法提供对LobCreator
类的访问,该方法用于创建要插入的新LOB对象。
LobCreator
和LobHandler
为LOB输入输出提供以下支持:
-
斑点
-
byte[]
:getBlobAsBytes
和setBlobAsBytes
-
InputStream
:getBlobAsBinaryStream
和setBlobAsBinaryStream
-
-
CLOB
-
字符串
:getClobAsString
和setClobAsString
-
InputStream
:getClobAsAsciiStream
和setClobAsAsciiStream
-
阅读器
:getClobAsCharacterStream
和setClobAsCharacterStream
-
下一个示例显示如何创建和插入BLOB。稍后,我们将展示如何从数据库中读回它。
此示例使用JdbcTemplate
和AbstractLobCreatingPreparedStatementCallback
.的实现它实现了一个方法,setValues
。此方法提供LobCreator
,我们使用它来设置SQL INSERT语句中LOB列的值。
对于本例,我们假设有一个变量lobHandler
已经设置为DefaultLobHandler
的一个实例。通常通过依赖项注入来设置此值。
下面的示例显示如何创建和插入Blob:
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 | Pass in the lobHandler that (in this example) is a plain DefaultLobHandler . |
2 | Using the method setClobAsCharacterStream to pass in the contents of the CLOB. |
3 | Using the method setBlobAsBinaryStream to pass in the contents of the BLOB. |
如果对从 请参阅有关JDBC驱动程序的文档,以验证它是否支持在不提供内容长度的情况下对LOB进行流式处理。 |
现在可以从数据库中读取LOB数据了。同样,您使用的JdbcTemplate
具有相同的实例变量lobHandler
以及对DefaultLobHandler
的引用。以下示例显示了如何执行此操作:
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 | Using the method getClobAsString to retrieve the contents of the CLOB. |
2 | Using the method getBlobAsBytes to retrieve the contents of the BLOB. |
3.8.3. Passing in Lists of Values for IN Clause
SQL标准允许基于包含变量值列表的表达式选择行。一个典型的例子是SELECT*FORM T_ACTOR WHERE id in(1,2,3)
。JDBC标准不直接支持预准备语句使用该变量列表。不能声明数量可变的占位符。您需要准备许多具有所需占位符数量的变体,或者一旦知道需要多少占位符,就需要动态生成SQL字符串。Named参数JdbcTemplate
和JdbcTemplate
中提供的命名参数支持采用后一种方法。您可以将这些值作为基本对象的java.util.List
传入。此列表用于在语句执行期间插入所需的占位符和传入值。
Be careful when passing in many values. The JDBC standard does not guarantee that you can use more than 100 values for an in expression list. Various databases exceed this number, but they usually have a hard limit for how many values are allowed. For example, Oracle’s limit is 1000. |
除了值列表中的原始值之外,您还可以创建对象数组的java.util.List
。此列表可以支持为in
子句定义多个表达式,例如SELECT*FROM T_ACTOR WHERE(id,last_name)in((1,‘Johnson’),(2,‘Harrop’))
。当然,这需要您的数据库支持此语法。
3.8.4. Handling Complex Types for Stored Procedure Calls
调用存储过程时,有时可以使用特定于数据库的复杂类型。为了适应这些类型,当它们从存储过程调用返回时,Spring提供SqlReturnType
来处理它们,并在它们作为参数传递到存储过程时提供SqlTypeValue
。
SqlReturnType
接口有一个必须实现的方法(名为getTypeValue
)。此接口用作SqlOutParameter
声明的一部分。下面的示例显示返回用户声明的类型Item_type
的OracleSTRUCT
对象的值:
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;
}));
// ...
}
您可以使用SqlTypeValue
将Java对象(如TestItem
)的值传递给存储过程。SqlTypeValue
接口有一个您必须实现的方法(名为createTypeValue
)。传入活动连接,您可以使用它来创建特定于数据库的对象,比如StructDescriptor
实例或ArrayDescriptor
实例。下面的示例创建一个StructDescriptor
实例:
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;
}
};
现在可以将这个SqlTypeValue
添加到Map
中,该映射包含存储过程的Execute
调用的输入参数。
SqlTypeValue
的另一个用途是将值的数组传递给Oracle存储过程。Oracle有自己的内部数组
类,在这种情况下必须使用它,您可以使用SqlTypeValue
创建Oracle数组
的实例,并用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;
}
};
3.9. Embedded Database Support
org.springframework.jdbc.datasource.embedded
包提供了对嵌入式Java数据库引擎的支持。本地提供了对HSQL、h2和Derby的支持。您还可以使用可扩展的API来插入新的嵌入式数据库类型和DataSource
实现。
3.9.1. Why Use an Embedded Database?
嵌入式数据库在项目的开发阶段非常有用,因为它具有轻量级的特性。好处包括易于配置、快速启动、可测试性以及在开发期间快速发展您的SQL的能力。
3.9.2. Creating an Embedded Database by Using Spring XML
如果您希望在SpringApplicationContext
中将嵌入式数据库实例公开为一个Bean,则可以在Spring-jdbc
命名空间中使用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数据库,该数据库使用类路径根目录中的schema.sql
和test-data.sql
资源中的SQL填充。此外,最佳做法是为嵌入式数据库分配一个唯一生成的名称。嵌入的数据库作为类型的Bean提供给Spring容器,然后可以根据需要将其注入到数据访问对象中。
3.9.3. Creating an Embedded Database Programmatically
EmbeddedDatabaseBuilder
类为以编程方式构建嵌入式数据库提供了流畅的API。当您需要在独立环境或独立集成测试中创建嵌入式数据库时,可以使用它,如下例所示:
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()
有关所有支持的选项的详细信息,请参阅javadoc forEmbeddedDatabaseBuilder
。
您还可以使用EmbeddedDatabaseBuilder
通过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();
}
}
3.9.4. Selecting the Embedded Database Type
本节介绍如何从Spring支持的三个嵌入式数据库中选择一个。它包括以下主题:
Using HSQL
Spring支持HSQL 1.8.0及以上版本。如果没有明确指定类型,则HSQL是默认的嵌入式数据库。要显式指定HSQL,请将嵌入式数据库
标记的type
属性设置为HSQL
。如果您使用的是构建器API,请使用EmbeddedDatabaseType.HSQL
调用setType(EmbeddedDatabaseType)
方法。
3.9.5. Testing Data Access Logic with an Embedded Database
嵌入式数据库提供了一种测试数据访问代码的轻量级方法。下一个示例是使用嵌入式数据库的数据访问集成测试模板。当嵌入式数据库不需要跨测试类重用时,使用这样的模板对于一次性很有用。但是,如果您希望创建在测试套件中共享的嵌入式数据库,请考虑使用Spring TestContext框架,并按照使用Spring XML创建嵌入式数据库和以编程方式创建嵌入式数据库中所述,在SpringApplicationContext
中将嵌入式数据库配置为Bean。下面的清单显示了测试模板:
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();
}
}
3.9.6. Generating Unique Names for Embedded Databases
如果开发团队的测试套件无意中尝试重新创建同一数据库的其他实例,则开发团队经常会遇到嵌入式数据库的错误。如果一个xml配置文件或@Configuration
类负责创建嵌入式数据库,然后在同一个测试套件(即,在同一个 - 进程内)的多个测试场景中重用相应的配置,则这很容易发生。例如,针对嵌入式数据库的集成测试,其ApplicationContext
配置仅在活动的Bean定义概要文件方面存在差异。
此类错误的根本原因是,如果没有另外指定,Spring的EmbeddedDatabaseFactory
(由<;jdbc:Embedded-DATABASE>;
XML命名空间元素和EmbeddedDatabaseBuilder
在内部使用)将嵌入式数据库的名称设置为testdb
。对于<;jdbc:Embedded-DATABASE&>
,嵌入式数据库通常被分配一个与Bean的id
相同的名称(通常类似于DataSource
)。因此,创建嵌入式数据库的后续尝试不会产生新的数据库。相反,相同的JDBC连接URL被重用,并且创建新的嵌入式数据库的尝试实际上指向从相同配置创建的现有嵌入式数据库。
为了解决这个常见问题,Spring Framework4.2支持为嵌入式数据库生成唯一名称。要启用生成的名称,请使用以下选项之一。
-
EmbeddedDatabaseFactory.setGenerateUniqueDatabaseName()
-
EmbeddedDatabaseBuilder.generateUniqueName()
-
<;jdbc:嵌入式数据库生成名称=“True”…>;
3.9.7. Extending the Embedded Database Support
您可以通过两种方式扩展Spring JDBC嵌入式数据库支持:
-
实现
EmbeddedDatabaseConfigurer
以支持新的嵌入式数据库类型。 -
实现
DataSourceFactory
以支持新的DataSource
实现,例如用于管理嵌入式数据库连接的连接池。
我们鼓励您在GitHub Issues上为Spring社区贡献扩展。
3.10. Initializing a DataSource
org.springframework.jdbc.datasource.init
包为初始化现有<代码>数据源
提供支持。嵌入式数据库支持为应用程序创建和初始化数据源
提供了一个选项。但是,您有时可能需要初始化在某个服务器上运行的实例。
3.10.1. Initializing a Database by Using Spring XML
如果您希望初始化数据库,并且可以提供对DataSource
Bean的引用,则可以在Spring-jdbc
命名空间中使用初始化-数据库
标记:
<jdbc:initialize-database data-source="dataSource">
<jdbc:script location="classpath:com/foo/sql/db-schema.sql"/>
<jdbc:script location="classpath:com/foo/sql/db-test-data.sql"/>
</jdbc:initialize-database>
前面的示例针对数据库运行两个指定的脚本。第一个脚本创建一个模式,第二个脚本用测试数据集填充表。脚本位置也可以是带有通配符的模式,其通配符采用用于Spring中的资源的常见Ant样式(例如,classpath*:/com/foo/**/sql/*-data.sql
).如果使用模式,脚本将按照其URL或文件名的词汇顺序运行。
数据库初始值设定项的默认行为是无条件运行所提供的脚本。例如,如果您对已有测试数据的数据库运行脚本,这可能并不总是您想要的 - 。通过遵循先创建表,然后插入数据的常见模式(如前所述),可以降低意外删除数据的可能性。如果表已经存在,则第一步失败。
但是,为了更好地控制现有数据的创建和删除,XML命名空间提供了一些附加选项。第一个是打开和关闭初始化的标志。您可以根据环境进行设置(例如从系统属性或环境Bean中提取布尔值)。下面的示例从系统属性中获取一个值:
<jdbc:initialize-database data-source="dataSource" enabled="#{systemProperties.INITIALIZE_DATABASE}"> (1)
<jdbc:script location="..."/>
</jdbc:initialize-database>
1 | Get the value for enabled from a system property called INITIALIZE_DATABASE . |
控制现有数据的第二个选项是更好地容忍故障。为此,您可以控制初始化器忽略其从脚本运行的SQL中的某些错误的能力,如下面的示例所示:
<jdbc:initialize-database data-source="dataSource" ignore-failures="DROPS">
<jdbc:script location="..."/>
</jdbc:initialize-database>
在前面的示例中,我们说我们预计脚本有时会在空数据库上运行,因此脚本中有一些Drop
语句会失败。因此失败的SQLDROP
语句将被忽略,但其他失败将导致异常。如果您的SQL方言不支持DROP…,这将非常有用如果存在
(或类似代码),但您希望在重新创建它之前无条件地删除所有测试数据,则为代码。在这种情况下,第一个脚本通常是一组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 | Set the separator scripts to @@ . |
2 | Set the separator for db-schema.sql to ; . |
在本例中,两个测试数据
脚本使用@@
作为语句分隔符,只有db-schema.sql
使用;
。此配置指定默认分隔符为@@
,并覆盖db-schema
脚本的默认分隔符。
如果您需要比从XML命名空间获得的更多的控制,您可以直接使用DataSourceInitializer
并将其定义为应用程序中的组件。
Initialization of Other Components that Depend on the Database
一大类应用程序(那些直到Spring上下文启动后才使用数据库的应用程序)可以使用数据库初始化器,而不会进一步复杂化。如果您的应用程序不是其中之一,则可能需要阅读本节的其余部分。
数据库初始化器依赖于DataSource
实例并运行其初始化回调中提供的脚本(类似于XML Bean定义中的init-method
、组件中的@PostConstruct
方法或实现InitializingBean
的组件中的After PropertiesSet()
方法)。如果其他Bean依赖于相同的数据源并在初始化回调中使用该数据源,则可能会出现问题,因为数据尚未初始化。这方面的一个常见示例是在应用程序启动时立即初始化并从数据库加载数据的缓存。
要解决此问题,您有两种选择:将缓存初始化策略更改为稍后阶段,或者确保首先初始化数据库初始化器。
如果应用程序在您的控制之下,更改缓存初始化策略可能很容易,而不是在其他情况下。关于如何实施这一点的一些建议包括:
-
使缓存在第一次使用时延迟初始化,从而缩短应用程序启动时间。
-
让您的缓存或初始化缓存的单独组件实现
生命周期
或SmartLifeccle
。当应用程序上下文启动时,您可以通过设置其自动启动
标志来自动启动SmartLifeccle
,并且可以通过在所包含的上下文上调用ConfigurableApplicationContext.start()
来手动启动生命周期
。 -
使用Spring
ApplicationEvent
或类似的自定义观察器机制来触发缓存初始化。ContextRereshedEvent
总是在准备好使用时(在所有Bean都已初始化之后)由上下文发布,因此这通常是一个有用的挂钩(这是SmartLifeccle
默认情况下的工作方式)。
确保首先初始化数据库初始值设定项也很容易。关于如何实施这一点的一些建议包括:
-
依赖于Spring
BeanFactory
的默认行为,即Bean按注册顺序初始化。通过采用XML配置中对应用程序模块进行排序的一组<;导入/
元素的常见实践,并确保首先列出数据库和数据库初始化,可以很容易地安排这一点。 -
分离
DataSource
和使用它的业务组件,并通过将它们放在单独的ApplicationContext
实例中来控制它们的启动顺序(例如,父上下文包含DataSource
,子上下文包含业务组件)。这种结构在Spring web应用程序中很常见,但可以更普遍地应用。
4. Data Access with R2DBC
R2DBC(“反应式关系数据库连接”)是社区驱动的规范工作,旨在使用反应式模式标准化对SQL数据库的访问。
4.1. Package Hierarchy
Spring框架的R2DBC抽象框架由两个不同的包组成:
-
core
:org.springFrawork.r2dbc.core
包包含DatabaseClient
类以及各种相关类。请参阅使用R2DBC核心类控制基本的R2DBC处理和错误处理。 -
Connection
:org.springframework.r2dbc.connection
包包含一个实用程序类,用于轻松访问ConnectionFactory
和各种简单的ConnectionFactory
实现,可用于测试和运行未经修改的R2DBC。请参阅控制数据库连接。
4.2. Using the R2DBC Core Classes to Control Basic R2DBC Processing and Error Handling
本节介绍如何使用R2DBC核心类来控制基本的R2DBC处理,包括错误处理。它包括以下主题:
4.2.1. Using DatabaseClient
DatabaseClient
是R2DBC核心包的核心类。它处理资源的创建和释放,这有助于避免常见错误,如忘记关闭连接。它执行核心R2DBC工作流的基本任务(如语句创建和执行),让应用程序代码提供SQL和提取结果。DatabaseClient
类:
-
运行SQL查询
-
UPDATE语句和存储过程调用
-
对
结果
实例执行迭代 -
捕获R2DBC异常并将其转换为
org.springFrawork.ao
包中定义的通用的、信息量更大的异常层次结构。(请参阅一致异常层次结构。)
客户端有一个功能强大、流畅的API,它使用反应类型进行声明性组合。
当您为您的代码使用DatabaseClient
时,您只需要实现java.util.unction
接口,为它们提供一个明确定义的约定。给定由DatabaseClient
类提供的连接
,函数
回调将创建发布者
。对于提取行
结果的映射函数也是如此。
您可以通过使用ConnectionFactory
引用的直接实例化在DAO实现中使用DatabaseClient
,也可以在Spring IOC容器中配置它并将其作为Bean引用提供给DAO。
创建DatabaseClient
对象的最简单方法是通过静态工厂方法,如下所示:
DatabaseClient client = DatabaseClient.create(connectionFactory);
The ConnectionFactory should always be configured as a bean in the Spring IoC container. |
前面的方法使用默认设置创建DatabaseClient
。
您也可以从DatabaseClient.Builder()
获取Builder
实例。您可以通过调用以下方法来自定义客户端:
-
<代码>….bindMarkers(…):提供特定的
BindMarkersFactory
来配置命名参数以进行数据库绑定标记转换。 -
<代码>….ecuteFunction(…):设置
ExecuteFunction
语句
对象的运行方式。 -
<代码>….named参数(FALSE):禁用命名参数扩展。默认情况下启用。
Dialects are resolved by BindMarkersFactoryResolver from a ConnectionFactory , typically by inspecting ConnectionFactoryMetadata . You can let Spring auto-discover your BindMarkersFactory by registering a class that implements org.springframework.r2dbc.core.binding.BindMarkersFactoryResolver$BindMarkerFactoryProvider through META-INF/spring.factories . BindMarkersFactoryResolver discovers bind marker provider implementations from the class path using Spring’s SpringFactoriesLoader . |
当前支持的数据库包括:
-
氢
-
马里亚布
-
Microsoft SQL Server
-
MySQL
-
波斯格雷斯
此类发出的所有SQL都记录在与客户端实例的完全限定类名对应的类别下的调试
级别(通常为DefaultDatabaseClient
)。此外,每次执行都会在反应序列中注册一个检查点,以帮助调试。
以下各节提供了一些DatabaseClient
用法的示例。这些示例并不是DatabaseClient
公开的所有功能的详尽列表。有关这一点,请参阅服务员javadoc。
Executing Statements
DatabaseClient
提供运行语句的基本功能。下面的示例显示了创建新表的最少但功能齐全的代码需要包括的内容:
Mono<Void> completion = client.sql("CREATE TABLE person (id VARCHAR(255) PRIMARY KEY, name VARCHAR(255), age INTEGER);")
.then();
DatabaseClient
旨在方便、流畅地使用。它在执行规范的每个阶段公开中间、延续和终止方法。上面的示例使用Then()
返回一个完成发布器
,该完成在查询(如果SQL查询包含多个语句)完成时立即完成。
execute(…) accepts either the SQL query string or a query Supplier<String> to defer the actual query creation until execution. |
Querying (SELECT
)
SQL查询可以通过Row
对象或受影响的行数返回值。DatabaseClient
可以返回更新后的行数或行本身,具体取决于发出的查询。
以下查询从表中获取id
和名称
列:
Mono<Map<String, Object>> first = client.sql("SELECT id, name FROM person")
.fetch().first();
以下查询使用绑定变量:
Mono<Map<String, Object>> first = client.sql("SELECT id, name FROM person WHERE first_name = :fn")
.bind("fn", "Joe")
.fetch().first();
您可能已经注意到在上面的示例中使用了fetch()
。fetch()
是一个连续运算符,它允许您指定要使用的数据量。
调用first()
返回结果中的第一行,并丢弃其余行。您可以通过以下运算符使用数据:
-
first()
返回整个结果的第一行。对于不可为空的返回值,它的kotlin Coroutine变量命名为waitSingle()
,如果值是可选的,则命名为waitSingleOrNull()
。 -
one()
只返回一个结果,如果结果包含更多行,则返回失败。使用Kotlin协程,waitOne()
表示只有一个值,或者waitOneOrNull()
如果该值可以是NULL
。 -
all()
返回结果的所有行。当使用Kotlin协程时,请使用flow()
。 -
<(
INSERT
/UPDATE
/DELETE
>rowsUpated()返回受影响的行数(计数)。它的Kotlin Coroutine变体被命名为waitRowsUpated()
。
在不指定更多映射详细信息的情况下,查询以Map
的形式返回表格结果,其键是映射到其列值的不区分大小写的列名。
您可以通过提供一个函数<;Row,T&>;
控制结果映射,该函数为每个行
调用,以便它可以返回任意值(单值、集合和映射以及对象)。
下面的示例提取name
列并发出其值:
Flux<String> names = client.sql("SELECT name FROM person")
.map(row -> row.get("name", String.class))
.all();
Updating (INSERT
, UPDATE
, and DELETE
) with DatabaseClient
修改语句的唯一区别是这些语句通常不返回表格数据,因此您使用rowsUpated()
来使用结果。
以下示例显示返回更新行数的UPDATE
语句:
Mono<Integer> affectedRows = client.sql("UPDATE person SET first_name = :fn")
.bind("fn", "Joe")
.fetch().rowsUpdated();
Binding Values to Queries
典型的应用程序需要参数化的SQL语句来根据某些输入选择或更新行。这些通常是受WHERE
子句约束的SELECT
语句或接受输入参数的INSERT
和UPDATE
语句。如果参数未正确转义,则参数化语句将承担SQL注入的风险。DatabaseClient
利用R2DBC的Bind
接口消除查询参数的SQL注入风险。您可以使用<代码>Execute(…)提供一个参数化的SQL语句运算符并将参数绑定到实际的语句
。然后,您的R2DBC驱动程序通过使用准备好的语句和参数替换来运行该语句。
参数绑定支持两种绑定策略:
-
按索引,使用从零开始的参数索引。
-
按名称,使用占位符名称。
以下示例显示查询的参数绑定:
db.sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)")
.bind("id", "joe")
.bind("name", "Joe")
.bind("age", 34);
查询预处理器将命名的集合
参数展开为一系列绑定标记,以消除基于参数数量动态创建查询的需要。嵌套的对象数组被扩展以允许使用(例如)选择列表。
请考虑以下查询:
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);
Usage of select lists is vendor-dependent. |
下面的示例显示了使用IN
谓词的更简单的变体:
client.sql("SELECT id, name, state FROM table WHERE age IN (:ages)")
.bind("ages", Arrays.asList(35, 50));
R2DBC itself does not support Collection-like values. Nevertheless, expanding a given List in the example above works for named parameters in Spring’s R2DBC support, e.g. for use in IN clauses as shown above. However, inserting or updating array-typed columns (e.g. in Postgres) requires an array type that is supported by the underlying R2DBC driver: typically a Java array, e.g. String[] to update a text[] column. Do not pass Collection<String> or the like as an array parameter. |
Statement Filters
有时,您需要在实际语句
运行之前对其选项进行微调。通过DatabaseClient
注册一个语句
过滤器(StatementFilterFunction
),在语句执行过程中对其进行拦截和修改,如下例所示:
client.sql("INSERT INTO table (name, state) VALUES(:name, :state)")
.filter((s, next) -> next.execute(s.returnGeneratedValues("id")))
.bind("name", …)
.bind("state", …);
数据库客户端
还公开简化的筛选器(…)
重载接受函数<;语句,>;
:
client.sql("INSERT INTO table (name, state) VALUES(:name, :state)")
.filter(statement -> s.returnGeneratedValues("id"));
client.sql("SELECT id, name, state FROM table")
.filter(statement -> s.fetchSize(25));
StatementFilterFunction
实现允许筛选语句
和筛选结果
对象。
DatabaseClient
Best Practices
配置后,DatabaseClient
类的实例是线程安全的。这一点很重要,因为它意味着您可以配置DatabaseClient
的单个实例,然后安全地将该共享引用注入多个DAO(或存储库)。DatabaseClient
是有状态的,因为它维护对ConnectionFactory
的引用,但此状态不是对话状态。
使用DatabaseClient
类时的一种常见做法是在您的Spring配置文件中配置一个ConnectionFactory
,然后依赖注入共享的ConnectionFactory
Bean到您的DAO类中。DatabaseClient
是在ConnectionFactory
的setter中创建的。这将导致类似以下内容的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...
}
1 | Annotate the class with @Component . |
2 | Annotate the ConnectionFactory setter method with @Autowired . |
3 | Create a new DatabaseClient with the ConnectionFactory . |
显式配置的另一种选择是使用组件扫描和注释支持进行依赖注入。在本例中,您可以使用@Component
来注释该类(这使它成为组件扫描的候选对象),并使用@AuTower
来注释ConnectionFactory
setter方法。以下示例显示了如何执行此操作:
@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 | Annotate the class with @Component . |
2 | Constructor injection of the ConnectionFactory . |
3 | Create a new DatabaseClient with the ConnectionFactory . |
无论您选择使用(或不使用)上述哪种模板初始化样式,很少需要在每次希望运行SQL时创建DatabaseClient
类的新实例。配置好后,DatabaseClient
实例就是线程安全的。如果您的应用程序访问多个数据库,您可能需要多个DatabaseClient
实例,这需要多个ConnectionFactory
以及随后多个配置不同的DatabaseClient
实例。
4.3. Retrieving Auto-generated Keys
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
4.4. Controlling Database Connections
本部分内容包括:
-
使用
TransactionAwareConnectionFactoryProxy
4.4.1. Using ConnectionFactory
Spring通过ConnectionFactory
获得到数据库的R2DBC连接。ConnectionFactory
是R2DBC规范的一部分,是驱动程序的公共入口点。它允许容器或框架对应用程序代码隐藏连接池和事务管理问题。作为开发人员,您不需要了解有关如何连接到数据库的详细信息。这是设置ConnectionFactory
的管理员的责任。在开发和测试代码时,您很可能同时担任这两个角色,但您不必知道生产数据源是如何配置的。
当您使用Spring的R2DBC层时,您可以使用第三方提供的连接池实现来配置您自己的R2DBC层。一个流行的实现是R2DBC Pool(r2dbc-pool
)。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");
4.4.2. Using ConnectionFactoryUtils
ConnectionFactoryUtils
类是一个方便且功能强大的帮助器类,它提供了静态
方法以从ConnectionFactory
获取连接并关闭连接(如果需要)。
它支持订阅者上下文
绑定的连接,例如R2dbcTransactionManager
。
4.4.3. Using SingleConnectionFactory
SingleConnectionFactory
类是DelegatingConnectionFactory
接口的实现,该接口包装单个连接
,该连接在每次使用后都不会关闭。
如果任何客户端代码在假定池连接的情况下调用Close
(如使用持久性工具时),则应将suppressClose
属性设置为true
。此设置返回包装物理连接的关闭抑制代理。请注意,您不能再将其强制转换为本机连接
或类似的对象。
SingleConnectionFactory
主要是一个测试类,如果您的R2DBC驱动程序允许使用管道等特定要求,则可以使用它。与池化ConnectionFactory
相比,它始终重复使用相同的连接,避免过度创建物理连接。
4.4.4. Using TransactionAwareConnectionFactoryProxy
TransactionAwareConnectionFactoryProxy
是目标<代码>连接工厂
的代理。代理包装目标ConnectionFactory
以增加对Spring管理事务的感知。
Using this class is required if you use a R2DBC client that is not integrated otherwise with Spring’s R2DBC support. In this case, you can still use this client and, at the same time, have this client participating in Spring managed transactions. It is generally preferable to integrate a R2DBC client with proper access to ConnectionFactoryUtils for resource management. |
有关更多详细信息,请参阅TransactionAwareConnectionFactoryProxy
javadoc。
4.4.5. Using R2dbcTransactionManager
R2dbcTransactionManager
类是单个R2DBC数据源的Reactive TransactionManager
实现。它将来自指定连接工厂的R2DBC连接绑定到订阅者上下文
,这可能允许每个连接工厂有一个订阅者连接。
通过ConnectionFactoryUtils.getConnection(ConnectionFactory)
,而不是R2DBC的标准ConnectionFactory.create()
检索R2DBC连接需要应用程序代码。
所有框架类(如DatabaseClient
)都隐式使用此策略。如果不与此事务管理器一起使用,则查找策略的行为与常见策略完全相同。因此,它可以在任何情况下使用。
R2dbcTransactionManager
类支持应用于连接的自定义隔离级别。
5. Object Relational Mapping (ORM) Data Access
本节介绍使用对象关系映射(ORM)时的数据访问。
5.1. Introduction to ORM with Spring
Spring框架支持与Java持久性API(JPA)的集成,并支持用于资源管理、数据访问对象(DAO)实现和事务策略的本机Hibernate。例如,对于Hibernate,有几个方便的IoC特性提供了一流的支持,这些特性解决了许多典型的Hibernate集成问题。您可以通过依赖注入为OR(对象关系)映射工具配置所有受支持的功能。它们可以参与Spring的资源和事务管理,并且遵循Spring的泛型事务和DAO异常层次结构。推荐的集成风格是针对普通Hibernate或JPAAPI对DAO进行编码。
在创建数据访问应用程序时,Spring为您选择的ORM层添加了显著的增强功能。您可以根据需要利用尽可能多的集成支持,并且应该将此集成工作与在内部构建类似基础设施的成本和风险进行比较。无论采用何种技术,您都可以像使用库一样使用大部分ORM支持,因为一切都被设计为一组可重用的JavaBean。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的相关代码通常需要使用相同的Hibernate会话
,以确保效率和正确的事务处理。通过HibernateSessionFactory
公开当前的会话
,Spring可以方便地透明地创建会话
并将其绑定到当前线程。因此,对于任何本地或JTA事务环境,Spring都解决了典型Hibernate使用的许多长期问题。 -
集成事务管理。您可以通过
@Transaction
注释或通过在XML配置文件中显式配置事务AOP建议,使用声明性的、面向方面的编程(AOP)样式的方法拦截器包装ORM代码。在这两种情况下,都会为您处理事务语义和异常处理(回滚等)。正如资源和事务管理中所讨论的,您还可以交换各种事务管理器,而不会影响与ORM相关的代码。例如,您可以在本地事务和JTA之间进行交换,并在这两种场景中使用相同的完整服务(如声明性事务)。此外,与JDBC相关的代码可以与您用来执行ORM的代码以事务方式完全集成。这对于不适合ORM的数据访问(如批处理和BLOB流)很有用,但仍需要与ORM操作共享公共事务。
For more comprehensive ORM support, including support for alternative database technologies such as MongoDB, you might want to check out the Spring Data suite of projects. If you are a JPA user, the Getting Started Accessing Data with JPA guide from https://spring.io provides a great introduction. |
5.2. General ORM Integration Considerations
本节重点介绍适用于所有ORM技术的注意事项。Hibernate部分提供了更多详细信息,并在具体的上下文中展示了这些功能和配置。
SpringORM集成的主要目标是明确的应用程序分层(使用任何数据访问和事务技术),以及应用程序对象 - 的松散耦合,不再依赖于数据访问或事务策略的业务服务,不再有硬编码的资源查找,不再有难以替换的单例,不再有定制服务注册中心。我们的目标是使用一种简单且一致的方法来连接应用程序对象,使其尽可能地保持可重用性并摆脱容器依赖。所有单独的数据访问功能本身都是可用的,但它们与Spring的应用程序上下文概念很好地集成在一起,提供了基于XML的配置和对不需要支持Spring的普通JavaBean实例的交叉引用。在典型的Spring应用程序中,许多重要的对象都是JavaBean:数据访问模板、数据访问对象、事务管理器、使用数据访问对象的业务服务和事务管理器、Web视图解析器、使用业务服务的Web控制器,等等。
5.2.1. Resource and Transaction Management
典型的业务应用程序中充斥着重复的资源管理代码。许多项目试图发明自己的解决方案,有时为了编程方便而牺牲了对故障的正确处理。Spring提倡使用简单的解决方案来正确处理资源,即通过JDBC中的模板实现IOC,并为ORM技术应用AOP拦截器。
基础设施提供适当的资源处理和特定API异常到未检查的基础设施异常层次结构的适当转换。Spring引入了DAO异常层次结构,适用于任何数据访问策略。对于直接JDBC,部分中提到的JdbcTemplate
类提供了连接处理和SQLException
到DataAccessException
层次结构的正确转换,包括将特定于数据库的SQL错误代码转换为有意义的异常类。对于ORM技术,请参阅下节,了解如何获得相同的异常转换好处。
在事务管理方面,JdbcTemplate
类与Spring事务支持挂钩,并通过各自的Spring事务管理器同时支持JTA和JDBC事务。对于受支持的ORM技术,Spring通过Hibernate和JPA事务管理器以及JTA支持来提供Hibernate和JPA支持。有关事务支持的详细信息,请参阅事务管理章节。
5.2.2. Exception Translation
当您在DAO中使用Hibernate或JPA时,必须决定如何处理持久性技术的本机异常类。DAO抛出HibernateException
或PersistenceException
的子类,具体取决于技术。这些异常都是运行时异常,不需要声明或捕获。您可能还必须处理IllegalArgumentException
和IllegalStateException
。这意味着调用者只能将异常视为通常是致命的,除非他们想要依赖持久性技术自己的异常结构。如果不将调用方与实现策略捆绑在一起,则无法捕获特定原因(如乐观锁定失败)。对于强烈基于ORM的应用程序或不需要任何特殊异常处理(或两者兼而有之)的应用程序,这种权衡可能是可以接受的。然而,Spring允许通过@Repository
注释透明地应用异常转换。以下示例(一个用于Java配置,一个用于XML配置)显示了如何执行此操作:
@Repository
public class ProductDaoImpl implements 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环境中的Hibernate5开始,用它来演示Spring集成OR映射器的方法。本节详细介绍了许多问题,并展示了DAO实现和事务划分的不同变体。这些模式中的大多数可以直接转换为所有其他受支持的ORM工具。本章后面的小节将介绍其他ORM技术,并给出简单的示例。
As of Spring Framework 5.3, Spring requires Hibernate ORM 5.2+ for Spring’s HibernateJpaVendorAdapter as well as for a native Hibernate SessionFactory setup. It is strongly recommended to go with Hibernate ORM 5.4 for a newly started application. For use with HibernateJpaVendorAdapter , Hibernate Search needs to be upgraded to 5.11.6. |
5.3.1. SessionFactory
Setup in a Spring Container
为了避免将应用程序对象绑定到硬编码的资源查找,您可以在Spring容器中将资源(如JDBCDataSource
或HibernateSessionFactory
)定义为Bean。需要访问资源的应用程序对象通过Bean引用接收对此类预定义实例的引用,如下节中的DAO定义所示。
以下摘录自一个XML应用程序上下文定义,展示了如何在其上设置一个JDBCDataSource
和一个HibernateSessionFactory
:
<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 DBCPBasicDataSource
切换到位于JNDI的DataSource
(通常由应用程序服务器管理)只是一个配置问题,如下例所示:
<beans>
<jee:jndi-lookup id="myDataSource" jndi-name="java:comp/env/jdbc/myds"/>
</beans>
您还可以访问位于JNDI的SessionFactory
,使用Spring的JndiObjectFactoryBean
/<;jee:jndi-lookup&>
来检索和公开它。但是,在EJB上下文之外,这通常并不常见。
Spring还提供了一个
从Spring Framework5.1开始,这样的本机Hibernate设置还可以在本机Hibernate访问旁边公开JPA |
5.3.2. Implementing DAOs Based on the Plain Hibernate API
Hibernate有一个称为上下文会话的功能,其中Hibernate自己管理每个事务的一个当前会话
。这大致相当于Spring在每个事务中同步一个Hibernate会话
。一个对应的DAO实现类似于下面的示例,它基于普通的Hibernate API:
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();
}
}
除了在实例变量中保存SessionFactory
之外,该样式类似于Hibernate参考文档和示例的样式。我们强烈推荐这种基于实例的设置,而不是Hibernate的CaveatEmptor样例应用程序中的老式静态
HibernateUtil
类。(通常,除非绝对必要,否则不要在静态
变量中保留任何资源。)
前面的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、不需要任何特殊异常处理或两者兼而有之的应用程序,这种权衡可能是可以接受的。
幸运的是,SpringLocalSessionFactoryBean支持任何Spring事务策略的
SessionFactory.getCurrentSession()
方法,返回当前由Spring管理的事务性会话
,即使使用HibernateTransactionManager
也是如此。该方法的标准行为仍然是返回与正在进行的JTA事务关联的当前会话
(如果有的话)。无论您使用的是Spring的JtaTransactionManager
、EJB容器管理事务(CMT)还是JTA,这种行为都适用。
总而言之,您可以基于普通的Hibernate API实现DAOS,同时仍然能够参与Spring管理的事务。
5.3.3. Declarative Transaction Demarcation
我们建议您使用Spring的声明性事务支持,它允许您将Java代码中的显式事务划分API调用替换为AOP事务拦截器。您可以通过使用Java注释或XML在Spring容器中配置该事务拦截器。这种声明性事务功能使您可以使业务服务远离重复的事务划分代码,并专注于添加业务逻辑,这才是您的应用程序的真正价值所在。
Before you continue, we are strongly encourage you to read Declarative Transaction Management if you have not already done so. |
您可以使用@Transaction
注释来注释服务层,并指示Spring容器查找这些注释并为这些带注释的方法提供事务语义。以下示例显示了如何执行此操作:
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();
}
}
在容器中,您需要设置PlatformTransactionManager
实现(作为一个Bean)和<;tx:Annotation-Driven/>;
条目,并在运行时选择@Transaction
处理。以下示例显示了如何执行此操作:
<?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. Programmatic Transaction Demarcation
您可以在跨越任意数量的操作的较低级别的数据访问服务之上,在较高级别的应用程序中划分事务。对周围业务服务的实现也不存在限制。它只需要一个SpringPlatformTransactionManager
。同样,后者可以来自任何地方,但最好是通过setTransactionManager(..)
方法作为Bean引用。此外,ductDAO
应该由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>
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...
}
});
}
}
Spring的TransactionInterceptor
允许使用回调代码抛出任何已检查的应用程序异常,而TransactionTemplate
仅限于回调中的未检查异常。TransactionTemplate
在发生未检查的应用程序异常或事务被应用程序标记为仅回滚(通过设置TransactionStatus
)的情况下触发回滚。默认情况下,TransactionInterceptor
的行为方式相同,但允许每个方法使用可配置的回滚策略。
5.3.5. Transaction Management Strategies
TransactionTemplate
和TransactionInterceptor
都将实际事务处理委托给PlatformTransactionManager
实例(可以是HibernateTransactionManager
(对于单个HibernateSessionFactory
),方法是在幕后使用ThreadLocal
会话
)或JtaTransactionManager
(委托给容器的JTA子系统)。您甚至可以使用定制的PlatformTransactionManager
实现。从本机Hibernate事务管理切换到JTA(例如,当您的应用程序的某些部署面临分布式事务需求时)只是一个配置问题。您可以用Spring的JTA事务实现替换Hibernate事务管理器。事务划分和数据访问代码都无需更改即可工作,因为它们使用通用事务管理API。
对于跨多个Hibernate会话工厂的分布式事务,您可以将JtaTransactionManager
组合为具有多个LocalSessionFactoryBean
定义的事务策略。然后,每个DAO都有一个特定的SessionFactory
引用传递到其相应的Bean属性。如果所有底层JDBC数据源都是事务性容器数据源,只要使用JtaTransactionManager
作为策略,业务服务就可以跨任意数量的DAO和任意数量的会话工厂划分事务,而无需特别注意。
HibernateTransactionManager
和JtaTransactionManager
都允许使用Hibernate进行适当的JVM级缓存处理,而无需特定于容器的事务管理器查找或JCA连接器(如果您不使用EJB来启动事务)。
HibernateTransactionManager
可以将休眠JDBC连接
导出到特定数据源
的纯JDBC访问代码。如果您只访问一个数据库,这种能力允许在完全不使用JTA的情况下使用Hibernate和JDBC混合数据访问进行高级事务划分。如果您通过LocalSessionFactoryBean
类的DataSource
属性设置了传入的SessionFactory
,HibernateTransactionManager
会自动将Hibernate事务公开为JDBC事务。或者,您可以通过HibernateTransactionManager
类的DataSource
属性显式指定事务应该公开的DataSource
。
5.3.6. Comparing Container-managed and Locally Defined Resources
您可以在容器管理的JNDISessionFactory
和本地定义的JNDI之间切换,而无需更改一行应用程序代码。是将资源定义保留在容器中还是本地保留在应用程序中,这主要取决于您使用的事务策略。与Spring定义的本地SessionFactory
相比,手动注册的JNDISessionFactory
没有任何好处。通过Hibernate的JCA连接器部署SessionFactory
提供了参与Jakarta EE服务器的管理基础设施的附加值,但不会增加更多的实际价值。
Spring的事务支持不绑定到容器。当使用除JTA之外的任何策略进行配置时,事务支持也可以在独立或测试环境中工作。特别是在单数据库事务的典型情况下,Spring的单资源本地事务支持是JTA的一个轻量级而强大的替代方案。当您使用本地EJB无状态会话Bean来驱动事务时,您同时依赖于EJB容器和JTA,即使您只访问单个数据库并且仅使用无状态会话Bean通过容器管理的事务来提供声明性事务。以编程方式直接使用JTA还需要一个Jakarta EE环境。
Spring驱动的事务可以使用本地定义的HibernateSessionFactory
,就像它们使用本地JDBCDataSource
一样,前提是它们访问单个数据库。因此,当您有分布式事务需求时,只需要使用Spring的JTA事务策略。JCA连接器首先需要特定于容器的部署步骤,而且(显然)需要JCA支持。与使用本地资源定义和Spring驱动的事务部署简单的Web应用程序相比,这种配置需要更多的工作。
考虑所有因素,如果您不使用EJB,请坚持使用本地SessionFactory
安装程序和Spring的HibernateTransactionManager
或JtaTransactionManager
。您可以获得所有好处,包括适当的事务性JVM级缓存和分布式事务,而不需要部署容器。通过JCA连接器对HibernateSessionFactory
的JNDI注册只有在与EJB结合使用时才会增加价值。
5.3.7. Spurious Application Server Warnings with Hibernate
在一些具有非常严格的XADataSource
实现的JTA环境中(当前的一些WebLogic Server和WebSphere版),如果在不考虑该环境的JTA事务管理器的情况下配置Hibernate,应用程序服务器日志中可能会出现虚假警告或异常。这些警告或异常表示正在访问的连接不再有效或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设置。最简单的方法是引用LocalSessionFactoryBean
Bean的jtaTransactionManager
属性(请参阅Hibernate Transaction Setup)。然后,Spring使相应的JTA策略可供Hibernate使用。 -
您还可以在
LocalSessionFactoryBean
上的“hibernateProperties”中显式配置Hibernate的JTA相关属性,特别是“hibernate.Transaction.coorator_class”、“hibernate.Connection.Handling_mode”和可能的“hibernate.Transaction.jta.form”(有关这些属性的详细信息,请参阅Hibernate手册)。
本节的其余部分描述了在Hibernate知晓JTAPlatformTransactionManager
的情况下和在没有知晓的情况下发生的一系列事件。
如果未使用JTA事务管理器的任何感知功能配置Hibernate,则在JTA事务提交时会发生以下事件:
-
JTA事务提交。
-
Spring的
JtaTransactionManager
与JTA事务同步,因此JTA事务管理器通过After Completion
回调它。 -
在其他活动中,这种同步可以通过Hibernate的
After TransactionCompletion
回调(用于清除Hibernate缓存)触发Spring对Hibernate的回调,然后对Hibernate会话进行显式的Close()
调用,这会导致Hibernate尝试Close()
JDBC连接。 -
在某些环境中,此
Connection.Close()
调用然后触发警告或错误,因为应用程序服务器不再认为连接
可用,因为事务已经提交。
如果使用JTA事务管理器的意识配置Hibernate,则在JTA事务提交时会发生以下事件:
-
JTA事务已准备好提交。
-
Spring的
JtaTransactionManager
与JTA事务同步,因此JTA事务管理器通过beForeCompletion
回调该事务。 -
Spring知道Hibernate本身是与JTA事务同步的,并且其行为与前面的场景不同。特别是,它与Hibernate的事务性资源管理保持一致。
-
JTA事务提交。
-
Hibernate与JTA事务同步,因此JTA事务管理器通过
After Completion
回调该事务,并可以正确地清除其缓存。
5.4. JPA
可以在org.springfrawork.orm.jpa
包下获得的Spring JPA以类似于与Hibernate集成的方式为Java持久性API提供全面的支持,同时了解底层实现以提供附加功能。
5.4.1. Three Options for JPA Setup in a Spring Environment
Spring JPA支持提供了三种方法来设置JPAEntityManagerFactory
,应用程序使用该JPA来获取实体管理器。
-
使用
LocalEntityManagerFactoryBean
-
使用
LocalContainerEntityManagerFactoryBean
Using LocalEntityManagerFactoryBean
您只能在简单的部署环境中使用此选项,例如独立应用程序和集成测试。
LocalEntityManagerFactoryBean
创建适用于应用程序仅使用JPA进行数据访问的简单部署环境的EntityManagerFactory
。工厂Bean使用JPAPersistenceProvider
自动检测机制(根据JPA的Java SE引导),并且在大多数情况下,只需要指定持久化单元名称。下面的XML示例配置这样一个Bean:
<beans>
<bean id="myEmf" class="org.springframework.orm.jpa.LocalEntityManagerFactoryBean">
<property name="persistenceUnitName" value="myPersistenceUnit"/>
</bean>
</beans>
这种形式的JPA部署是最简单和最受限制的。您不能引用现有的JDBCDataSource
Bean定义,并且不支持全局事务。此外,持久化类的编织(字节码转换)是特定于提供程序的,通常需要在启动时指定特定的JVM代理。此选项仅适用于JPA规范所针对的独立应用程序和测试环境。
Obtaining an EntityManagerFactory from JNDI
在部署到Jakarta EE服务器时,您可以使用此选项。查看服务器的文档,了解如何将定制的JPA提供程序部署到服务器中,从而允许使用不同于服务器默认提供程序的提供程序。
从JNDI(例如,在Jakarta EE环境中)获取EntityManagerFactory
需要更改XML配置,如下面的示例所示:
<beans>
<jee:jndi-lookup id="myEmf" jndi-name="persistence/myPersistenceUnit"/>
</beans>
此操作假定为标准的Jakarta EE自举。Jakarta EE服务器自动检测持久化单元(实际上是应用程序JAR中的META-INF/Persistence.xml
文件)和Jakarta EE部署描述符中的Persistence-unit-ref
条目(例如,web.xml
),并为这些持久化单元定义环境命名上下文位置。
在这样的场景中,整个持久化单元部署,包括持久化类的编织(字节码转换),都取决于Jakarta EE服务器。JDBCDataSource
是通过META-INF/sistence.xml
文件中的JNDI位置定义的。EntityManager
事务与服务器的JTA子系统集成。Spring只使用获得的EntityManagerFactory
,通过依赖项注入将其传递给应用程序对象,并管理持久化单元的事务(通常通过JtaTransactionManager
)。
如果在同一应用程序中使用多个持久化单元,则此类JNDI检索的持久化单元的Bean名称应该与应用程序用来引用它们的持久化单元名称匹配(例如,在@PersistenceUnit
和@PersistenceContext
注释中)。
Using LocalContainerEntityManagerFactoryBean
在基于Spring的应用程序环境中,您可以使用该选项来实现完整的JPA功能。这包括Tomcat等Web容器、独立应用程序和具有复杂持久性需求的集成测试。
If you want to specifically configure a Hibernate setup, an immediate alternative is to set up a native Hibernate LocalSessionFactoryBean instead of a plain JPA LocalContainerEntityManagerFactoryBean , letting it interact with JPA access code as well as native Hibernate access code. See Native Hibernate setup for JPA interaction for details. |
LocalContainerEntityManagerFactoryBean
提供对实体管理器工厂
配置的完全控制,适用于需要细粒度定制的环境。LocalContainerEntityManagerFactoryBean
基于sistence.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>
下面的示例显示了一个典型的sistence.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>
The <exclude-unlisted-classes/> shortcut indicates that no scanning for annotated entity classes is supposed to occur. An explicit 'true' value (<exclude-unlisted-classes>true</exclude-unlisted-classes/> ) also means no scan. <exclude-unlisted-classes>false</exclude-unlisted-classes/> does trigger a scan. However, we recommend omitting the exclude-unlisted-classes element if you want entity class scanning to occur. |
使用LocalContainerEntityManagerFactoryBean
是最强大的JPA设置选项,允许在应用程序中进行灵活的本地配置。它支持指向现有JDBC数据源
的链接,支持本地和全局事务,等等。但是,它也对运行时环境提出了要求,例如,如果持久性提供者需要字节码转换,则是否需要具有编织功能的类加载器。
此选项可能与Jakarta EE服务器的内置JPA功能冲突。在一个完整的Jakarta EE环境中,可以考虑从JNDI获取EntityManagerFactory
。或者,在您的LocalContainerEntityManagerFactoryBean
定义(例如,META-INF/my-sistence.xml)上指定一个定制的sistenceXmlLocation
,并在您的应用程序JAR文件中只包含一个具有该名称的描述符。因为Jakarta EE服务器只查找默认的META-INF/Persistence.xml
文件,所以它忽略了这样的定制持久性单元,从而避免了与Spring驱动的JPA设置的冲突。(例如,这适用于树脂3.1。)
LoadTimeWeaver
接口是一个Spring提供的类,它允许以特定方式插入JPAClassTransformer
实例,具体取决于环境是Web容器还是应用程序服务器。通过代理挂接ClassTransformers
通常效率不高。代理针对整个虚拟机工作,并检查加载的每个类,这在生产服务器环境中通常是不可取的。
Spring为各种环境提供了许多LoadTimeWeaver
实现,允许ClassTransformer
实例仅应用于每个类加载器,而不是应用于每个VM。
有关LoadTimeWeaver
实现及其设置的更多信息,请参阅AOP章节中的Spring配置,这些实现可以是通用的,也可以是针对各种平台(如Tomcat、JBoss和WebSphere)定制的。
如Spring配置中所述,您可以使用@EnableLoadTimeWeving
注释或上下文:Load-Time-weaver
元素来配置上下文范围的LoadTimeWeaver
。这样的全局编织器被所有JPALocalContainerEntityManagerFactoryBean
实例自动拾取。下面的示例显示了设置加载时编织器、提供平台自动检测(例如,Tomcat的支持编织的类加载器或Spring的JVM代理)以及将编织器自动传播到所有支持编织器的Bean的首选方法:
<context:load-time-weaver/>
<bean id="emf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
...
</bean>
但是,如果需要,您可以通过loadTimeWeaver
属性手动指定专用编织器,如下面的示例所示:
<bean id="emf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="loadTimeWeaver">
<bean class="org.springframework.instrument.classloading.ReflectiveLoadTimeWeaver"/>
</property>
</bean>
无论LTW如何配置,通过使用此技术,依赖于规范的JPA应用程序都可以在目标平台(例如Tomcat)上运行,而不需要代理。当宿主应用程序依赖于不同的JPA实现时,这一点尤其重要,因为JPA转换器仅在类加载器级别应用,因此彼此隔离。
Dealing with Multiple Persistence Units
对于依赖于多个持久化单元位置的应用程序(例如,存储在类路径中的各种JAR中),Spring提供了PersistenceUnitManager
作为中央存储库,以避免可能代价高昂的持久化单元发现过程。默认实现允许指定多个位置。这些位置被解析,然后通过持久化单元名称进行检索。(默认情况下,在类路径中搜索META-INF/sistence.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>
默认实现允许以声明方式(通过其影响所有托管单元的属性)或以编程方式(通过PersistenceUnitPostProcessor
,允许选择持久性单元)定制PersistenceUnitInfo
实例(在将它们提供给JPA提供程序之前)。如果未指定PersistenceUnitManager
,则创建一个并由LocalContainerEntityManagerFactoryBean
.在内部使用
Background Bootstrapping
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 data JPA时,请确保也为其存储库设置了延迟引导。
5.4.2. Implementing DAOs Based on JPA: EntityManagerFactory
and EntityManager
Although EntityManagerFactory instances are thread-safe, EntityManager instances are not. The injected JPA EntityManager behaves like an EntityManager fetched from an application server’s JNDI environment, as defined by the JPA specification. It delegates all calls to the current transactional EntityManager , if any. Otherwise, it falls back to a newly created EntityManager per operation, in effect making its usage thread-safe. |
通过使用注入的EntityManagerFactory
或EntityManager
,可以在没有任何Spring依赖的情况下针对普通JPA编写代码。如果启用了PersistenceAnnotationBeanPostProcessor
,Spring可以在字段和方法级别理解<代码>@PersistenceUnit
和<代码>@PersistenceContext
注释。下面的示例显示了一个使用@PersistenceUnit
注释的普通JPA DAO实现:
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();
}
}
}
}
前面的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>
作为显式定义注释的替代方法,可以考虑在应用程序上下文配置中使用Spring<PersistenceAnnotationBeanPostProcessor
,>上下文:注解-配置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的共享的、线程安全的代理)而不是工厂来避免这种情况。以下示例显示了如何执行此操作:
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();
}
}
@PersistenceContext
批注有一个可选属性,名为type
,默认为PersistenceContextType.TRANSACTION
.您能够使用此默认值接收共享的EntityManager
代理。可选的PersistenceContextType.EXTENDED
则完全是另一回事。这导致了所谓的扩展EntityManager
,它不是线程安全的,因此不能在并发访问的组件中使用,例如Spring管理的单例Bean。扩展的EntityManager
实例应该只用于有状态组件中,例如驻留在会话中,EntityManager
的生命周期与当前事务无关,而完全取决于应用程序。
注入的EntityManager
是由Spring管理的(知道正在进行的事务)。尽管新的DAO实现使用EntityManager
而不是EntityManager
的方法级注入,但由于使用了注释,应用程序上下文XML中不需要任何更改。
这种DAO风格的主要优点是它只依赖于Java持久性API。不需要导入任何Spring类。此外,正如理解JPA注释一样,注入由Spring容器自动应用。从非侵入性的角度来看,这很有吸引力,JPA开发人员会觉得更自然。
5.4.3. Spring-driven JPA transactions
We strongly encourage you to read Declarative Transaction Management, if you have not already done so, to get more detailed coverage of Spring’s declarative transaction support. |
JPA的推荐策略是通过JPA的本地事务支持进行本地事务。Spring的JpaTransactionManager
针对任何常规的JDBC连接池(没有XA要求)提供了许多从本地JDBC事务中已知的功能(例如特定于事务的隔离级别和资源级别的只读优化)。
Spring JPA还允许已配置的JpaTransactionManager
向访问相同数据源
的JDBC访问代码公开JPA事务,前提是已注册的JpaDialect
支持底层JDBC连接
的检索。Spring为NikpseLink和Hibernate JPA实现提供了方言。有关JpaDialect
机制的详细信息,请参阅下一节。
As an immediate alternative, Spring’s native HibernateTransactionManager is capable of interacting with JPA access code, adapting to several Hibernate specifics and providing JDBC interaction. This makes particular sense in combination with LocalSessionFactoryBean setup. See Native Hibernate Setup for JPA Interaction for details. |
5.4.4. Understanding JpaDialect
and JpaVendorAdapter
作为一项高级功能,JpaTransactionManager
和AbstractEntityManagerFactoryBean
的子类允许将自定义JpaDialect
传递到jpaDialect
Bean属性。JpaDialect
实现可以启用Spring支持的以下高级功能,通常是以特定于供应商的方式:
-
应用特定的事务语义(如自定义隔离级别或事务超时)
-
检索事务性JDBC
连接
(用于公开基于JDBC的DAO) -
PersistenceExceptions
到SpringDataAccessExceptions
的高级翻译
这对于特殊的事务语义和异常的高级翻译特别有价值。默认实现(DefaultJpaDialect
)不提供任何特殊功能,如果需要前面列出的功能,则必须指定适当的方言。
As an even broader provider adaptation facility primarily for Spring’s full-featured LocalContainerEntityManagerFactoryBean setup, JpaVendorAdapter combines the capabilities of JpaDialect with other provider-specific defaults. Specifying a HibernateJpaVendorAdapter or EclipseLinkJpaVendorAdapter is the most convenient way of auto-configuring an EntityManagerFactory setup for Hibernate or EclipseLink, respectively. Note that those provider adapters are primarily designed for use with Spring-driven transaction management (that is, for use with JpaTransactionManager ). |
请参阅JpaDialect
和
JpaVendorAdapter
,了解其操作的更多详细信息以及如何在Spring的JPA支持中使用它们。
5.4.5. Setting up JPA with JTA Transaction Management
作为JpaTransactionManager
的替代方案,Spring还允许通过JTA进行多资源事务协调,无论是在Jakarta EE环境中,还是使用独立的事务协调器(如Atom)。除了选择Spring的JtaTransactionManager
而不是JpaTransactionManager
之外,您还需要采取以下几个步骤:
-
底层JDBC连接池需要支持XA,并与您的事务协调器集成。在Jakarta EE环境中,这通常很简单,通过JNDI公开不同类型的
DataSource
。有关详细信息,请参阅您的应用程序服务器文档。类似地,独立事务协调器通常带有特殊的XA集成DataSource
变体。同样,请查看其文档。 -
需要为JTA配置JPA
EntityManager
安装程序。这是特定于提供程序的,通常通过在LocalContainerEntityManagerFactoryBean
.上指定为jpaProperties
的特殊属性实现在Hibernate中,这些属性甚至是特定于版本的。有关详细信息,请参阅您的Hibernate文档。 -
Spring的
HibernateJpaVendorAdapter
强制使用了某些面向Spring的默认设置,比如连接释放模式On-Close
,它与Hibernate 5.0中的默认设置相匹配,但在5.1+中不再匹配。对于JTA设置,确保将持久化单元事务类型声明为“JTA”。或者,将Hiberate5.2的hibernate.connection.handling_mode
属性设置为DELAYED_ACQUISITION_AND_RELEASE_AFTER_STATEMENT
以恢复Hibernate5.2自己的默认设置。有关说明,请参阅Hibernate的虚假应用程序服务器警告。 -
或者,考虑从您的应用程序服务器本身获取<代码>EntityManager Factory (即,通过JNDI查找而不是本地声明的
LocalContainerEntityManagerFactoryBean
).服务器提供的EntityManagerFactory
可能需要在您的服务器配置中使用特殊定义(这会降低部署的可移植性),但它是为服务器的JTA环境设置的。
5.4.6. Native Hibernate Setup and Native Hibernate Transactions for JPA Interaction
本机LocalSessionFactoryBean
安装程序与HibernateTransactionManager
结合使用,允许与@PersistenceContext
和其他JPA访问代码交互。HibernateSessionFactory
现在本机实现了JPA的EntityManagerFactory
接口,而Hibernate会话
句柄本机是JPAEntityManager
。Spring的JPA支持工具会自动检测本机Hibernate会话。
因此,在许多场景中,这种本机Hibernate设置可以作为标准JPACode和<LocalContainerEntityManagerFactoryBean
>组合的替代品,允许在同一本地事务中与<SessionFactory.getCurrentSession()
>@PersistenceContext EntityManager旁边的HibernateTemplate
进行交互。这样的设置还提供了更强大的Hibernate集成和更大的配置灵活性,因为它不受JPA引导协议的限制。
在这样的场景中,您不需要HibernateJpaVendorAdapter
配置,因为Spring的原生Hibernate设置提供了更多的特性(例如,定制的Hibernate Integrator设置、Hibernate5.3Bean容器集成,以及针对只读事务的更强大的优化)。最后但并非最不重要的一点是,您还可以通过LocalSessionFactoryBuilder
表示原生Hibernate设置,与@Bean
样式配置无缝集成(不涉及FactoryBean
)。
在 |
6. Marshalling XML by Using Object-XML Mappers
6.1. Introduction
本章描述了Spring的对象-XML映射支持。对象-XML映射(简称O-X映射)是将XML文档与对象相互转换的行为。此转换过程也称为XML编组或XML序列化。本章互换使用这些术语。
在O-X映射领域中,封送处理程序负责将对象(图)序列化为XML。以类似的方式,解组程序将XML反序列化为对象图。该XML可以采用DOM文档、输入或输出流或SAX处理程序的形式。
使用Spring满足您的O/X映射需求的一些好处包括:
6.1.1. Ease of configuration
Spring的Bean工厂使得配置封送处理程序变得很容易,而无需构造JAXB上下文、JiBX绑定工厂等。您可以像配置应用程序上下文中的任何其他Bean一样配置封送处理程序。此外,许多封送处理程序都可以使用基于XML名称空间的配置,这使得配置更加简单。
6.1.2. Consistent Interfaces
Spring的O-X映射通过两个全局接口进行操作:Marshaller
和Unmarshaller
。这些抽象允许您相对轻松地切换O-X映射框架,只需要对执行编组的类进行很少或根本不需要更改。这种方法还有一个额外的好处,即可以使用混合匹配方法(例如,使用JAXB执行的一些编组和使用XStream执行的一些编组)以非侵入性的方式进行XML编组,从而使您能够利用每种技术的优势。
6.2. Marshaller
and Unmarshaller
如简介中所述,封送处理程序将对象序列化为XML,而解组处理程序将XML流反序列化为对象。本节介绍用于此目的的两个Spring接口。
6.2.1. Understanding 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.form.Result
。结果是一个基本上表示XML输出抽象的标记接口。具体实现包装了各种XML表示形式,如下表所示:
Result implementation | Wraps XML representation |
---|---|
|
|
|
|
|
|
Although the marshal() method accepts a plain object as its first parameter, most Marshaller implementations cannot handle arbitrary objects. Instead, an object class must be mapped in a mapping file, be marked with an annotation, be registered with the marshaller, or have a common base class. Refer to the later sections in this chapter to determine how your O-X technology manages this. |
6.2.2. Understanding 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.form.Source
(一个XML输入抽象)中读取并返回读取的对象。与Result
一样,Source
是一个有三个具体实现的标记接口。每个包装了不同的XML表示形式,如下表所示:
Source implementation | Wraps XML representation |
---|---|
|
|
|
|
|
|
尽管有两个独立的编组接口(Marshaller
和Unmarshaller
),但Spring-WS中的所有实现都在一个类中实现。这意味着您可以连接一个编组程序类,并在ApplationConext.xml
中将其同时作为编组程序和解组程序引用。
6.3. Using Marshaller
and Unmarshaller
您可以在多种情况下使用SpringOXM。在下面的示例中,我们使用它将Spring管理的应用程序的设置封送为一个XML文件。在下面的示例中,我们使用一个简单的JavaBean来表示设置:
public class Settings {
private boolean fooEnabled;
public boolean isFooEnabled() {
return fooEnabled;
}
public void setFooEnabled(boolean fooEnabled) {
this.fooEnabled = fooEnabled;
}
}
应用程序类使用此Bean存储其设置。除了main方法外,该类还有两个方法:saveSettings()
将设置Bean保存到名为settings.xml
的文件中,loadSettings()
再次加载这些设置。下面的main()
方法构造一个Spring应用程序上下文并调用这两个方法:
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(); } }
应用程序
要求同时设置编组程序
和反编组程序
属性。我们可以通过使用以下applationConext.xml
来完成此操作:
<beans>
<bean id="application" class="Application">
<property name="marshaller" ref="xstreamMarshaller" />
<property name="unmarshaller" ref="xstreamMarshaller" />
</bean>
<bean id="xstreamMarshaller" class="org.springframework.oxm.xstream.XStreamMarshaller"/>
</beans>
此应用程序上下文使用XStream,但我们也可以使用本章后面介绍的任何其他封送处理程序实例。请注意,默认情况下,XStream不需要任何进一步的配置,因此Bean定义非常简单。还要注意,XStreamMarshaller
同时实现了Marshaller
和Unmarshaller
,因此我们可以在应用程序的marshaller
和unmarshaller
属性中引用xstream Marshaller
Bean。
此示例应用程序生成以下settings.xml
文件:
<?xml version="1.0" encoding="UTF-8"?>
<settings foo-enabled="false"/>
6.4. XML Configuration Namespace
通过使用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 | Reference the oxm schema. |
2 | Specify the oxm schema location. |
该架构使以下元素可用:
每个标记都在其各自的封送处理程序部分进行了说明。但是,作为一个示例,JAXB2封送处理程序的配置可能类似于以下内容:
<oxm:jaxb2-marshaller id="marshaller" contextPath="org.springframework.ws.samples.airline.schema"/>
6.5. JAXB
JAXB绑定编译器将W3CXML模式转换为一个或多个Java类、jaxb.properties
文件,可能还有一些资源文件。JAXB还提供了一种从带注释的Java类生成模式的方法。
Spring支持JAXB 2.0API作为XML编组策略,遵循编组
和编组
中描述的编组
和解组
接口。对应的集成类位于org.springFrawork.oxm.jaxb
包中。
6.5.1. Using Jaxb2Marshaller
Jaxb2Marshaller
类实现了Spring的Marshaller
和Unmarshaller
接口。它需要一个上下文路径才能运行。您可以通过设置上下文路径
属性来设置上下文路径。上下文路径是包含架构派生类的冒号分隔的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 Configuration Namespace
jaxb2-marshaller
org.springframework.oxm.jaxb.Jaxb2Marshaller
,>元素配置一个代码,如下面的示例所示:
<oxm:jaxb2-marshaller id="marshaller" contextPath="org.springframework.ws.samples.airline.schema"/>
或者,您可以通过使用要绑定的类
子元素来提供要绑定到封送处理程序的类的列表:
<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 | Description | Required |
---|---|---|
|
封送处理程序的ID |
不是 |
|
JAXB上下文路径 |
不是 |
6.6. JiBX
JiBX框架提供了一种类似于Hibernate为ORM提供的解决方案:绑定定义定义了如何将Java对象转换为XML或从XML转换的规则。在准备绑定并编译类之后,JiBX绑定编译器增强类文件并添加代码来处理类实例从XML到XML的转换。
有关JiBX的更多信息,请访问JiBX网站。Spring集成类位于org.springFrawork.oxm.jibx
包中。
6.6.1. Using JibxMarshaller
JibxMarshaller
类实现Marshaller
和Unmarshaller
接口。要进行操作,它需要要编组的类的名称,您可以使用Target Class
属性设置该名称。或者,您可以通过设置bindingName
属性来设置绑定名称。在下面的示例中,我们绑定了航班
类:
<beans>
<bean id="jibxFlightsMarshaller" class="org.springframework.oxm.jibx.JibxMarshaller">
<property name="targetClass">org.springframework.oxm.jibx.Flights</property>
</bean>
...
</beans>
为单个类配置了JibxMarshaller
。如果要封送多个类,则必须使用不同的Target Class
属性值配置多个JibxMarshaller
实例。
XML Configuration Namespace
jibx-marshaller
标记配置org.springframework.oxm.jibx.JibxMarshaller
,,如下面的示例所示:
<oxm:jibx-marshaller id="marshaller" target-class="org.springframework.ws.samples.airline.schema.Flight"/>
下表描述了可用的属性:
Attribute | Description | Required |
---|---|---|
|
封送处理程序的ID |
不是 |
|
此封送处理程序的目标类 |
是 |
|
此封送处理程序使用的绑定名称 |
不是 |
6.7. XStream
Xstream是一个简单的库,可以将对象序列化为XML,然后再序列化为XML。它不需要任何映射,并生成干净的XML。
有关XStream的详细信息,请参阅XStream网站。Spring集成类位于org.springFrawork.oxm.xstream
包中。
6.7.1. Using 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序列化效果。因此,我们不建议使用 如果选择使用
这样做可以确保只有注册的类有资格进行解组。 此外,您可以注册自定义转换器,以确保只有您支持的类才能被解组。除了显式支持应该支持的域类的转换器之外,您可能希望添加 |
Note that XStream is an XML serialization library, not a data binding library. Therefore, it has limited namespace support. As a result, it is rather unsuitable for usage within Web Services. |
7. Appendix
7.1. XML Schemas
附录的这一部分列出了用于数据访问的XML架构,包括:
7.1.1. The tx
Schema
tx
标记处理在Spring对事务的全面支持中配置所有这些Bean。这些标记在题为事务管理的一章中介绍。
We strongly encourage you to look at the 'spring-tx.xsd' file that ships with the Spring distribution. This file contains the XML Schema for Spring’s transaction configuration and covers all of the various elements in the tx namespace, including attribute defaults and similar information. This file is documented inline, and, thus, the information is not repeated here in the interests of adhering to the DRY (Don’t Repeat Yourself) principle. |
为了完整起见,要使用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 | Declare usage of the tx namespace. |
2 | Specify the location (with other schema locations). |
Often, when you use the elements in the tx namespace, you are also using the elements from the aop namespace (since the declarative transaction support in Spring is implemented by using AOP). The preceding XML snippet contains the relevant lines needed to reference the aop schema so that the elements in the aop namespace are available to you. |
7.1.2. The jdbc
Schema
要使用JDBC
模式中的元素,您需要在您的Spring XML配置文件的顶部有以下前言。以下代码段中的文本引用了正确的架构,以便您可以使用JDBC
命名空间中的元素:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jdbc="http://www.springframework.org/schema/jdbc" (1) xsi:schemaLocation=" http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/jdbc https://www.springframework.org/schema/jdbc/spring-jdbc.xsd"> (2)
<!-- bean definitions here -->
</beans>
1 | Declare usage of the jdbc namespace. |
2 | Specify the location (with other schema locations). |