1. Hibernate分页

数据分页显示,在系统实现中往往会带来较大的工作量,对于基于JDBC的程序而言,不同数据库提供的分页(部分读取)模式往往不同,也带来了数据库间可移植性上的问题。
Hibernate中,通过对不同数据库的统一接口设计,实现了透明化、通用化的分页实现机制。
我们可以通过Criteria.setFirstResult和Criteria.setFetchSize方法设定分页范围,如:

Criteria criteria = session.createCriteria(TUser.class);
criteria.add(Expression.eq("age","20"));
//从检索结果中获取第100条记录开始的20条记录
criteria.setFirstResult(100);
criteria.setFetchSize(20);

同样,Query接口也提供了与其一致的方法。
Hiberante中,抽象类net.sf.hibernate.dialect指定了所有底层数据库的对外统一接口。通过针对不同数据库提供相应的dialect实现,数据库之间的差异性得以消除,从而为上层机制提供了透明的、数据库无关的存储层基础。
对于分页机制而言,dialect中定义了一个方法如下:

public String getLimitString(
    String querySelect,
    boolean hasOffset
)

此方法用于在现有Select语句基础上,根据各数据库自身特性,构造对应的记录返回限定子句。如MySQL中对应的记录限定子句为Limit,而Oracle中,可通过rownum子句实现。
来看MySQLDialect中的getLimitString实现:

public String getLimitString(String sql, boolean hasOffset){
    return new StringBuffer(sql.length()+20)
        .appeng(sql)
        .appeng(hasOffset ?" limit ?,?" : " limit ?")
        .toString();
}

从上面可以看到,MySQLDialect.getLimitString方法的实现实际上是在给定的Select语句后追加MySQL所提供的专有SQL子句limit来实现。
下面是Oracle9Dialect中的getLimitString实现,其中通过Oracle特有的rownum子句实现了数据的部分读取。

public String getLimitString(String sql, boolean hasOffset){
    StringBuffer pagingSelect = new StringBuffer(sql.length()+100);
    if(hasOffset){
        pagingSelect.append("select * from (select row_.*,rownum rownum_ from(");
    }else{
        pagingSelect.append("select * from (");
    }
    paginSelect.appeng(sql);
    if(hasOffset){
        pagingSelect.append(") row_ where rownum<=?)  where rownum_>?");
    }
    else{
        pagingSelect.append(") where rownum<=?");
    }
    return pagingSelect.toString();
}

大多数主流数据库都提供了数据部分读取机制,而对于某些没有提供过相应机制的数据库而言,Hibernate也通过其它途径实现了分页,如通过Scrollable ResultSet,如果JDBC不支持Scrollable ResultSet,Hibernate也会自动通过ResultSet的next方法进行记录定位。
这样,Hibernate通过底层对分页机制的良好封装,使得开发人员无需关心数据分页的细节实现,将数据逻辑和存储逻辑分离开来,在提高生产效率的同时,也大大加强了系统在不同数据库平台之间的可移植性。


2. Session管理

无疑Session是Hibernate运作的灵魂,作为贯穿Hibernate应用的关键,Session中包含了数据库操作相关的状态信息。如对JDBC Connection的维护,数据实体的状态维持等。
对Session进行有效管理的意义,类似JDBC程序设计中对于JDBC Connection的调度管理。有效的Session管理机制,是Hibernate应用设计的关键。
大多数情况下,Session管理的目标聚焦于通过合理的设计,避免Session的频繁创建和销毁,从而避免大量的内存开销和频繁的JVM垃圾回收,保证系统高效平稳运行。
在各种Session管理方案中,ThreadLocal模式得到了大量使用。
ThreadLocal是Java中一种较为特殊的线程绑定机制。通过ThreadLocal存取的数据,总是与当前线程相关,也就是说,JVM为每个运行的线程,绑定了私有的本地实例存取空间,从而为多线程环境常出现的并发访问问题提供了一种隔离机制。
首先,我们需要知道,SessinFactory负责创建Session, SessionFactory是线程安全的,多个并发线程可以同时访问一个SessionFactory并从中获取Session实例。
Session并非线程安全,也就是说,如果多个线程同时使用一个Session实例进行数据存取,则将会导致Session数据存取逻辑混乱。下面是一个典型的Servlet,我们试图通过一个类变量session实现Session的重用,以避免每次操作都要重新创建:

public class TestServlet entends HttpServlet{
    private Session session;
    public void doGet(HttpServletRequest request, HttpServletResponse response){
        session = getSession();
        doSomething();
        session.flush();
    }
    public void doSomething(){
        …//基于session的存取操作
    }
}

代码看上去正确无误,甚至在单机测试的时候可能也不会发生什么问题,但这样的代码一旦编译部署到实际运行环境中,接踵而来的莫名其妙的错误很可能会使得我们摸不着头脑。
问题出在哪里?
首先,Servlet运行是多线程的,而应用服务器并不会为每个线程都创建一个Servlet实例,也就是说,TestServlet在应用服务器中只有一个实例(在tomcat中是这样,其他的应用服务器可能有不同的实现),而这个实例会被许多个线程并发调用,doGet方法也将被不同的线程反复调用,可想而知,每次调用doGet方法,这个唯一的TestServlet实例的session变量都会被重置,线程A的运行过程中,其他的线程如果也被执行,那么session的引用将发生改变,之后线程A再调用session,可能此时的session与之前所使用的session就不再一致,显然,错误也就不期而至。
ThreadLocal的出现,使得这个问题迎刃而解。
对上面的例子进行一些小小的修改:

public class TestServlet entends HttpServlet{
    private ThreadLocal localSession = new ThreadLocal();;
    public void doGet(HttpServletRequest request, HttpServletResponse response){
        localSession .set(getSession());
        doSomething();
        session.flush();
    }
    public void doSomething(){
        Session session = (Session)localSession.get();
        …//基于session的存取操作
    }
}

可以看到,localSession是一个ThreadLocal类型的对象,在doGet方法中,我们通过其set方法将获取的session实例保存,而在doSomething方法中,通过get方法取出session实例。
这也就是ThreadLocal的独特之处,它会为每个线程维护一个私有的变量空间。实际上,其实现原理是在JVM中维护一个Map,这个Map的key就是当前的线程对象,而value则是线程通过ThreadLocal.set方法保存的对象实例。当线程调用ThreadLocal.get方法时,ThreadLocal会根据当前线程对象的引用,取出Map中对应的对象返回。
这样,ThreadLocal通过以各个线程对象的引用作为区分,从而将不同线程的变量隔离开来。
回到上面的例子,通过应用ThreadLocal机制,线程A的session实例只能为线程A所用,同样,其他线程的session实例也各自从从属自己的线程。这样,我们就实现了线程安全的session共享机制。
Hibernate官方开发手册的示例中,提供了一个通过ThreadLocal维护session的好榜样:

public class HibernateUtil {
    private static SessionFactory sessionFactory;
    static{
        try{
            //create the sessionFactory
            sessionFactory = new Configuration().configure().buildSessionFactory();
        }catch(HibernateException ex){
            throw new RuntimeException(“Configuration problem: “+ ex.getMessage(), 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();
    }
}

在代码中,只要借助上面这个工具类获取Session实例,我们就可以实现线程范围内的Session共享,从而避免了在线程中频繁的创建和销毁Session实例。不过注意在线程结束时关闭Session。
同时值得一提的是,当前版本的Hibernate在处理Session的时候已经内置了延迟加载机制,只有在真正发生数据库操作的时候,才会从数据库连接池获取数据库连接,我们不必过于担心Session的共享会导致整个线程生命周期内数据库连接被持续占用。


上面的HibernateUtil类可以应用在任何类型的Java程序中。特别的,对于Web程序而言,我们可以借助Servlet2.3规范中新引入的Filter机制,轻松实现线程生命周期内的Session管理(关于Filter的具体描述,参考Servlet2.3规范)。
Filter的生命周期贯穿了其所覆盖的Servlet(JSP也可以看作是一种特殊的Servlet)及其底层对象。Filter在Servlet被调用之前执行,在Servlet调用结束之后结束。因此,在Filter中管理Session对于Web程序而言就显得水到渠成。下面是一个通过Filter进行Session管理的典型案例:

public class PersistenceFilter implements Filter{
    protected static ThreadLocal hibernateHolder = new ThreadLocal();
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException{
        hibernateHolder.set(getSession());
        try{
            …
            chain.doFilter(request, response);
            …
        }finally{
            Session sess = (Session)hibernateHolder.get();
            if(sess!=null){
                hibernateHolder.set(null);
                try{
                    sess.close();
                }catch(HibernateException ex){
                    throw new ServletException(ex);
                }
            }
        }
    }
    …
}

通过在doFilter中获取和关闭Session,并在周期内运行的所有对象(Filter链中其余的Filter,及其覆盖的Servlet和其他对象)对此Session实例进行重用,保证了一个HttpRequest处理过程中只占用一个Session,提高了整体性能表现。
在实际设计中,大多数情况下Session重用做到线程级别一般已经足够,企图通过HttpSession实现用户级的Session重用反而可能导致其他的问题。凡事不能过火,Session重用也一样。

3. Hibernate与Spring Framework

下面主要就Hibernate在Spring Framework中的应用加以介绍,关于Spring Framework请参见《深入浅出Spring Framework》。
Spring的参数化事务管理功能相对强大,笔者建议在基于Spring Framework的应用开发中,尽量使用容器管理事务,以获得数据逻辑代码的最佳可读性。下面的介绍,将略过代码控制的事务管理部分,而将重点放在参数化的容器事务管理应用。代码级事务管理实现原理参见《深入浅出Spring Framework》相关内容。
首先,针对Hibernate,需进行如下配置:

Hibernate-Context.xml

<beans>
    <bean id=”dataSource”
        class=”org.apache.common.dbcp.BasicDataSource”
        destroy-method=”close”>
        <property name=”driverClassName”>
            <value>net.sourceforge.jtds.jdbc.Driver</value>
        </property>
        <property name=”url”>
            <value>jdbc:jtds:sqlserver://127.0.0.1:1433/sample</value>
        </property>
        <property name=”username”>
            <value>test</value>
        </property>
        <property name=”password”>
            <value>changeit</value>
        </property>
    </bean>
    <bean id=”sessionFactory”
        class=”org.springframework.orm.hibernate.LocalSessionFactoryBean”>
        <property name=”dataSource”>
            <ref local=”dataSource”/>
        </property>
        <property name=”mappingResources”>
            <list>
                <value>net/xiaxin/dao/entity/User.hbm.xml</value>
            </list>
        </property>
        <property name=”hibernateProperties”>
            <props>
                <prop key=”hibernate.dialect”>
                    net.sf.hibernate.dialect.SQLServerDialect
                </prop>
                <prop key=”hibernate.show_sql”>
                    true
                </prop>
            </props>
        </property>
    </bean>
    <bean id=”transactionManager”
        class=”org.springframework.orm.hibernate.HibernateTransactionManager”>
        <property name=”sesisonFactory”>
            <ref local=”sessionFactory”/>
        </property>
    </bean>
    <bean id=”userDAO” class=”net.xiaxin.dao.UserDAO”>
        <property name=”sessionFactory”>
            <ref local=”sessionFactory”/>
        </property>
    </bean>
    <bean id=”userDAOProxy”
        class=”org.springframework.transaction.interceptor.
TransactionProxyFactoryBean”>
        <property name=”transactionManager”>
            <ref bean=”transactionManager”/>
        </property>
        <property name=”target”>
            <ref local=”userDAO”/>
        </property>
        <property name=”transactionAttributes”>
            <props>
                <prop key=”insert*”>PROPAGATION_REQUIRED</prop>
                <prop key=”get”>PROPAGATION_REQUIRED,readOnly</prop>
            </props>
        </property>
    </bean>
<beans>

其中:

1)SessionFactory的配置
Hibernate中通过SessionFacctory创建和维护Session,Spring对SessionFactory的配置也进行了整合,无需再通过Hibernate.cfg.xml对SessionFactory进行设定。
SessionFactory节点的mappingResources属性包含了映射文件的路径,list节点下可配置多个映射文件。
hibernateProperties节点则容纳了所有的属性配置。
可以对应传统的hibernate.cfg.xml文件结构对这里的SessionFactory配置进行解读。
2)采用面向Hibernate的TransactionManager实现
org.springframework.orm.hibernate.HiberateTransactionManager
这里引入了一个非常简单的库表:Users,建立如下映射类:
User.java

/**@hibernate.class  table=”users”*/
public class User{
    public Integer id;
    public String username;
    public String password;
    /**@hibernate.id column=”id” type=”java.lang.Integer”
            generate-class=”native”*/
    public Integer getId(){
        return id;
    }
    public void setId(Integer id){
        this.id = id;
}
    /**@hibernate.property column=”password” length=”50”*/
    public String getPassword(){
        return password;
    }
    public void setPassword(String password){
        this.password = password;
    }
    /**@hibernate.property column=”username” length=”50”*/
    public String getUsername(){
        return username;
    }
    public void setUsername(String username){
        this.username = username;
    }
}

上面的代码中,通过xdoclet指定了类/表、属性/字段的映射关系,通过xdoclet ant task我们可以根据代码生成对应的user.hbm.xml文件。
下面是生成的user.hbm.xml:

<hibernate-mapping>
    <class name=”net.xiaxin.dao.entity.User”
        table=”users”
        dynamic-update=”false”
        dynamic-insert=”false”>
        <id name=”id”
            column=”id”
            type=”java.lang.Integer”>
            <generator class=”native”/>
        </id>
        <property name=”password”
            column=”password”
            type=”java.lang.String”
            update=”true”
            insert=”true”
            access=”property”
            length=”50”/>
        <property name=”username”
            column=”username”
            type=”java.lang.String”
            update=”true”
            insert=”true”
            access=”property”
            length=”50”/>
    </class>
</hibernate-mapping>

UserDAO.java

public class UserDAO extends HibernateDaoSupport implementd IUserDAO{
    public void insertUser(User user){
        getHibernateTemplate().saveOrUpdate(user);
    }
}

看到这段代码想必会有点诧异,似乎太简单了一点,不过已经足够。短短一行代码我们已经实现了与上一章中示例相同的功能,这也正体现了Spring+Hibernate的威力所在。
上面的UserDAO实现了自定义的IUserDAO接口,并扩展了抽象类: HibernateDaoSupport。
HibernateDaoSupport实现了HibernateTemplate和SessionFactory实例的关联。
HibernateTemplate对Hibernate Session操作进行了封装,而HibernateTemplate.execute方法则是一封装机制的核心,有兴趣可以研究其实现机制。
借助HibernateTemplate我们可以脱离每次数据操作必须首先获得Session实例、启动事务、提交/回滚事务以及try/catch/finally等繁杂的操作。从而获得以上代码中精于集中的逻辑呈现效果。
对比下面这段实现了同样功能的Hibernate原生代码,想必更有体会:

Session session;
try{
    Configuration config = new Configuration().configure();
    SessionFactory sessionFactory = config.buildSessionFactory();
    session = sessionFactory.openSession();
    Transaction tx = session.beginTransaction();
    User user = new User();
    user.setName(“Erica”);
    user.setPassword(“mypass”);
    session.save(user);
    tx.commit();
}catch(HibernateException e){
    e.printStackTrace();
    tx.rollback();
}finally{
    session.close();
}

附上例的测试代码:

InputStream is = new FileInputStream(“Hibernate-Context.xml”);
    XmlBeanFactory factory = new XmlBeanFactory(is);
IUserDAO userDAO = (IUserDAO)factory.getBean(“userDAOProxy”);
User user = new User();
user.setUsername(“Erica”);
user.setPassword(“mypass”);
userDAO.insertUser(user);