Hibernate深入淺出(十二)Hibernate實用技術(分頁、Session管理、Spring Framework)

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



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章