|
对于最新稳定版本,请使用 Spring Framework 7.0.6! |
Hibernate
我们首先介绍在 Spring 环境中使用 Hibernate 5, 以此展示 Spring 集成对象关系映射(ORM)框架所采用的方法。 本节详细讨论了许多问题,并展示了 DAO 实现和事务界定的不同变体。 其中大多数模式可直接应用于所有其他受支持的 ORM 工具。 本章后续部分将介绍其他 ORM 技术,并提供简要示例。
|
从 Spring Framework 6.0 起,Spring 在使用 Hibernate ORM 6.x 仅作为 JPA 提供商( |
SessionFactory在 Spring 容器中设置
为了避免将应用程序对象与硬编码的资源查找绑定在一起,您可以将资源(例如 JDBC DataSource 或 Hibernate SessionFactory)定义为 Spring 容器中的 bean。需要访问这些资源的应用程序对象通过 bean 引用获得对这些预定义实例的引用,如下一节中的 DAO 定义所示。
以下是从 XML 应用上下文定义中摘录的片段,展示了如何在此基础上配置 JDBC DataSource 和 Hibernate SessionFactory:
<beans>
<bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="org.hsqldb.jdbcDriver"/>
<property name="url" value="jdbc:hsqldb:hsql://localhost:9001"/>
<property name="username" value="sa"/>
<property name="password" value=""/>
</bean>
<bean id="mySessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean">
<property name="dataSource" ref="myDataSource"/>
<property name="mappingResources">
<list>
<value>product.hbm.xml</value>
</list>
</property>
<property name="hibernateProperties">
<value>
hibernate.dialect=org.hibernate.dialect.HSQLDialect
</value>
</property>
</bean>
</beans>
从本地的 Jakarta Commons DBCP BasicDataSource 切换到通过 JNDI 定位的DataSource(通常由应用服务器管理)仅涉及配置上的更改,如下例所示:
<beans>
<jee:jndi-lookup id="myDataSource" jndi-name="java:comp/env/jdbc/myds"/>
</beans>
您也可以通过 Spring 的 SessionFactory / JndiObjectFactoryBean 来访问位于 JNDI 中的 <jee:jndi-lookup>,以检索并暴露它。
然而,这通常在 EJB 上下文之外并不常见。
|
Spring 还提供了一个
从 Spring Framework 5.1 开始,这种原生 Hibernate 配置除了支持原生 Hibernate 访问外,还可以同时暴露一个 JPA 的 |
基于原生 Hibernate API 实现 DAO
Hibernate 有一个名为上下文会话(contextual sessions)的特性,其中 Hibernate 本身为每个事务管理一个当前的Session。这大致等同于 Spring 为每个事务同步一个 Hibernate Session 的机制。相应的 DAO 实现类似于以下示例,该示例基于原生的 Hibernate API:
-
Java
-
Kotlin
public class ProductDaoImpl implements ProductDao {
private SessionFactory sessionFactory;
public void setSessionFactory(SessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
}
public Collection loadProductsByCategory(String category) {
return this.sessionFactory.getCurrentSession()
.createQuery("from test.Product product where product.category=?")
.setParameter(0, category)
.list();
}
}
class ProductDaoImpl(private val sessionFactory: SessionFactory) : ProductDao {
fun loadProductsByCategory(category: String): Collection<*> {
return sessionFactory.currentSession
.createQuery("from test.Product product where product.category=?")
.setParameter(0, category)
.list()
}
}
这种风格类似于 Hibernate 参考文档和示例中的做法,只是将 SessionFactory 保存在实例变量中。我们强烈推荐采用这种基于实例的设置方式,而不是使用 Hibernate 的 CaveatEmptor 示例应用程序中那种传统的 static HibernateUtil 类。(通常情况下,除非绝对必要,否则不要将任何资源保存在 static 变量中。)
前面的 DAO 示例遵循了依赖注入模式。它能很好地融入 Spring IoC 容器,就像使用 Spring 的 HibernateTemplate 编码时一样。
你也可以在纯 Java 环境中(例如在单元测试中)配置这样的 DAO。为此,只需实例化该 DAO 并调用 setSessionFactory(..) 方法传入所需的 SessionFactory 引用即可。作为 Spring 的 bean 定义,该 DAO 类似于以下内容:
<beans>
<bean id="myProductDao" class="product.ProductDaoImpl">
<property name="sessionFactory" ref="mySessionFactory"/>
</bean>
</beans>
这种 DAO 风格的主要优势在于它仅依赖于 Hibernate API。无需导入任何 Spring 类。从非侵入性角度来看,这颇具吸引力,对 Hibernate 开发者而言也可能感觉更加自然。
然而,DAO 抛出的是普通的 HibernateException(这是一个非检查型异常,因此无需声明或捕获),这意味着调用者只能将这些异常视为一般性的致命错误——除非他们愿意依赖 Hibernate 自身的异常体系结构。如果不将调用者与具体的实现策略耦合,就无法捕获特定原因的异常(例如乐观锁失败)。对于那些重度依赖 Hibernate、不需要特殊异常处理,或者同时满足这两个条件的应用程序来说,这种权衡可能是可以接受的。
幸运的是,Spring 的 LocalSessionFactoryBean 支持 Hibernate 的
SessionFactory.getCurrentSession() 方法,适用于任何 Spring 事务策略,
即使使用 Session,也能返回当前由 Spring 管理的事务性 HibernateTransactionManager。
该方法的标准行为仍然是:如果存在正在进行的 JTA 事务,则返回与之关联的当前 Session。
无论你使用的是 Spring 的 JtaTransactionManager、EJB 容器管理的事务(CMT),还是直接使用 JTA,
这一行为都适用。
总之,你可以基于原生的 Hibernate API 实现 DAO,同时仍然能够参与 Spring 管理的事务。
声明式事务界定
我们建议您使用 Spring 的声明式事务支持,它允许您用 AOP 事务拦截器替代 Java 代码中显式的事务界定 API 调用。您可以在 Spring 容器中通过 Java 注解或 XML 来配置该事务拦截器。这种声明式事务功能可让您避免在业务服务中编写重复的事务界定代码,从而专注于添加业务逻辑——这才是您应用程序的真正价值所在。
| 在继续之前,如果您尚未阅读声明式事务管理,我们强烈建议您先阅读该部分内容。 |
你可以使用 @Transactional 注解对服务层进行标注,并指示 Spring 容器查找这些注解,为这些被标注的方法提供事务语义。以下示例展示了如何实现这一点:
-
Java
-
Kotlin
public class ProductServiceImpl implements ProductService {
private ProductDao productDao;
public void setProductDao(ProductDao productDao) {
this.productDao = productDao;
}
@Transactional
public void increasePriceOfAllProductsInCategory(final String category) {
List productsToChange = this.productDao.loadProductsByCategory(category);
// ...
}
@Transactional(readOnly = true)
public List<Product> findAllProducts() {
return this.productDao.findAllProducts();
}
}
class ProductServiceImpl(private val productDao: ProductDao) : ProductService {
@Transactional
fun increasePriceOfAllProductsInCategory(category: String) {
val productsToChange = productDao.loadProductsByCategory(category)
// ...
}
@Transactional(readOnly = true)
fun findAllProducts() = productDao.findAllProducts()
}
在容器中,您需要配置 PlatformTransactionManager 的实现
(作为一个 bean),并添加一个 <tx:annotation-driven/> 条目,以在运行时启用 @Transactional
注解的处理。以下示例展示了如何进行配置:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
https://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- SessionFactory, DataSource, etc. omitted -->
<bean id="transactionManager"
class="org.springframework.orm.hibernate5.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory"/>
</bean>
<tx:annotation-driven/>
<bean id="myProductService" class="product.SimpleProductService">
<property name="productDao" ref="myProductDao"/>
</bean>
</beans>
编程式事务界定
你可以在应用程序的较高层级上界定事务,该层级位于跨越任意数量操作的底层数据访问服务之上。对周边业务服务的实现也没有任何限制。它只需要一个 Spring 的 PlatformTransactionManager。同样,后者可以来自任意位置,但最好通过 setTransactionManager(..) 方法以 bean 引用的方式注入。此外,productDAO 也应通过 setProductDao(..) 方法进行设置。以下两段代码片段分别展示了在 Spring 应用上下文中事务管理器和业务服务的定义,以及一个业务方法实现的示例:
<beans>
<bean id="myTxManager" class="org.springframework.orm.hibernate5.HibernateTransactionManager">
<property name="sessionFactory" ref="mySessionFactory"/>
</bean>
<bean id="myProductService" class="product.ProductServiceImpl">
<property name="transactionManager" ref="myTxManager"/>
<property name="productDao" ref="myProductDao"/>
</bean>
</beans>
-
Java
-
Kotlin
public class ProductServiceImpl implements ProductService {
private TransactionTemplate transactionTemplate;
private ProductDao productDao;
public void setTransactionManager(PlatformTransactionManager transactionManager) {
this.transactionTemplate = new TransactionTemplate(transactionManager);
}
public void setProductDao(ProductDao productDao) {
this.productDao = productDao;
}
public void increasePriceOfAllProductsInCategory(final String category) {
this.transactionTemplate.execute(new TransactionCallbackWithoutResult() {
public void doInTransactionWithoutResult(TransactionStatus status) {
List productsToChange = this.productDao.loadProductsByCategory(category);
// do the price increase...
}
});
}
}
class ProductServiceImpl(transactionManager: PlatformTransactionManager,
private val productDao: ProductDao) : ProductService {
private val transactionTemplate = TransactionTemplate(transactionManager)
fun increasePriceOfAllProductsInCategory(category: String) {
transactionTemplate.execute {
val productsToChange = productDao.loadProductsByCategory(category)
// do the price increase...
}
}
}
Spring 的 TransactionInterceptor 允许回调代码抛出任何已检查的应用程序异常,而 TransactionTemplate 在回调中仅限于未检查的异常。TransactionTemplate 在发生未检查的应用程序异常时,或者当应用程序将事务标记为仅回滚(通过设置 TransactionStatus)时,会触发回滚。默认情况下,TransactionInterceptor 的行为与此相同,但允许为每个方法配置可自定义的回滚策略。
事务管理策略
TransactionTemplate和TransactionInterceptor都将实际的事务处理委托给一个PlatformTransactionManager实例(对于单个 Hibernate SessionFactory,它可以是一个HibernateTransactionManager,其底层使用ThreadLocalSession),或者对于 Hibernate 应用程序,委托给一个JtaTransactionManager(将事务委派给容器的 JTA 子系统)。您甚至可以使用自定义的PlatformTransactionManager实现。从原生 Hibernate 事务管理切换到 JTA(例如,当应用程序的某些部署面临分布式事务需求时)仅需进行配置更改。您可以将 Hibernate 事务管理器替换为 Spring 的 JTA 事务实现。事务界定和数据访问代码无需任何修改即可正常工作,因为它们使用的是通用的事务管理 API。
对于跨多个 Hibernate SessionFactory 的分布式事务,您可以将 JtaTransactionManager 作为事务策略,与多个 LocalSessionFactoryBean 定义结合使用。每个 DAO 都会通过其对应的 bean 属性注入一个特定的 SessionFactory 引用。只要所有底层的 JDBC 数据源都是支持事务的容器数据源,并且业务服务使用 JtaTransactionManager 作为事务策略,那么该业务服务就可以在任意数量的 DAO 和任意数量的 SessionFactory 上划分事务,而无需特别关注细节。
HibernateTransactionManager 和 JtaTransactionManager 均支持在 Hibernate 中进行适当的 JVM 级别缓存处理,而无需依赖容器特定的事务管理器查找或 JCA 连接器(前提是您不使用 EJB 来启动事务)。
HibernateTransactionManager 可以将 Hibernate JDBC Connection 导出为针对特定 DataSource 的普通 JDBC 访问代码。此功能允许在仅访问一个数据库的情况下,完全无需 JTA 即可实现混合使用 Hibernate 和 JDBC 数据访问的高层级事务划分。如果您通过 LocalSessionFactoryBean 类的 dataSource 属性将传入的 SessionFactory 配置了 DataSource,那么 HibernateTransactionManager 会自动将 Hibernate 事务暴露为 JDBC 事务。或者,您也可以通过 HibernateTransactionManager 类的 dataSource 属性显式指定应暴露其事务的 DataSource。
比较容器管理资源与本地定义资源
你可以在容器管理的 JNDI SessionFactory 和本地定义的 SessionFactory 之间切换,而无需更改一行应用程序代码。资源定义是放在容器中还是放在应用程序本地,主要取决于你所采用的事务策略。与 Spring 定义的本地 SessionFactory 相比,手动注册的 JNDI SessionFactory 并不会带来任何额外优势。通过 Hibernate 的 JCA 连接器部署 4 虽然可以参与 Jakarta EE 服务器的管理基础设施,但除此之外并不会带来实质性的附加价值。
Spring 的事务支持并不依赖于容器。当配置为 JTA 以外的任何策略时,事务支持在独立环境或测试环境中同样有效。 特别是在典型的单数据库事务场景中,Spring 对单资源本地事务的支持是一种轻量级且功能强大的 JTA 替代方案。当你使用本地 EJB 无状态会话 Bean 来驱动事务时,即使仅访问单个数据库,并且仅使用无状态会话 Bean 通过容器管理事务来提供声明式事务,你仍然同时依赖于 EJB 容器和 JTA。此外,直接以编程方式使用 JTA 也需要 Jakarta EE 环境。
由 Spring 管理的事务既可以与本地定义的 Hibernate SessionFactory 配合使用,也可以像与本地 JDBC DataSource 一样正常工作,前提是它们都只访问单一数据库。因此,只有在存在分布式事务需求时,才需要使用 Spring 的 JTA 事务策略。JCA 连接器首先需要容器特定的部署步骤(显然也需要容器本身支持 JCA)。相比仅使用本地资源定义和 Spring 管理事务来部署一个简单的 Web 应用,这种配置需要更多的工作量。
综上所述,如果您不使用 EJB,则应坚持采用本地 SessionFactory 配置,并配合使用 Spring 的 HibernateTransactionManager 或 JtaTransactionManager。这样您就能获得所有优势,包括恰当的事务性 JVM 级缓存和分布式事务支持,同时又避免了容器部署带来的不便。只有在与 EJB 结合使用时,通过 JCA 连接器将 Hibernate SessionFactory 注册到 JNDI 中才有实际价值。
使用 Hibernate 时出现虚假的应用服务器警告
在某些具有非常严格的 XADataSource 实现的 JTA 环境中(目前包括某些 WebLogic Server 和 WebSphere 版本),如果 Hibernate 的配置未考虑该环境的 JTA 事务管理器,则应用程序服务器日志中可能会出现虚假的警告或异常。这些警告或异常表明正在访问的连接已不再有效,或者 JDBC 访问已不再有效,可能是因为事务已不再处于活动状态。例如,以下是来自 WebLogic 的一个实际异常:
java.sql.SQLException: The transaction is no longer active - status: 'Committed'. No further JDBC access is allowed within this transaction.
另一个常见问题是 JTA 事务之后出现连接泄漏,Hibernate 会话(以及潜在的底层 JDBC 连接)未能正确关闭。
你可以通过让 Hibernate 感知到 JTA 事务管理器来解决此类问题,Hibernate 会与该事务管理器(以及 Spring)进行同步。你有两种方式可以实现这一点:
-
将您的 Spring
JtaTransactionManagerBean 传递给 Hibernate 配置。最简单的方法是将该 Bean 的引用设置到您的jtaTransactionManagerBean 的LocalSessionFactoryBean属性中(参见Hibernate 事务配置)。 随后,Spring 会将相应的 JTA 策略提供给 Hibernate。 -
您也可以在
LocalSessionFactoryBean的 "hibernateProperties" 中显式配置 Hibernate 与 JTA 相关的属性,特别是 "hibernate.transaction.coordinator_class"、"hibernate.connection.handling_mode",以及可能用到的 "hibernate.transaction.jta.platform"(有关这些属性的详细信息,请参阅 Hibernate 手册)。
本节其余部分描述了在 Hibernate 是否感知 JTA PlatformTransactionManager 的情况下所发生的事件序列。
当 Hibernate 未配置为感知 JTA 事务管理器时,在 JTA 事务提交时会发生以下事件:
-
JTA 事务提交。
-
Spring 的
JtaTransactionManager与 JTA 事务同步,因此它会通过 JTA 事务管理器的afterCompletion回调被调用。 -
除了其他操作外,这种同步还可能通过 Hibernate 的
afterTransactionCompletion回调(用于清除 Hibernate 缓存)触发 Spring 对 Hibernate 的回调,随后显式调用 Hibernate 会话的close()方法,这将导致 Hibernate 尝试关闭 JDBC 连接(close())。 -
在某些环境中,此
Connection.close()调用会触发警告或错误,因为应用服务器不再认为该Connection可用,原因是事务已经提交。
当 Hibernate 配置为感知 JTA 事务管理器时,JTA 事务提交时将发生以下事件:
-
JTA 事务已准备好提交。
-
Spring 的
JtaTransactionManager与 JTA 事务同步,因此事务会通过 JTA 事务管理器的beforeCompletion回调被调用。 -
Spring 意识到 Hibernate 本身已与 JTA 事务同步,并且其行为与前一种场景有所不同。特别是,它与 Hibernate 的事务性资源管理保持一致。
-
JTA 事务提交。
-
Hibernate 与 JTA 事务同步,因此事务会通过 JTA 事务管理器的
afterCompletion回调被调用,从而能够正确地清空其缓存。