Hibernate
我们从在 Spring 环境中使用 Hibernate 5 开始,使用它来演示 Spring 采用集成 OR 映射器的途径。本节详细介绍了许多问题,并展示了 DAO 实现和事务分隔的不同变体。其中大多数模式可以直接转换为所有其他受支持的 ORM 工具。本章后面的章节将介绍其他 ORM 技术并展示简短的示例。
从 Spring Framework 6.0 开始,Spring 要求 Hibernate ORM 5.5+ 用于 Spring 的 Hibernate ORM 6.x 仅作为 JPA 提供程序( |
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 还提供了一个
从 Spring Framework 5.1 开始,这样的原生 Hibernate 设置还可以公开一个 JPA |
基于纯 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
的行为方式相同,但允许对每个方法配置回滚策略。
事务管理策略
TransactionTemplate
和 TransactionInterceptor
都将实际事务处理委托给 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
作为策略即可。
HibernateTransactionManager
和 JtaTransactionManager
都允许使用 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 的 HibernateTransactionManager
或 JtaTransactionManager
。您将获得所有好处,包括适当的事务性 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
回调调用事务,并且可以正确清除其缓存。