Hibernate批量操作(一)


在項目的開發過程之中,我們常會遇到數據的批量處理問題。在持久層采用Hibernate框架時,在進行批量操作時,需要考慮Hibernate實現機制帶來的一些問題。

我們知道在每個Hibernate Session中都維持了一個必選的數據緩存,所有保存的實例都將保存在Session緩存中,這個緩存隨着Session的創建而存在,隨着Session的銷毀而消亡。這個內部緩存正常情況下是由Hibernate自動維護的,並且沒有容量限制。在批量插入與更新時,由於每次保存的實體都會保存在Session緩存中,當數據量大的時候,就可能出現OutOfMemoryException(內存溢出異常)。所以批量增加或更新操作中, 應該考慮到控制內部緩存的過度增長而出現OutOfMemeoryError錯誤。

這里有兩個方面進行控制:一是在數據保存過程中周期性的對Session調用flush和clear方法,確保Session的容量不至於太大。二是設置hibernate.jdbc.batch_size參數來指定每次提交SQL的數量。

周期性調用session的flush和clear方法,最簡單的一個方式是:設置一個計數器,每保存一個實例時,計數器加1。根據計數器的值決定是否需要將Session緩存中的數據刷入數據庫。

配置hibernate.jdbc.batch_size參數的原因就是盡量少讀數據庫,hibernate.jdbc.batch_size參數值越大,讀數據庫的次數越少,速度越快。

另外一個需要考慮的問題是在批量更新或批量刪除時,最簡單的思路是都是先將符合要求的數據查出來,然后再做更新/刪除操作。而且更新/刪除操作通常是逐行更新/刪除,即每更新/刪除一行記錄,都需要執行一條update/delete語句。這樣做的目的在於緩存同步,所以往往是一次批量更新/刪除往往執行的是1+N次操作. 第一次是查詢, 第二次是逐條更除/刪除。由於帶來的問題是內存消耗。特別是過大數據量時還可能在查詢時導致OutOfMemeoryError錯誤。

這里可以考慮使用session.iterator或者Query.iterate方法逐條獲取數據,或者采用基於游標的數據遍歷操作(JDBC驅動需支持游標), 通過游標來逐條獲取數據,從而控制內存的使用。(注:iterate方法首先從本地緩存中根據id查找對應的實體對象——類似Session.load方法,如果實體在緩存中已經存在,則直接以此數據對象作為查詢結果,如果沒找到,再執行相應的Select語句獲得對應的庫表記錄)

另外也可以采用Hibernate 3.0后提供的批量更新/刪除接口。Hibernate3.0 采用新的基於ANTLR的HQL/SQL查詢翻譯器,在Hibernate的配置文件中hibernate.query.factory_class屬性用來選擇查詢翻譯器。

1)選擇Hibernate3.0的查詢翻譯器:hibernate.query.factory_class= org.hibernate.hql.ast.ASTQueryTranslatorFactory

2)選擇Hibernate2.1的查詢翻譯器:hibernate.query.factory_class= org.hibernate.hql.classic.ClassicQueryTranslatorFactory

為了使用3.0的批量更新和刪除功能,只能選擇ASTQueryTranslatorFactory,否則不能解釋批量更新的語句。

注:ANTLR是用純Java語言編寫出來的一個編譯工具,它可生成Java語言或者是C++的詞法和語法分析器,並可產生語法分析樹並對該樹進行遍歷。ANTLR由於是純Java的,因此可以安裝在任意平台上,但是需要JDK的支持。

下面就分別從批量插入、批量更新和批量刪除3個方面總結一下Hibernate批量處理的情開。

批量增加

由於批量增加會帶來session級緩存的增長,所以我們一般在數據保存過程中周期性地調用Session的flush和clear方法。如下:

public <T> int batchSave(final T[] array) {

   Transaction tx = session.beginTransaction(); 

   for(int i = 0; i < array.length; i++) {

      session.save(array[i]);

      if (i % BATCH_MAX_ROW == 0) {

         session.flush();

         session.clear();

      }

   }

   session.flush();

   session.clear();

   tx.commit();

   return array.length;

}

上面代碼中,當i % BATCH_MAX_ROW == 0時,就手動Session處的緩存數據寫入數據庫。

其中,Session.flush()方法會完成兩個主要任務(見DefaultFlushEventListener類):1.刷新所有數據;2.執行數據庫SQL完成持久化動作;flush方法必須在操作結束且在提交事務和關閉連接之前被調用。Session.clear()則是清除session中的緩存數據。這樣就達到控制session的一級緩存的大小。

如果在Spring+Hibernate環境下,利用Spring提供的Hibernate模板,我們可以如下定義:

public <T> int batchSave(final T[] array) {

    int affectedRow = (Integer) getHibernateTemplate().execute(

        new HibernateCallback() {

            public Object doInHibernate(Session session)

                   throws HibernateException, SQLException {

                for (int i = 0; i < array.length; ++i) {

                    session.save(array[i]);

                    if (i % BATCH_MAX_ROW == 0) {

                        session.flush();

                        session.clear();

                    }

                }

                session.flush();

                session.clear();

                return array.length;

            }

    });

    return affectedRow;

}

 

我們也可以通過SQL的方式來批量增加數據,那么在JDBC中使用批量增加的情況如下:

Statement stmt = connection.createStatement();  

connection.setAutoCommit(false);//將Auto commit設置為false,不允許自動提交

stmt.addBatch("insert into employee values(23,'wang','man',20)");  

stmt.addBatch("insert into employee values(24,'xiaowu','woman',24)");  

stmt.executeBatch();   //將一批命令提交給數據庫來執行,如果全部命令執行成功,則返回更新計數組成的數組

connection.setAutoCommit(true);

以上代碼顯示的是利用JDBC Statement接口來處理批量增加的情況,其中,statement接口中的兩個方法:

   addBatch(String sql)——在批處理緩存中加入一條sql語句;

   executeBatch()——執行批處理緩存中的所有sql語句

另外一種方式是利用PrepareStatement來處理批量增加,如下:

PreparedStatement  pstm = connection.prepareStatement("insert into employee values(?,?,?,?)");  

connection.setAutoCommit(false);//將Auto commit設置為false,不允許自動提交  

//設置第一條語句 

pstm.setInt(1, 33);  

pstm.setString(2,"wang");  

pstm.setString(3, "man");  

pstm.setDouble(4, 20);  

pstm.addBatch();  //將一組參數添加到此 PreparedStatement 對象的批處理命令中。

//設置第二條語句  

pstm.setInt(1, 34);  

pstm.setString(2,"xiaowu");  

pstm.setString(3, "woman");  

pstm.setDouble(4, 24);  

pstm.addBatch();  

pstm.executeBatch();//將一批參數提交給數據庫來執行,如果全部命令執行成功,則返回更新計數組成的數組

connection.commit();

connection.setAutoCommit(true);//將Auto commit還原為true

其中, PreparedStatement接口中的兩個方法:

      addBatch()——將一組參數添加到PreparedStatement對象中。

      executeBatch()——將一批參數提交給數據庫來執行,如果命令執行成功,則返回更新計數組成的數組。

在以上的代碼中,只需使用Hibernate連接來改造上述代碼就可實現Hibernate利用JDBC來批量增加。

 

批量修改

批量增加的思路同樣適用於批量更新數據,如果需要返回多行數據,可以使用scroll()方法,從而可充分利用服務器端游標所帶來的性能優勢。如下代碼:為

String hqlString = ...;

Transaction tx = session.beginTransaction();

Iterator iter = session.find(hqlString).iterator();

//ScrollableResults users = session.createQuery(hqlString)

//    .setCacheMode(CacheMode.IGNORE)

//    .scroll(ScrollMode.FORWARD_ONLY);

int count=0;

while(iter.hasNext()){

  Oject obj = iter.next();

   // do something here….

  if (++count % BATCH_MAX_ROW == 0 ) {

     session.flush();

     session.clear();

  }

}

tx.commit();

session.close();

如前所述,我們應該力求避免出現先執行數據查詢進行數據逐行更新(即每更新一行記錄,都需要執行一條update語句)的情況。實際上,我們可考慮利用Hibernate提供的類似於SQL的批量更新/刪除的HQL語法來進行批量操作。

我們先看一下在SQL中進行批量更新的操作:

采用Statement接口進行更新:

Statement stmt = connection.createStatement();  

String sqlString = "update s set age=20 where id=s1";

stmt.executeUpdate(sql); 

采用PreparedStatement接口進行更新:

PreparedStatement stmt = connection.prepareStatement("update s set age=? where id=?");

stmt.setObject(1, "20");

stmt.setObject(2, "s1");

int i = statement.executeUpdate();

我們在Hibernate中繞過Hibernate API,通過JDBC API來執行SQL語句,示例:

String sqlString = ...;  //定義批量更新的SQL語句

tx = session.beginTransaction();

Connection con = session.connection();

PreparedStatement stmt = con.prepareStatement(sqlString);

//set the parameter

stmt.executeUpdate();

tx.commit();

利用Spring模板示例:

public Integer executeBySql(final String sqlString, final Object[] values)

            throws HibernateException, SQLException {

    return (Integer) getHibernateTemplate().execute(

        new HibernateCallback() {

            public Object doInHibernate(Session session) throws HibernateException, SQLException {

                Transaction tx = session.beginTransaction();

                Integer result = -1;

                try {

                    tx.begin();

                    SQLQuery query = session.createSQLQuery(sqlString);

                    for (int k = 0; k < values.length; k++) {

                        query.setParameter(k, values[k]);    //按位置進行綁定。。

                    }

                    result = query.executeUpdate();

                    tx.commit();

                } catch (HibernateException e) {

                     e.printStackTrace();

                     if (tx != null) {

                         tx.rollback();

                     }

                 } finally {

                      session.clear();

                 }

                 return result;

             }

         }

     );

}

利用Hiberante3提供的批量操作接口處理如下:

Session session = sessionFactory.openSession();

Transaction tx = session.beginTransaction();

String hqlUpdate = "update Customer set name = :newName where name = :oldName";

 int updatedEntities = s.createQuery( hqlUpdate )

          .setString( "newName", newName )

          .setString( "oldName", oldName )

          .executeUpdate();

 tx.commit();

 session.close();

由Query.executeUpdate()方法返回一個整型值,該值是受此操作影響的記錄數量。實際上,Hibernate的底層操作是通過JDBC完成的。因此,如果有批量的UPDATE或DELETE操作被轉換成多條UPDATE或DELETE語句,該方法返回的是最后一條SQL語句影響的記錄行數。

利用Spring模板示例:

    public Integer exeByHQL(final String hqlString, final Object[] params) {

        return (Integer) getHibernateTemplate().execute(

                new HibernateCallback() {

                    public Object doInHibernate(Session session) throws HibernateException, SQLException {

                       Integer result = -1;

                       Transaction tx = session.beginTransaction();

                        Query query = session.createQuery(hqlString);

                            for (int i = 0; i < params.length; i++)

                                query.setParameter(i, params[i]);

                         result = Integer.valueOf(query.executeUpdate());

                         tx.commit();

                         return result;

                    }

                });

    }

 

如果底層數據庫支持存儲過程,也可以通過存儲過程來執行批量更新。以下是利用Spring模板的一個例子:

public String executeByProcedure(final String sqlString, final Object[] values)

        throws HibernateException, SQLException {

    return (String) getHibernateTemplate().execute(

            new HibernateCallback() {

                public Object doInHibernate(Session session) throws HibernateException, SQLException {

                    try {

                        Connection connection = session.connection();

                        CallableStatement cstm = connection.prepareCall(sqlString);

                        String as[] = values;

                        int j = as.length;

                        for (int k = 0; k < j; k++) {

                            String s1 = as[k];

                            cstm.setString(k, s1);

                        }

 

                        cstm.executeUpdate();

                        String s = cstm.getString(1);

                        if (cstm != null)

                            cstm.close();

                        return s;

                    }

                    catch (RuntimeException runtimeException) {

                        runtimeException.printStackTrace();

                        throw runtimeException;

                    }

                }

            }

     );

}

在Hiberante中的示例如下;

Transaction tx = session.beginTransaction();
Connection con=session.connection();
String procedure = "{call batchUpdateXXX(?) }";
CallableStatement cstmt = con.prepareCall(procedure);
cstmt.setInt(1,0);
cstmt.executeUpdate();
tx.commit();

 

批量刪除

批量刪除與批量修改類似,以下是通過Hibernate3.0執行批量刪除的程序代碼:

代碼

Session session = sessionFactory.openSession();

 Transaction tx = session.beginTransaction();

 String hqlDelete = "delete from Customer where name = :oldName";

 int deletedEntities = s.createQuery( hqlDelete )

        .setString( "oldName", oldName )

        .executeUpdate();

 tx.commit();

 session.close();

 

通過JDBC API執行相關的SQL語句或調用相關的存儲過程是批量更新和批量刪除的理想方式,它具有以下優點:

(1) 不會消耗大量內存。因為它不再將數據庫中的大批量數據先加載到內存中,然后再逐個更新或修改。

(2) 可以在一條SQL語句中更新或刪除大批量的數據。

無論是直接引用SQL方式還是使用Hibernate 3的bulk接口, 通過一條的SQL完成對數據的批量刪除/更新都在一個問題:無法解決緩存同步問題(包括一級緩存和二級緩存)。為了保持緩存數據的一致性,簡單的辦法就是清空緩存數據。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM