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://127.0.0.1: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 的 JndiObjectFactoryBean / <jee:jndi-lookup> 访问 JNDI 定位的 SessionFactory,以检索并公开它。但是,这通常在 EJB 上下文之外并不常见。

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

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

从 Spring Framework 5.1 开始,这样的原生 Hibernate 设置还可以公开一个 JPA EntityManagerFactory,用于在原生 Hibernate 访问旁边进行标准 JPA 交互。有关详细信息,请参阅 JPA 的原生 Hibernate 设置

基于纯 Hibernate API 实现 DAO

Hibernate 有一个称为上下文会话的功能,其中 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。为此,实例化它并使用所需的工厂引用调用 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 容器管理事务 (CMT) 还是 JTA,此行为都适用。

总之,您可以基于普通的 Hibernate API 实现 DAO,同时仍然能够参与 Spring 管理的事务。

声明式事务划分

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

在继续之前,如果您尚未阅读 声明式事务管理,我们强烈建议您阅读。

您可以使用 @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 的行为方式相同,但允许对每个方法配置回滚策略。

事务管理策略

TransactionTemplateTransactionInterceptor 都将实际事务处理委托给 PlatformTransactionManager 实例(它可以是 HibernateTransactionManager(对于单个 Hibernate SessionFactory),通过使用底层的 ThreadLocal Session)或 JtaTransactionManager(委托给容器的 JTA 子系统)对于 Hibernate 应用程序。您甚至可以使用自定义 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 访问代码。此功能允许对混合 Hibernate 和 JDBC 数据访问进行高级事务划分,完全不需要 JTA,前提是您只访问一个数据库。如果您已通过 LocalSessionFactoryBean 类的 dataSource 属性使用 DataSource 设置了传入的 SessionFactory,则 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,即使您只访问一个数据库并只使用无状态会话 bean 通过容器管理的事务来提供声明性事务也是如此。以编程方式直接使用 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 中虚假的应用程序服务器警告

在一些具有非常严格的 XADataSource 实现的 JTA 环境中(当前一些 WebLogic Server 和 WebSphere 版本),当 Hibernate 在不考虑该环境的 JTA 事务管理器的情况下进行配置时,应用程序服务器日志中可能会出现虚假的警告或异常。这些警告或异常表明正在访问的连接不再有效,或者 JDBC 访问不再有效,可能是因为事务不再处于活动状态。例如,下面是 WebLogic 中的实际异常

java.sql.SQLException: The transaction is no longer active - status: 'Committed'. No
further JDBC access is allowed within this transaction.

另一个常见问题是 JTA 事务后的连接泄漏,Hibernate 会话(以及潜在的底层 JDBC 连接)未正确关闭。

您可以通过使 Hibernate 了解 JTA 事务管理器来解决此类问题,它会与 JTA 事务管理器同步(以及 Spring)。您有两种选择来执行此操作

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

  • 您还可以显式配置 Hibernate 的 JTA 相关属性,特别是 LocalSessionFactoryBean 上的 "hibernateProperties" 中的 "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 的回调,然后显式 close() 调用 Hibernate 会话,这会导致 Hibernate 尝试 close() JDBC 连接。

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

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

  • JTA 事务已准备好提交。

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

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

  • JTA 事务提交。

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