Hibernate

我们从在Spring环境中覆盖Hibernate 5开始,使用它来演示Spring在集成对象关系映射器(ORM)方面的方法。本节详细介绍了许多问题,并展示了DAO实现和事务划分的不同变体。这些模式大多可以直接转换到所有其他支持的ORM工具。本章的后续部分将涵盖其他ORM技术,并展示简要示例。

从Spring Framework 6.0开始,Spring要求Hibernate ORM 5.5+用于Spring的HibernateJpaVendorAdapter以及本地HibernateSessionFactory设置。我们建议使用Hibernate ORM 5.6作为该Hibernate版本的最后一个功能分支。

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

在Spring容器中设置SessionFactory

为了避免将应用程序对象与硬编码的资源查找绑定在一起,您可以将资源(如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>

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

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

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

从Spring Framework 5.1开始,这样的本地Hibernate设置还可以公开JPA EntityManagerFactory,用于标准JPA交互,与本地Hibernate访问并存。有关详细信息,请参阅JPA的本地Hibernate设置

基于纯Hibernate API实现DAO

Hibernate具有称为上下文会话的功能,其中Hibernate本身管理每个事务的当前Session。这大致相当于Spring每个事务同步一个HibernateSession。基于纯Hibernate API的相应DAO实现类似于以下示例:

  • 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()
	}
}
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>
HibernateException(未经检查,因此不必声明或捕获),这意味着调用者只能将异常视为通常致命的情况 - 除非他们希望依赖Hibernate自己的异常层次结构。捕获特定原因(例如乐观锁定失败)是不可能的,除非将调用者与实现策略绑定。这种权衡对于强烈基于Hibernate的应用程序、不需要任何特殊异常处理或两者都需要的应用程序可能是可以接受的。

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

声明式事务划分

我们建议您使用Spring的声明式事务支持,这使您可以在Java代码中用AOP事务拦截器替换显式事务划分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引用。此外,应该通过setProductDao(..)方法设置productDAO。以下代码片段显示了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仅限于回调中的未经检查的异常。在发生未经检查的应用程序异常或应用程序将事务标记为仅回滚(通过设置TransactionStatus)时,TransactionTemplate会触发回滚。默认情况下,TransactionInterceptor的行为方式相同,但允许每个方法配置可回滚策略。

事务管理策略

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

对于跨多个Hibernate会话工厂的分布式事务,您可以将JtaTransactionManager作为事务策略与多个LocalSessionFactoryBean定义结合使用。然后,每个DAO都会将一个特定的SessionFactory引用传递给其对应的bean属性。只要使用JtaTransactionManager作为策略,业务服务就可以跨任意数量的DAO和任意数量的会话工厂划分事务,而无需特别考虑。

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

HibernateTransactionManager可以将Hibernate JDBCConnection导出给特定DataSource的普通JDBC访问代码。只要访问一个数据库,这种能力就可以完全在没有JTA的情况下使用混合Hibernate和JDBC数据访问进行高级事务划分。如果您通过LocalSessionFactoryBean类的dataSource属性设置了传入的SessionFactoryDataSourceHibernateTransactionManager会自动将Hibernate事务公开为JDBC事务。或者,您可以通过HibernateTransactionManager类的dataSource属性明确指定要公开事务的DataSource

对于JTA风格的实际资源连接的延迟检索,Spring为目标连接池提供了相应的DataSource代理类:参见LazyConnectionDataSourceProxy。这对于Hibernate的只读事务特别有用,因为这些事务通常可以从本地缓存中处理,而不必访问数据库。

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

您可以在不更改任何应用程序代码的情况下在容器管理的JNDISessionFactory和本地定义的SessionFactory之间切换。将资源定义保留在容器中还是在应用程序内部本地定义主要取决于您使用的事务策略。与Spring定义的本地SessionFactory相比,手动注册的JNDISessionFactory不提供任何好处。通过Hibernate的JCA连接器部署SessionFactory提供了参与Jakarta EE服务器管理基础设施的附加价值,但除此之外并没有实际价值。

Spring的事务支持不受容器限制。当配置为除JTA之外的任何策略时,事务支持也可以在独立或测试环境中工作。特别是在单数据库事务的典型情况下,Spring的单资源本地事务支持是JTA的一种轻量且强大的替代方案。当您使用本地EJB无状态会话bean来驱动事务时,即使只访问单个数据库并且仅使用无状态会话bean通过容器管理的事务提供声明性事务,您也依赖于EJB容器和JTA。直接以编程方式使用JTA还需要Jakarta EE环境。

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

总的来说,如果您不使用EJB,那么请坚持使用本地SessionFactory设置和Spring的HibernateTransactionManagerJtaTransactionManager。您将获得所有的好处,包括适当的事务级别的JVM缓存和分布式事务,而无需容器部署的不便。通过JCA连接器注册HibernateSessionFactory只有在与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事务管理器来解决此类问题,从而与之同步(以及Spring)。您有两种选项可以做到这一点:

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

  • 您还可以显式配置Hibernate的与JTA相关的属性,特别是在LocalSessionFactoryBeanhibernateProperties上配置"hibernate.transaction.coordinator_class"、"hibernate.connection.handling_mode"和可能的"hibernate.transaction.jta.platform"(有关这些属性的详细信息,请参阅Hibernate的手册)。

本节的其余部分描述了在Hibernate是否意识到JTAPlatformTransactionManager的情况下发生的事件序列。

当Hibernate未配置任何关于JTA事务管理器的意识时,以下事件发生在JTA事务提交时:

  • JTA事务提交。

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

  • 在其他活动中,此同步可以通过Spring触发Hibernate的afterTransactionCompletion回调(用于清除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回调回调,并可以正确清除其缓存。