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);