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