跳到主要内容
版本:7.0.3

Hibernate

Hunyuan 7b 中英对照 Hibernate

我们首先介绍在Spring环境中使用Hibernate,通过它来演示Spring在集成OR映射器时所采用的方法。本节详细涵盖了许多问题,并展示了不同版本的DAO实现和事务划分方式。这些模式中的大多数可以直接应用到其他所有支持的ORM工具中。本章的后续部分将介绍其他ORM技术,并提供简短的示例。

备注

从 Spring Framework 7.0 开始,Spring 要求使用 Hibernate ORM 7.x 来支持其 HibernateJpaVendorAdapter

org.springframework.orm.jpa.hibernate 包取代了之前的 orm.hibernate5:现在它与 Hibernate ORM 7.1+ 版本兼容,与 HibernateJpaVendorAdapter 紧密集成,并且还支持 Hibernate 的原生 SessionFactory.getCurrentSession() 方法。

在Spring容器中设置SessionFactory

为了避免将应用程序对象与硬编码的资源查找机制绑定在一起,你可以在Spring容器中将资源(如JDBC的DataSource或Hibernate的SessionFactory)定义为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.jpa.hibernate.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的JndiObjectFactoryBean<jee:jndi-lookup>来访问位于JNDI中的SessionFactory,以检索和暴露它。然而,在EJB上下文之外,这种情况通常并不常见。

备注

Spring还提供了一个LocalSessionFactoryBuilder变体,可以无缝地与@Bean风格的配置和编程式设置集成(无需使用FactoryBean)。

LocalSessionFactoryBeanLocalSessionFactoryBuilder都支持后台启动,Hibernate的初始化会在给定的启动执行器(如SimpleAsyncTaskExecutor)上与应用程序的启动线程并行进行。在LocalSessionFactoryBean中,这是通过bootstrapExecutor属性来实现的。而在编程式的LocalSessionFactoryBuilder中,有一个重载的buildSessionFactory方法,该方法接受一个启动执行器参数。

这样的原生Hibernate设置还可以提供一个JPA EntityManagerFactory,以便进行标准的JPA交互。详情请参见Native Hibernate Setup for JPA

基于纯Hibernate API实现DAO

Hibernate有一个名为“上下文会话(contextual sessions)”的功能,其中Hibernate本身每个事务会管理一个当前的Session。这大致相当于Spring在每个事务中同步一个Hibernate Session的做法。相应的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();
}
}

这种风格与Hibernate参考文档和示例中的风格类似,不同之处在于将SessionFactory保存在实例变量中。我们强烈推荐这种基于实例的设置方式,而不是Hibernate的CaveatEmptor示例应用程序中使用的传统static HibernateUtil类。(一般来说,除非绝对必要,否则不要在static变量中存储任何资源。)

前面的DAO示例遵循了依赖注入模式。它非常适合Spring IoC容器,就像使用Spring的HibernateTemplate编码时那样。你也可以用纯Java来设置这样的DAO(例如,在单元测试中)。为此,需要实例化它并调用setSessionFactory(..),传入所需的工厂引用。作为Spring Bean定义,DAO的结构大致如下:

<beans>

<bean id="myProductDao" class="product.ProductDaoImpl">
<property name="sessionFactory" ref="mySessionFactory"/>
</bean>

</beans>

这种DAO风格的主要优点在于它仅依赖于Hibernate API,无需导入任何Spring类。从非侵入性的角度来看,这一点非常吸引人,对于Hibernate开发者来说也可能感觉更加自然。

然而,DAO抛出的是普通的HibernateException(这种异常是未检查的,因此不需要声明或捕获),这意味着调用者只能将这类异常视为致命性错误——除非他们愿意依赖Hibernate自身的异常层次结构。如果不将调用者与实现策略绑定在一起,就无法捕获具体的异常原因(例如乐观锁定失败)。对于那些基于Hibernate的应用程序、不需要任何特殊异常处理的情况,或者两者兼有的情况来说,这种权衡可能是可以接受的。

幸运的是,Spring的LocalSessionFactoryBean支持Hibernate的SessionFactory.getCurrentSession()方法,适用于任何Spring事务策略,即使在使用HibernateTransactionManager的情况下也能返回当前由Spring管理的Session。该方法的标准行为是返回与正在进行的JTA事务(如果有的话)相关联的当前Session。无论您是使用Spring的JtaTransactionManager、EJB容器管理的事务(CMTs),还是JTA,这种行为都适用。

总之,你可以基于纯Hibernate API来实现DAO(数据访问对象),同时仍然能够参与由Spring管理的事务。

声明式事务划分

我们建议您使用Spring的声明式事务支持,该功能允许您用AOP事务拦截器替换Java代码中的显式事务划分API调用。您可以通过使用Java注解或XML在Spring容器中配置此事务拦截器。这种声明式事务能力可以让业务服务免于重复的事务划分代码,让您能够专注于添加业务逻辑,而这才是应用程序的真正价值所在。

备注

在继续之前,如果您还没有阅读过声明式事务管理,我们强烈建议您先阅读该文档。

你可以使用@Transactional注解来标注服务层,并指示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/> 入口元素,以便在运行时启用 @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.jpa.hibernate.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即可。同样,这个PlatformTransactionManager可以来自任何地方,但最好是通过setTransactionManager(..)方法以bean引用的形式提供。此外,productDAO也应该通过setProductDao(..)方法进行设置。以下两段代码片段展示了在Spring应用程序上下文中事务管理器和业务服务的定义,以及一个业务方法实现的示例:

<beans>

<bean id="myTxManager" class="org.springframework.orm.jpa.hibernate.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允许任何受检(checked)的应用程序异常与回调代码一起被抛出,而TransactionTemplate则仅允许在回调中抛出未受检(unchecked)异常。如果发生未受检的应用程序异常,或者应用程序通过设置TransactionStatus将事务标记为“仅回滚”(rollback-only),TransactionTemplate会触发回滚操作。默认情况下,TransactionInterceptor的行为与此相同,但它允许为每个方法配置不同的回滚策略。

事务管理策略

TransactionTemplateTransactionInterceptor 都将实际的事务处理委托给一个 PlatformTransactionManager 实例(对于单个 Hibernate SessionFactory,这个 PlatformTransactionManager 可以是 HibernateTransactionManager,它在内部使用 ThreadLocal 来管理 Session);而对于 Hibernate 应用程序来说,这个 PlatformTransactionManager 也可以是 JtaTransactionManager(它将事务处理委托给容器的 JTA 子系统)。你甚至可以使用自定义的 PlatformTransactionManager 实现。从原生 Hibernate 事务管理切换到 JTA 事务管理(例如在应用程序的某些部署中遇到分布式事务需求时),只需要进行配置上的调整即可。你可以用 Spring 的 JTA 事务实现来替换 Hibernate 事务管理器。事务的分界代码和数据访问代码无需任何修改就能继续使用,因为它们都使用了通用的事务管理 API。

对于跨多个Hibernate会话工厂的分布式事务,你可以将JtaTransactionManager作为事务策略与多个LocalSessionFactoryBean定义结合起来使用。每个DAO(数据访问对象)会通过其对应的bean属性获得一个特定的SessionFactory引用。如果所有底层的JDBC数据源都是支持事务的容器型数据源,那么一个业务服务就可以在任意数量的DAO和任意数量的会话工厂之间划分事务,而无需特别处理,只要它使用JtaTransactionManager作为事务策略即可。

HibernateTransactionManagerJtaTransactionManager都允许与Hibernate一起进行适当的JVM级别的缓存处理,无需依赖特定容器的事务管理器查找或JCA连接器(如果您不使用EJB来发起事务的话)。

HibernateTransactionManager可以将Hibernate JDBC的Connection导出为针对特定DataSource的普通JDBC访问代码。这种能力使得在不使用JTA的情况下,也能实现高级别的交易划分,尤其是在混合使用Hibernate和JDBC数据访问时。只要你只访问一个数据库,就可以做到这一点。如果你通过LocalSessionFactoryBean类的dataSource属性将传入的SessionFactoryDataSource关联起来,HibernateTransactionManager会自动将Hibernate交易暴露为JDBC交易。或者,你也可以通过HibernateTransactionManager类的dataSource属性明确指定要暴露交易的DataSource

为了实现类似JTA的懒加载实际资源连接的方式,Spring为目标连接池提供了一个相应的DataSource代理类:请参见LazyConnectionDataSourceProxy。这对于Hibernate的只读事务尤其有用,因为这些事务通常可以直接从本地缓存中处理,而无需访问数据库。

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

你可以在容器管理的JNDI SessionFactory和本地定义的SessionFactory之间切换,而无需修改应用程序的任何一行代码。是否将资源定义保留在容器中还是保留在应用程序内部,主要取决于你所使用的事务策略。与Spring定义的本地SessionFactory相比,手动注册的JNDI SessionFactory并没有提供任何额外的优势。通过Hibernate的JCA连接器部署SessionFactory可以带来参与Jakarta EE服务器管理基础设施的额外价值,但除此之外并没有其他实际意义。

Spring的事务支持并不依赖于特定的容器。即使使用非JTA的事务策略进行配置,事务支持仍然可以在独立环境或测试环境中正常工作。特别是在单数据库事务的典型场景下,Spring提供的单资源本地事务支持是一种轻量级且功能强大的替代方案,可以替代JTA。当你使用无状态的EJB会话bean来驱动事务时,即使你只访问一个数据库,并且仅通过容器管理的事务来实现声明式事务,你仍然需要依赖EJB容器和JTA。而直接以编程方式使用JTA,则还需要Jakarta EE环境。

Spring驱动的事务与本地定义的Hibernate SessionFactory配合使用,其效果与与本地JDBC DataSource配合使用时一样好,前提是它们都访问同一个数据库。因此,只有当存在分布式事务需求时,才需要使用Spring的JTA事务策略。而JCA连接器则需要特定的容器部署步骤,并且(显然)首先需要JCA的支持。这种配置比部署一个使用本地资源定义和Spring驱动事务的简单Web应用程序要复杂得多。

综合考虑,如果你不使用EJB,那就坚持使用本地的SessionFactory设置以及Spring的HibernateTransactionManagerJtaTransactionManager。这样你就可以获得所有好处,包括适当的JVM级事务缓存和分布式事务功能,而无需面对容器部署带来的不便。只有当通过JCA连接器将Hibernate的SessionFactory进行JNDI注册时(且这种注册是与EJB结合使用时),这种做法才有其价值。

使用Hibernate时出现的虚假应用服务器警告

在某些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 JtaTransactionManager bean 传递给 Hibernate 的配置。最简单的方法是在 LocalSessionFactoryBean bean 的 jtaTransactionManager 属性中引用该 bean(参见 Hibernate 事务配置)。这样,Spring 就会使得相应的 JTA 策略对 Hibernate 可用。

  • 你也可以在 LocalSessionFactoryBeanhibernateProperties 中显式配置 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事务是同步的,因此它会通过afterCompletion回调被JTA事务管理器调用回来。

  • 除此之外,这种同步还可能触发Spring对Hibernate的回调,通过Hibernate的afterTransactionCompletion回调(用于清除Hibernate缓存),随后会显式地对Hibernate会话执行close()操作,这会导致Hibernate尝试关闭JDBC连接。

  • 在某些环境中,这个Connection.close()操作会引发警告或错误,因为应用服务器不再认为该Connection是可用的了,因为事务已经提交。

当Hibernate配置为使用JTA事务管理器时,当一个JTA事务提交时,会发生以下事件:

  • JTA事务已准备好提交。
  • Spring的JtaTransactionManager与JTA事务同步,因此事务会通过beforeCompletion回调被JTA事务管理器调用回。
  • Spring知道Hibernate本身也与JTA事务同步,其行为与之前的场景不同。特别是,它会与Hibernate的事务资源管理保持一致。
  • JTA事务提交。
  • Hibernate与JTA事务同步,因此事务会通过afterCompletion回调被JTA事务管理器调用回,从而可以正确地清除其缓存。