Hibernate
我们首先介绍Spring环境中Hibernate的使用, 并以此演示Spring集成ORM映射器的方法。 本节详细涵盖了许多问题,并展示了DAO 实现和事务划分的不同变体。这些模式中的大多数可以直接 应用于所有其他受支持的ORM工具。本章后面的部分将介绍 其他ORM技术并展示简要示例。
|
自 Spring Framework 7.0 起,Spring 要求 Hibernate ORM 7.x 用于 Spring 的
|
在 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>
您还可以访问 JNDI 定位的 SessionFactory,使用 Spring 的
JndiObjectFactoryBean / <jee:jndi-lookup> 来检索和暴露它。
然而,这在 EJB 上下文之外通常不常见。
|
Spring 还提供了一个 |
基于纯 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 事务策略,
返回当前 Spring 管理的事务性 Session,即使使用
HibernateTransactionManager 也是如此。该方法的标准行为仍然是
返回与正在进行的 JTA 事务(如果有)关联的当前 Session。
这种行为适用于您是使用 Spring 的 JtaTransactionManager、EJB 容器管理的事务 (CMT) 还是 JTA。
总之,您可以基于纯 Hibernate API 实现 DAO,同时仍然能够 参与 Spring 管理的事务。
声明式事务划分
我们建议您使用 Spring 的声明式事务支持,它允许您将 Java 代码中显式的事务划分 API 调用替换为 AOP 事务拦截器。您可以使用 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.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。同样,后者可以来自任何地方,但最好
通过 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>
-
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 实例(对于单个 Hibernate SessionFactory
,它可以是 HibernateTransactionManager,在底层使用 ThreadLocal Session),
或者对于 Hibernate 应用程序,可以是 JtaTransactionManager(委托给容器的 JTA 子系统)。
您甚至可以使用自定义 PlatformTransactionManager 实现。从原生 Hibernate 事务
管理切换到 JTA(例如,当您的应用程序的某些部署面临分布式事务要求时)
只是一个配置问题。您可以将 Hibernate 事务管理器替换为 Spring 的 JTA 事务实现。
事务划分和数据访问代码都可以无需更改地工作,因为它们
使用通用的事务管理 API。
对于跨多个 Hibernate 会话工厂的分布式事务,您可以将
JtaTransactionManager 作为事务策略与多个
LocalSessionFactoryBean 定义结合使用。每个 DAO 然后获得一个特定的 SessionFactory
引用传递到其相应的 bean 属性中。如果所有底层 JDBC 数据源
都是事务性容器数据源,则业务服务可以跨任意数量的 DAO 和任意数量的会话工厂划分事务,而无需特殊考虑,
只要它使用 JtaTransactionManager 作为策略即可。
HibernateTransactionManager 和 JtaTransactionManager 都允许与 Hibernate
进行适当的 JVM 级别缓存处理,无需容器特定的事务管理器
查找或 JCA 连接器(如果您不使用 EJB 启动事务)。
HibernateTransactionManager 可以将 Hibernate JDBC Connection 导出到特定 DataSource
的纯 JDBC 访问代码。此功能允许在不使用 JTA 的情况下,完全混合 Hibernate 和 JDBC 数据访问进行高级
事务划分,前提是您只访问一个数据库。如果您通过
LocalSessionFactoryBean 类的 dataSource 属性为传入的 SessionFactory
设置了 DataSource,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 仅在与 EJB 结合使用时才增加价值。
Hibernate 导致的虚假应用服务器警告
在某些 JTA 环境中,如果 XADataSource 实现非常严格(目前是
某些 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
JtaTransactionManagerbean 传递给您的 Hibernate 设置。最简单的方法 是将其作为 bean 引用传递给LocalSessionFactoryBeanbean 的jtaTransactionManager属性(请参阅 Hibernate 事务设置)。 然后 Spring 会将相应的 JTA 策略提供给 Hibernate。 -
您也可以在
LocalSessionFactoryBean的 "hibernateProperties" 中显式配置 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 事务同步,因此它通过 JTA 事务管理器的afterCompletion回调被调用。 -
除其他活动外,此同步可以触发 Spring 通过 Hibernate 的
afterTransactionCompletion回调(用于清除 Hibernate 缓存)对 Hibernate 的回调, 然后是 Hibernate 会话上的显式close()调用,这会导致 Hibernate 尝试close()JDBC Connection。 -
在某些环境中,此
Connection.close()调用会触发警告或 错误,因为应用程序服务器不再认为Connection可用, 因为事务已提交。
当 Hibernate 配置为感知 JTA 事务管理器时, JTA 事务提交时会发生以下事件:
-
JTA 事务准备提交。
-
Spring 的
JtaTransactionManager与 JTA 事务同步,因此事务通过 JTA 事务管理器的beforeCompletion回调被调用。 -
Spring 意识到 Hibernate 本身已与 JTA 事务同步,并且 行为与上一个场景不同。特别是,它与 Hibernate 的事务资源管理保持一致。
-
JTA 事务提交。
-
Hibernate 与 JTA 事务同步,因此事务通过 JTA 事务管理器的
afterCompletion回调被调用,并且可以 正确清除其缓存。