第15章 Spring对各种ORM的集成
- Spring对Hibernate的集成
- Spring对IBATIS的集成
- Spring中对其他ORM方案的集成概述
Spring对当前各种流行的ORM解决方案的集成主要体现在以下几个万面。
- 统一的资源管理方式。
- 特定于ORM的数据访问异常到Spring统一异常体系的转译。
- 统一的数据访问事务管理及控制方式。
以下内容将主要围绕Spring对Hibernate3和iBATIS 2这两种ORM方案的集成来进行讲述,最后会对其他ORM方案的集成做一定的概述,以便在你要使用这些ORM方案的时候,知道如何从Spring这里获得支持。
现在,就让我们开始这趟Spring中对各种ORM解决方案的集成之旅吧!
15.1 Spring对Hibernate的集成
在当今Java的各种ORM解决方案中,Hibernate凭借其先期的优势以及后期的持续跟进,无疑已经成为基于ORM进行数据访问的事实标准。
15.1.1 旧日“冬眠”时光
先从Hibernate参考文档所提供的Session管理工具类说起,先看一下下方代码清单的内容吧(摘自Hibernate2.1.8参考文档)!
public class HibernateUtil {
private static Log log = Logractory.getLog(Hibernateutll.class);
private static final SessionFactory sessionFactory;
static {
try {
// Create the SessionFactory
sessionFactory = new Configuration().configure().buildSessionFactory();
} catch (Throwable ex) {
log.error("Initial SessionFactory creation failed.", ex);
throw new ExceptionInInitializerError(ex);
}
}
public static final ThreadLocal session = new ThreadLocal();
public static Session currentSession() throws HibernateException {
Session s = (Session) session.get();
// Open a new Session, if this Thread has none yet
if (s == null) {
s = sessionFactory.openSession();
session.set(s);
}
return s;
}
public static void closeSession() throws HibernateException {
Session s = (Session) session.get();
session.set(null);
if (s != null) {
s.close();
}
}
}
HibernateUtil的作用是对Session的初始化,获取以及释放进行统一的管理。 坦诚地说,只从HibernateUtil本身来说,我们没有太多可以指摘的,但是,当大部分人都按照下方代码清单中示例代码的样子来使用HibernateUtil的时候,问题就比较容易出现了。
Session session = HibernateUtil.currentSession();
Transaction tx = session.beginTransaction();
Cat princess = new Cat();
princess.setName("Princess");
princess.setSex('F');
princess.setWeight(7.4f);
session.save(princess);
tx.commit();
HibernateUtil.closeSession();
仅关注这段使用代码好像没有什么不妥之处,可是,以同样的方式铺开到整个团队的开发中的话,就真的不妥了。
对于一个团队来说,工作的分配通常是按照功能模块划分的,而不是按照应用程序的分层划分的,这就意味着,每个开发人员通常要负责自己模块中每层的各种功能实现,具体点儿说就是,每个开发人员都需要负贵自己功能模块中的DAO以及Service层对象开发。
为了能够给每个开发人员开发DAO以及Service对象提供适度的“公共设施”,我们通常会将Hibernate的使用进行适当的封装,将公用的一些行为抽象到父类中定义,这样,我们就有了类似下方代码清单所示的DAO父类实现(忽略了异常处理和其他细节)。
public class AbstractHibernateDao {
private Session session = null;
private Transaction transaction = null;
public AbstractHibernateDao() throws HibernateException {
session = HibernateUtil.currentSession();
}
public Session getCurrentSession() {
return this.session;
}
public Transaction beginTransaction() {
return getCurrentSession().beginTransaction();
}
public void commit() {
if (this.transaction != null) {
this.transaction.commit();
}
}
public void rollback() {
if (this.transaction != null) {
this.transaction.rollback();
}
}
public void closeSession() {
HibernateUtil.closeSession();
}
}
开发人员要实现DAO,需要继承该AbstractHibernateDao,并在构造方法中调用父类的构造方法。这样一旦子类实例化完毕,即可获得相应的Session进行数据访问,如下方代码清单所示。
public class FooHibernateDao extends AbstractHibernateDao implements IBarDao {
public FooHibernateDao() {
super();
}
public void saveOperation(Object po) {
getCurrentSession().save(po);
}
...
}
因为事务控制通常在Service层,所以FooHibernateDao中没有过多涉及特定的事务API以及Session管理的代码。不过,在仔细看加入这些相关逻辑的Service实现类之后,就会发现以这种方式使用Hibernate会是一种多么糟糕的体验。下方代码清单给出的就是这样一个Service实现类。
public class FooService {
private FooHibernateDao dao;
public void someBusinessLogic() throws Exception {
try {
getDao().beginTransaction();
Object mockPO = new Object();
getDao().saveOperation(mockPO);
getDao().commit();
} catch (HibernateException e) {
getDao().rollbck();
throw e;
} catch (Exception e) {
getDao().rollbck();
throw e;
} finally {
getDao().closeSession();
}
}
...
public FooHibernateDao getDao() {
return dao;
}
public void setDao(FooHibernateDao dao) {
this.dao = dao;
}
}
如果只是一个类或一个方法,即使这样处理,只要谨慎一些,还是可以保证程序的正常运行的。可是,在团队中充斥着各色开发人员的时候,当编程规范不能得到严格执行的时候,这样的Hibernate使用方式以及类似结构的程序设计,对于整个团队来说都不是一件容易的事情。
要关注事务管理,同时也要关注资源(Session)的管理,还得适度进行异常的处理,也许每个开发人员都精明能千,但当所有这一切凑在一起的时候,就难免会有疏漏了。这其实也不难解释,以这样的实践方式来使用Hibermate为什么会时不时发生资源泄漏之类的事故了。
当然,以上示例只是抽象出来的,以分散的Hibernate API封装,来使用Hibernate进行数据访问的方式中的一种。实际上,只是同时关注数据访问中的多个方面,并且没有合适的封装方式来使用Hibernate。项目中类似于这样的代码应该并不少见,而这直接影响到软件产品的质量,这才是我们应该重点关注的。所以,我们应该寻求新的Hibernate实践,将开发人员从复杂的数据访问API的使用中解放出来,使其能够更加高效地工作,从而促进整个软件产品质量的提高。
15.1.2 “春天”里的“冬眠"
鉴于HibernateAPI的使用,在具体项目实践过程中,也存在类似于JDBC API使用中资源管理以及异常的处理之类的普遍问题。Spring在JdbcTemplate的成功理念下,对Hibernate的使用以相同的方式进行了封装,从而使得我们不用在资源管理以及异常处理方面投入过多的精力。
同时,Spring对所有的数据访问技术相关的事务管理,也通过AOP的形式剥离出去,进一步避免了过多的方面纠缠在一起的困境。
1. HibernateTemplate的登场
对于旧日的Hibernate操作的封装方式来说,没有能够对Session资源的管理,以及数据访问异常进行更为集中合理的处理。所以,Spring提供了HibernateTemplate对Hibernate的使用进行模板化封装,并且在模板方法内部统一进行数据访问异常的处理。
基于HibernateTemplate的Session资源管理
Session是使用Hibernate与关系数据库进行数据访问的“纽带”,所有的数据访问操作必须经由Session的支持才能完成。对于日常开发来说,我们应该更多关注的是如何使用Session进行数据访问,而至于Session的获取以及释放等资源管理问题,则应该尽可能从频繁的数据访问代码中解脱出去,以避免过多方面的纠缠。 过去的Hibernate使用方式,就是因为每个开发人员都需要同时关注Session资源的管理,以及具体的基于Session的数据访问逻辑,才会出现稍有不慎就出现资源泄漏的情况。
HibernateTemplate统一对Session的获取以及释放等管理逻辑进行封装,将Session管理尽量保持在一处进行。而对于不同的数据访问需求,HibernateTemplate提供了HibernateCallback回调接口,以便调用方可以根据各自的数据访问需求进行定制,完全不会限制Hibernate任何功能的发挥。
与JdbcTemplate的实现相似,HibernateTemplate所有的模板方法以一个主要的模板方法为中心,该方法通过HibernateCallback回调接口为调用方公开Session资源以进行数据访问。下方代码清单是这个主要的模板方法定义。
public Object execute(HibernateCallback action, boolean exposeNativeSession) throws DataAccessException {
Assert.notNull(action, "Callback object must not be null");
Session session = getSession();
boolean existingTransaction = (!isAlwaysUseNewSession() &&
(!isAllowCreate() || SessionFactoryUtils.isSessionTransactional(session,
getSessionFactory())));
if (existingTransaction) {
logger.debug("Found thread-bound Session for HibernateTemplate");
}
FlushMode previousFlushMode = null;
try {
previousFlushMode = applyFlushMode(session, existingTransaction);
enableFilters(session);
Session sessionToExpose = (exposeNativeSession ? session : createSessionProxy(session));
Object result = action.doInHibernate(sessionToExpose);
flushIfNecessary(session, existingTransaction);
return result;
} catch (HibernateException ex) {
throw convertHibernateAccessException(ex);
} catch (SQLException ex) {
throw convertJdbcAccessException(ex);
} catch (RuntimeException ex) {
// Callback code threw application exception...
throw ex;
} finally {
if (existingTransaction) {
logger.debug("Not closing pre-bound Hibernate Session after HibernateTemplate");
disableFilters(session);
if (previousFlushMode != null) {
session.setFlushMode(previousFlushMode);
} else {
// Never use deferred close for an explicitly new Session.
if (isAlwaysUseNewSession()) {
SessionFactoryUtils.closeSession(session);
} else {
SessionFactoryUtils.closeSessionOrRegisterDeferredClose(session, getSessionFactory());
}
}
}
}
}
因为该模板方法要处理的方面很多,所以,看起来比较复杂。不过,让我们先把事务管理相关的代码搁置一边,对该模板方法进行简化,这样就比较容易看出该模板方法的真面目了。简化后的核心模板方法定义见下方代码清单。
public Object execute(HibernateCallback action, boolean exposeNativeSession) throws DataAccessException {
Assert.notNull(action, "Callback object must not be null");
Session session = getSession();
try {
Object result = action.doInHibernate(sessionToExpose);
return result;
} catch (HibernateException ex) {
throw convertHibernateAccessException(ex);
} catch (SQLException ex) {
throw convertJdbcAccessException(ex);
} catch (RuntimeException ex) {
// Callback code threw application exception...
throw ex;
} finally {
closeSession(session);
}
}
实际上,去除事务相关的代码,为Hibernate操作提供一个模板方法就这么简单,只要将Session资源的管理纳入一个模板方法,而不是让它任意的散落到代码各处,我们就可以在很大程度上避免Session资源泄漏的危险。
如果愿意,我们可以在这个主要模板方法的基础上,为HibernateTemplate添加更多的数据访问操作,所有之后添加的模板方法最终都通过该主要模板方法执行数据访问。当然,这部分工作Spring也已经为我们做了,基本上也不需要我们自己劳神。
特定于Hibernate的数据访问异常转译
在简化后的HibernateTemplate核心模板方法中,除了对Session资源管理的代码,还需要我们关注的就是数据访问异常的处理。
Hibermate3之前的Hibernate数据访问异常类型是checkedexception,自然而然地就会出现本章开头的一幕,客户端需要根据特定的数据访问方式,捕获并处理特定于数据访问技术的异常类型。Spring从开始对Hibernate2提供支持的时候,就提供了SessionFactoryUtils工具类,帮助实现从HibernateException到Spring统一异常体系的转译。
随着越来越多的开发人员对uncheckedexception在数据访问领域存在合理性的认同,Hibermate3之后的HibernateException也改为了uncheckedexception类型,但将uncheckedexception的HibernateException转译到Spring的异常体系依然是值得做的一件事情。
SessionFactoryUtils类提供了convertHibernateAccessException静态方法进行HibernateException到Spring异常体系的转译(如下):
public static DataAccessException convertHibernateAccessException(HibernateException ex)
HibernateTemplate内部在实现HibernateException的异常转译的时候,最终也是委派该方法进行的。至于模板方法中需要处理的SQLException,HibernateTemplate则将这部分的转译工作转交给了SQLExceptionTranslator。通过学习前面的JdbcTemplate,我们对其并不陌生。
如果使用HibernateTemplate进行基于Hibernate的数据访问,我们无需关心具体的异常转译,因为HibernateTemplate内部可以很好地处理这一点。不过,即使不能够使用HibernateTemplate,也没关系,在基于Hibernate数据访问代码中,依然可以借助SessionFactoryUtils的convertHibernateAccessException方法进行异常的转译。
基本数据访问操作
HibernateOperations接口定义了能够通过HibernateTemplate使用的所有基于Hibermate的数据访问方法,HibernateTemplate实现了该接口。HibernateOperations的定义实在是太长,这里不详细罗列,在使用的过程中可以参考HibernateTemplate或者Hibernateoperations的Javadoc文档。
要使用HibernateTemplate进行数据访问,只要为其提供一个可以使用的SessionFactory实例即可。通过编程的方式提供还是通过Spring的IoC容器进行注入,完全可以根据具体应用场最来决定。
(1)基于HibernateTemplate的查询。HibernateTemplate为查询操作提供的模板方法比较丰富,主要分为如下4组。
这里不做介绍 有兴趣可以看原文
(2)基于HibernateTemplate的更新。我们这里的更新属于广义上的更新,包括数据的插入、更新和删除操作。HibernateTemplate为这些具体的操作同样提供了完善的模板方法支持,如下所述。
这里不做介绍 有兴趣可以看原文
2. Spring中的sessionFactory的配置及获取
Hibernate的SessionFactory,就好像JDBC的DataSource,它是所有数据访问资源的发源地。只有获取了SessionFactory的支持,后继的数据访问才能继续,这也是为什么HibernateTemplate必须拥有一个SessionFactory才能工作的原因。
要配置并构建一个SessionFactory,最简单的方法就是直接通过编程的方式获得,如下所示:
Configuration cfg = new Configuration()
.addResource("mappingResource.hbm.xm1")
...
.setProperty("hibernate.dialect", "org.hibernate.dialect.MySQLInnoDBDialect")
...;
SessionFactory sessionFactory = cfg.buildSessionFactory();
// sessionFactory 处于可用状态
不过,这里要说的是在Spring中如何配置和获取SessionFactory,这些基本的方式就先放在一边不谈。
LocalSessionFactoryBean
Spring采用FactoryBean的形式对SessionFactory的配置和获取进行封装,org.springframework.orm.hibernate3.LocalSessionFactoryBean
将是我们在Spring中配置和获取SessionFactory最为常用的方式。
LocalSessionFactoryBean作为一个FactoryBean实现,其对应的getobject()
方法将返回SessionFactory类型的对象。我们可以通过LocalSessionFactoryBean配置Hibernate数据访问相关的所有资源,包括DataSource、配置文件位置、映射文件位置甚至于处理LOB数据的LobHandler。
下方代码清单给出了LocalSessionFactoryBean在Spring的IoC容器中的常见配置情况。
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="c1ose">
<property name="url">
<value>$(jdbc.url)</value>
</property>
<property name="driverClassName">
<value>$(jdbc.driver]</value>
</property>
<property name="username">
<value>$(jdbc.username)</value>
</property>
<property name="password">
<value>$fjdbc.password)</value>
</property>
</bean>
<bean id="sessionFactory" class="org.springframework.onm.hibernate3.Loca1SessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="configLocation">
<value>org.spring21..hibernate.cfg.xml</value>
</property>
</bean>
Spring为了能够以统一的方式对各种数据访问方式的事务管理进行抽象,在通过LocalSessionFactoryBean构建SessionFactory的时候,使用的是容器内定义的DataSource定义,而不是使用Hibermate内部的ConnectionProvider。
实际上,在构建SessionFactory的实际过程中,Spring将根据传入的DataSource来决定为将要构建的SessionFactory提供什么样的ConnectionProvider实现。这些ConnectionProvider实现包括LocalDataSourceConnectionProvider以及TransactionAwareDataSourceConnectionProvider,我们可以在LocalSessionFactoryBean同一包下面找到它们。
在上方代码清单所示的容器配置文件中,只通过configLocation就完成了整个的LocalSessionFactoryBean的配置,这是因为通常的hibernate.cfg.xml配置文件中已经包含了几乎所有必需的配置信息,包括映射文件资源、各种Hibernate配置参数等。
AnnotationSessionFactoryBean
Hibernate从2.x版本就支持基于XML的映射文件来定义映射信息。不过,ORM的映射信息可以通过多种方式来表达。随着Java5的普及,越来越多的开发人员喜欢使用Java5中的注解来记录一些元数据信息。Hibernate在这一点上自然也不落后,通过Hibernate注解的支持,Hibernate中使用的映射信息也可以通过注解的形式来表达。而AnnotationSessionFactoryBean就是为那些通过注解获取映射信息的SessionFactory的配置以及构建而准备的。
AnnotationSessionFactoryBean是在LocalSessionFactoryBean基础上构建的,除了可以指定LocalSessionFactoryBean的配置项之外,AnnotationSessionFactoryBean追加定义了几个专门用于获取注解元数据定义类的配置项,包括annotatedClasses、annotatedPackages以及configurationClass。下方代码清单是AnnotationSessionFactoryBean的简单配置实例。
<bean id="sessionFactory" class="org.springframework.orm.hibennate3.annotation.AnnotationSessionFactoryBean">
<property name="dataSource" ref="dataSource">
<property name="annotatedClasses">
<list>
<value>cn.spring21..mapping.AnnotationMappingClass1</value>
<value>cn.spring21..mapping.AnnotationMappingClass1</value>
</list>
</property>
<propertyn ame="annotatedPackages">
<list>
<value>cn.spring21..package</value>
</list>
</property>
</bean>
因为标注有映射信息注解的类也可以在hibernate.cfg.xml
之类的统一的配置文件中配置,所以,这种情况下,也可以不使用annotateaClasses之类的属性,直接使用一个configLocation属性配置即可搞定。
通过JNDI获取SessionFactory
除了可以在本地直接构建SessionFactory实例使用,或者通过Spring的IoC容器注册LocalSessionFactoryBean或者AnnotationSessionFactoryBean来获取相应的SessionFactory支持,我们也可以将SessionFactory绑定到服务器端的JNDI服务上,当需要使用的时候,可以通过JNDI查询来获得。
但将SessionFactory通过JNDI或者JCA绑定到具体的容器中是否必要却有值得商榷的地方。通过LocalSessionFactoryBean或者AnnotationSessionFactoryBean,在Spring容器中本地定义并获取sessionFactory,对于通常的应用程序来说是最为合适的方式,而通过JNDI绑定到具体容器则无法获取任何益处。 SessionFactory所持有的配置信息以及资源很大一部分是应用程序独有的,比如DomainObject,这不同于DataSource,将这些独有的信息绑定到JNDI再去获取,就好像你自己家的电视机必须放到一个地方去保管,当你要看的时候,还需要到那个地方去申请领回一样。
但对于某些场景来说,通过JCA来注册SessionFactory确实可以获得一定的优势,如结合EJB使用的时候。
总之,是否要通过JNDI来获取SessionFactory应该根据具体应用程序的场景来决定,但一般情况下,基于SpringIoC容器的SessionFactory配置及构建,就已经是最佳方案了。
3. HibernateDaoSupport
与JdbcDaoSupport所完成的使命相同,即使我们可以在DAO中直接使用HibernateTemplate进行数据访问,但也没有必要每个开发人员在各自的每个DAO实现类中都声明一个HibernateTemplate的实例。通常我们会提供一个DAO的基类,其中声明HibernateTemplate的支持,这样,子类只需要使用父类提供的HibernateTemplate进行数据访问操作即可。
HibernateDaoSupport的出现让我们免于重新去发明这种基于HibernateTemplate的DAO基类的轮子。在实现基于HibernateTemplate的DAO实现类的时候,我们直接继承HibernateDaoSupport就可以获得HibernateTemplate的数据访问支持,如下方代码清单所示。
public class FooHibernateDao extends HibernateDaoSupport implements IFooDao {
public void insertNews(FXNewsBean newsBean) {
getHibernateTemplate().save(newsBean);
}
public void deleteNews(Object po) {
getHibernateTemplate().delete(po);
}
...
}
HibernateDaoSupport还为子类公开了异常转译的方法,如下所示:
protected final DataAccessException convertHibernateAccessException(HibernateException ex)
即使在具体的DAO实现类中使用原始的HibernateAPI进行数据访问,只要我们继承了HibernateDaoSupport,也可以获得HibernateException到Spring统一异常体系的转译支持。