第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统一异常体系的转译支持。