跳到主要内容

休眠

ChatGPT-4o 中英对照 Hibernate

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

备注

从 Spring Framework 6.0 开始,Spring 要求 Hibernate ORM 5.5+ 作为 Spring 的 HibernateJpaVendorAdapter 以及原生 Hibernate SessionFactory 设置。我们推荐使用 Hibernate ORM 5.6 作为该 Hibernate 版本的最后一个功能分支。

Hibernate ORM 6.x 仅作为 JPA 提供者(HibernateJpaVendorAdapter)支持。不再支持使用 orm.hibernate5 包的简单 SessionFactory 设置。我们建议在新的开发项目中使用 JPA 风格设置的 Hibernate ORM 6.1/6.2。

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

从本地 Jakarta Commons DBCP BasicDataSource 切换到 JNDI 位置的 DataSource(通常由应用服务器管理)只是配置的问题,如以下示例所示:

<beans>
<jee:jndi-lookup id="myDataSource" jndi-name="java:comp/env/jdbc/myds"/>
</beans>
xml

您还可以使用 Spring 的 JndiObjectFactoryBean / <jee:jndi-lookup> 来访问 JNDI 定位的 SessionFactory,以检索并暴露它。然而,这在 EJB 上下文之外通常并不常见。

备注

Spring 也提供了一个 LocalSessionFactoryBuilder 变体,与 @Bean 风格配置和编程设置无缝集成(不涉及 FactoryBean)。

LocalSessionFactoryBeanLocalSessionFactoryBuilder 都支持后台引导,Hibernate 初始化可以在应用程序引导线程的并行引导执行器(如 SimpleAsyncTaskExecutor)中运行。在 LocalSessionFactoryBean 上,可以通过 bootstrapExecutor 属性实现。在编程式的 LocalSessionFactoryBuilder 上,有一个重载的 buildSessionFactory 方法,可以接受一个引导执行器参数。

这种原生的 Hibernate 设置也可以为标准 JPA 交互提供一个 JPA EntityManagerFactory,以便于原生 Hibernate 访问。详情请参见 JPA 的原生 Hibernate 设置

基于 Plain Hibernate API 实现 DAO

Hibernate 有一个名为上下文会话的功能,其中 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();
}
}
java

这种风格类似于 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>
xml

这种 DAO 风格的主要优点是它仅依赖于 Hibernate API。不需要导入任何 Spring 类。从非侵入性的角度来看,这很有吸引力,并且可能让 Hibernate 开发人员感觉更自然。

然而,DAO 抛出的是普通的 HibernateException(这是一个未检查异常,因此不必声明或捕获),这意味着调用者只能将异常视为一般性的致命错误——除非他们想依赖 Hibernate 自身的异常层次结构。捕获特定原因(例如乐观锁定失败)是不可能的,除非将调用者与实现策略绑定。这种权衡可能对那些强烈依赖 Hibernate 的应用程序、无需特殊异常处理的应用程序或两者兼而有之的应用程序是可以接受的。

幸运的是,Spring 的 LocalSessionFactoryBean 支持 Hibernate 的 SessionFactory.getCurrentSession() 方法用于任何 Spring 事务策略,即使使用 HibernateTransactionManager,也能返回当前由 Spring 管理的事务性 Session。该方法的标准行为仍然是返回与正在进行的 JTA 事务关联的当前 Session(如果有)。无论您使用 Spring 的 JtaTransactionManager、EJB 容器管理事务 (CMT) 还是 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();
}
}
java

在容器中,你需要设置 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>
xml

编程式事务划分

您可以在应用程序的更高层次上划分事务,位于跨越任意数量操作的低级数据访问服务之上。对于周围业务服务的实现没有限制。它只需要一个 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>
xml
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...
}
});
}
}
java

Spring 的 TransactionInterceptor 允许任何已检查的应用程序异常与回调代码一起抛出,而 TransactionTemplate 在回调中仅限于未检查的异常。TransactionTemplate 在遇到未检查的应用程序异常或事务被应用程序标记为仅回滚(通过设置 TransactionStatus)时触发回滚。默认情况下,TransactionInterceptor 的行为方式相同,但允许每个方法配置回滚策略。

事务管理策略

TransactionTemplateTransactionInterceptor 都将实际的事务处理委托给一个 PlatformTransactionManager 实例(对于 Hibernate 应用程序,可以是一个 HibernateTransactionManager(通过使用 ThreadLocalSession 在底层实现,适用于单个 Hibernate SessionFactory)或一个 JtaTransactionManager(委托给容器的 JTA 子系统))。你甚至可以使用自定义的 PlatformTransactionManager 实现。从原生的 Hibernate 事务管理切换到 JTA(例如,当你的应用程序的某些部署需要分布式事务时)仅仅是配置的问题。你可以用 Spring 的 JTA 事务实现替换 Hibernate 事务管理器。因为它们使用通用的事务管理 API,所以事务划分和数据访问代码无需更改。

对于跨多个 Hibernate 会话工厂的分布式事务,可以将 JtaTransactionManager 作为事务策略与多个 LocalSessionFactoryBean 定义结合使用。然后,每个 DAO 都会将一个特定的 SessionFactory 引用传递到其对应的 bean 属性中。如果所有底层的 JDBC 数据源都是事务性的容器数据源,那么业务服务可以在不需要特别考虑的情况下跨任意数量的 DAO 和任意数量的会话工厂划分事务,只要它使用 JtaTransactionManager 作为策略即可。

HibernateTransactionManagerJtaTransactionManager 都允许在使用 Hibernate 时进行适当的 JVM 级缓存处理,而无需容器特定的事务管理器查找或 JCA 连接器(如果你不使用 EJB 来启动事务)。

HibernateTransactionManager 可以将 Hibernate JDBC Connection 导出到特定 DataSource 的普通 JDBC 访问代码。这种能力允许在不使用 JTA 的情况下,通过混合 Hibernate 和 JDBC 数据访问进行高级事务划分,前提是您只访问一个数据库。如果您通过 LocalSessionFactoryBean 类的 dataSource 属性为传入的 SessionFactory 设置了 DataSourceHibernateTransactionManager 会自动将 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 来驱动事务时,即使您只访问一个数据库并仅使用无状态会话 Bean 通过容器管理事务提供声明性事务,您也依赖于 EJB 容器和 JTA。以编程方式直接使用 JTA 也需要 Jakarta EE 环境。

Spring 驱动的事务可以像使用本地 JDBC DataSource 一样,与本地定义的 Hibernate SessionFactory 一起正常工作,前提是它们访问的是单个数据库。因此,只有在有分布式事务需求时,才需要使用 Spring 的 JTA 事务策略。JCA 连接器需要特定于容器的部署步骤,并且(显然)首先需要 JCA 支持。与使用本地资源定义和 Spring 驱动的事务部署简单的 Web 应用程序相比,这种配置需要更多的工作。

综上所述,如果你不使用 EJBs,建议使用本地 SessionFactory 设置以及 Spring 的 HibernateTransactionManagerJtaTransactionManager。这样你可以获得所有的好处,包括适当的事务性 JVM 级缓存和分布式事务,而无需容器部署的不便。只有在与 EJBs 一起使用时,通过 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 事务管理器来解决这些问题,它会与 Spring 一起进行同步。你有两种方法可以做到这一点:

  • 将 Spring 的 JtaTransactionManager bean 传递给 Hibernate 设置。最简单的方法是在 LocalSessionFactoryBean bean 的 jtaTransactionManager 属性中引用该 bean(参见 Hibernate 事务设置)。Spring 然后将相应的 JTA 策略提供给 Hibernate。

  • 你也可以显式配置 Hibernate 的 JTA 相关属性,特别是 "hibernate.transaction.coordinator_class"、"hibernate.connection.handling_mode" 和可能的 "hibernate.transaction.jta.platform" 在 LocalSessionFactoryBean 的 "hibernateProperties" 中(有关这些属性的详细信息,请参见 Hibernate 的手册)。

本节的其余部分描述了在有和没有 Hibernate 对 JTA PlatformTransactionManager 的感知情况下发生的事件序列。

当 Hibernate 未配置为感知 JTA 事务管理器时,JTA 事务提交时会发生以下事件:

  • JTA 事务提交。

  • Spring 的 JtaTransactionManager 与 JTA 事务同步,因此通过 JTA 事务管理器的 afterCompletion 回调被调用。

  • 在其他活动中,这种同步可以通过 Hibernate 的 afterTransactionCompletion 回调(用于清除 Hibernate 缓存)触发 Spring 对 Hibernate 的回调,随后对 Hibernate 会话进行显式的 close() 调用,这会导致 Hibernate 尝试 close() JDBC 连接。

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

当 Hibernate 配置为支持 JTA 事务管理器时,JTA 事务提交时会发生以下事件:

  • JTA 事务已准备好提交。

  • Spring 的 JtaTransactionManager 与 JTA 事务同步,因此事务通过 JTA 事务管理器的 beforeCompletion 回调被调用。

  • Spring 知道 Hibernate 本身与 JTA 事务同步,并且行为与之前的场景不同。特别是,它与 Hibernate 的事务性资源管理保持一致。

  • JTA 事务提交。

  • Hibernate 与 JTA 事务同步,因此事务通过 JTA 事务管理器的 afterCompletion 回调被调用,并可以正确清除其缓存。