Clob和Blob對象的存取

作爲測試,我們新建一個TUser對象,其image屬性中,保存了一個圖片文件的二進制內容。而其resume屬性,我們以一個簡單的字符串作爲填充。

 

 1 TUser user  = new  TUser(); 
 2
 3 user.setAge( new  Integer( 20 )); 
 4
 5 user.setName( " Shark " ); 
 6
 7 FilelnputStream imgis  = new  FileinputStream( " C://inimage.jpg "  
 8
 9 Blob img  =  Hibernate.createBlob(imgis); 
10
11 user.setlmage(img); 
12
13 Clob resume  =  Hibernate.createClob( " This is Clob " ); 
14
15 user. setResume(resume); 
16
17 Transaction tx  =  session.beginTransaction(); 
18
19 session.save(user); 
20
21 tx.commit(); 
22
23

 

上面的代碼中,我們通過Hibemate.createBlobHibemate.createClob創建了對應的BlobClob對象。其中Blob對象基於一個FileInputStream構建,而Clob對象基於一個字符串構建。

 

完成了寫入操作,對應的讀取操作代碼如下:

 

 1 //  假設庫表記錄的id字段等於3 
 2 TUser user = (TUser)  session.load(TUger.elaa., load(TUser. class ,   new  Integer( 3 )); 
 3 Clob resume = user.getResume(); 
 4 //  通過Clob.getSubString()方法獲取Clob字段內容 
 5 System.out.println( " User resume=> " + resume.getSubString( 1 ,( int )resume.length())); 
 6 Blob img  =  user.getImage(); 
 7 //  通過Blob.getBinaryS=ream()方法獲取二進制流 
 8 InputStream is  =  img.getBinaryStream(); 
 9 FileOutputStream fos = new  FileOutputStream( " C://outimage.jpg " ); 
10 byte [] buf = new   byte ( 102400 ); 
11 int  len; 
12 while ((len  =  is.read(buf)) !=- 1 )
13     fos.write(buf, 0 ,len); 
14 }
 
15 fos.close(); 
16 is.close(); 
17
18

 

通過上面的代碼,我們完成了針對SQLServerBlob/Clob型字段操作.看起來非常簡單,不過,並非侮種數據庫都如此友善。讓我們接着來看看Oracle數據庫下的Blob/Clob字段讀寫,

通過修改hibernate.cfg.xml中的Dialect配置完成數據庫切換後,我們再次運行上面的TUser對象保存例程。

程序運行期間拋出異常:

 

Hibernate:select hibernate_sequence.nextval from dual

Hibernate:insert into T_USER (name, age,  image,resume. id) values(?, ?, ?, ?, ?)

17:27:24,161 ERROR JDBCExceptionReporter:58 - - 不允許的操作: Streams type cannot be used in batching

17:27:24,171 ERROR Sessionlmpl:2399 - Could not synchronize database state with session

net.sf.hibernate.exception.GenericJDBCException:could not insert:[com.redsaga.hibernate.db.entity.TUser#6]

...

 

觀察異常信息:streams type cannot be used in batching.這意味着Oracle JDBC不允許流操作以批量方式執行(Oracle CLOB採用流機制作爲數據讀寫方式)

這種錯誤一般發生在hibernate.cfg.xml中的hibernate jdbc.batch_size設定大於0的情況,將hibernate.jdbc.batch_size修改爲0即可消除。

 

<hibernate-configuration>

<session-factory>

             ...

             <property name='hibernate. jdbc.batch_size">0</property>

             ...

</session-factory>

</hibernate-configuration>

 

再次嘗試啓動代碼,發現依然無法成功運行,拋出異常如下:

 

Hibernate: select hibernate_sequence.nextval from dual

Hibernate: insert into T--USER  (name,  age,  image,resume,id) values(?,?,?,?,?)

19:02:21,054 ERROR JDBCExceptionReporter:58 IO異常:Connection reset bypeer: socket write error

19:02:21,054 ERROR JDBCExceptionReporter:58 I。異常:Connection reset by peer:socket write error

19:02:21 064 ERROR JDBCExceptionReporter:58Io異常:Connection reset by peer: socket wrto error

19:02:21,064 ERROR Sessionlrnpl:2399 Could not synchronize database state with session

net.sf.hibernate.exception.GenericJDSCException:  could not insert:[com.redsaga.hibernate.db.entity.TUser#27]

...

 

爲什麼會出現這樣的情況?

問題出在OracceBlob/Clob字段獨特的訪問方式,Oracle Blob/Clob字段本身擁有一個遊標(cursor) , JDBC必須通過遊標對Blob/ Clob字段進行操作,在Blob/Clob字段被創建之前,我們無法獲取其遊標句柄,這也就意味着,我們必須首先創建一個空Blob/Clob字段,再從這個空Blob/Clob字段獲取遊標,寫入我們所期望保存的數據。

如果用JDBC代碼來表示這個過程,則得到如下代碼:

 

//... 獲取JDBC連接

dbconn.setAutoCommit(falee);

// =======插入數據,BLOB CLOB字段插入空值

PreparedStatenent preStmt=

dbconn.prepareStatement(

    "insert into T_USER (name, age,  id,image,resume) values

     (?,?,?,?,?)");

preStmt.setString(1,"Shark");

preStmt.setInt(2,20);

preStmt.setInt(3,5); 

// 通過oracle.sgl.BLOB/CLOB.empty_lob()方法構造空Blob/Clob對象

preStmt.setBlob(4 oracle.sgl.BLOB.empty_lob());

preStmt.setClob(5,oracle.sgl.CLOB.empty_lob());

preStmt.executeUpdate();

preStmt.close():

//========== 再次從庫表讀出,獲得Blob/Clob句柄

preStmt=

  dbconn.prepareStatement(

      "select  image,resume from T_USER where id=?for update');

preStmt.setint(l,5);

ResultSet rset=preStmt.executeQuery();

// 注意我們這裏需要引用Oracle原生BLOB定義,如果使用了Weblogic JDBC Vendor

// 則應使用weblogic.jdbc.vendor.oracle. OracleThinBLob/OracleThinCLOb

rset.next();

oracle.sql.BLOB imqBlob = (oracle.sql.BLOB) rset.getBlob(1);

oracle.sql.CLOB resClob = (oracle.sql.CLOB) rset.getClob(2);

//======= 將二進創數據寫入Blob

FileInputStream inStream = new FileinputStream("c//inimage.jpg");

OutputStream outStream = imgBlob.getBinaryOutputStream();

byte[] buf=new byte[10240];//10K 讀取緩存

int len;

while((len=inStream.read(buf))>0){

   outStream.write(buf,0,len);

}

inStream.close();

outStream.close():

//======= 將字符串寫入Clob

resClob.putString(1 "This is my Glob"

//======= Blob/Clob字段更新到數據序

preStmt= dbconn.prepareStatement("update T_USER set  image=?,  resume=? where id=?");

preStmt.setBlob(1,imgBlob);

preStmt.setClob(2,resClob):

preStmt.setlnt(3 5);

preStmt.executeUpdate();

preStmt.close():

dbconn.commit();

dbconn.close():

 

上面的代碼說明了OracleBlob/Clob字段操作的一般機制,那麼,基於Hibernate的持久層實現中,應該如何對Blob/Clob字段進行處理?

我們知道,Hibernate底層數據訪問機制仍然是基於JDBC實現,那麼也就意味着我們必須在Hibernate中模擬JDBC的訪問流程:

TUser user=new TUser();

user.setAge(new Integer(20));

user.setName("Shark');

user.setImage(Hibernate.createSlob(new byte [1])):

user.setResume(Hibernate.createClob(" "));// 注意這裏的參教是一個空格

Transaction tx=session.beginTransaction();

session.save(user):

// 調用flush方法,強制Hibernate立即執行insert sql

session.flush();

// 通過refresh方法,強制Hibernate執行select for update

session.refresh(user, LockMode.UPGRADE);

// Blob寫入實際內容

oracle.sql.BLOB blob=(oracle.sql.BLOB)user.getImage();

OutputStream out=blob. getBinaryOutputStream();

FileInputStream imgis=new FileInputStream("C://inimage.jpg");

byte[] buf=new byte[10240];//10K 緩存

int len;

while((len=imgis.read(buf))>0){

  out.write(buf,0,len);

}

imgis.close();

out.close();

// Clob寫入實際內容

oracle.sql.CLOB clob=(oracle.sgl.CLOB)  user.getResume();

java.io.Writer writer = clob.getCharacterOutputStream();

writer.write("this is my  resume');

writer.close();

session.save(user);

tx.commit();

實際應用中,對於Clob字段,我們可以簡單地將其映射爲String類型,不過在這種情況下需要注意,Oracle Thin DriverClob字段支持尚有欠缺,當Clob內容超出4000字節時將無法讀取,而Oracle OCI Driver(需要在本地安裝Oracle客戶端組件)則可以成功地完成大容量Clob字段的操作。

上面的代碼中,我們通過Session.save/flush/refresh方法的組合使用,實現了上面JDBC代碼中的Blob/Clob訪問邏輯。

Blob/Clob 字段的Hibernate保存實現如上所述,相對來講,讀取則沒有太多的障礙,之前的讀取代碼依然可以正常運行。

對於上面的實現,相信大家都感覺到了一些Bad Smell,如果Blob/Clob字段普遍存在,那麼我們的持久層邏輯中可能遍佈如此複雜的數據存儲邏輯、並與數據庫原生類緊密禍

如何解決這些問題?

回憶之前關於自定義數據類型的討論。通過自定義數據類型我們可以對數據的通用特徵進行抽象,那麼,對於OracleBlob/Clob字段,我們是否可以也對其進行抽象,並以其作爲所有Oracle Blob/Clob字段的映射類型?

下面的StringClobType實現了這一目標:

public class StringClobType implements UserType{

    private static final String ORACLE_DRIVER_NAME="Oracle JDBC driver";

       private static final int ORACLE_DRIVER_MAJOR_VERSION=9;

    private static final int ORACLE_DRIVER_MINOR_VERSION=0;

    public int[] sqlTypes(){

       return new int[] {Types.CLOB};

    }

    public Class returnedClass{

       return String.class;

    }

    public boolean equals(Object x, object y){

       return org.apache.commons.lang.ObjectUtils.equals(x, y);

    }

    public Object nullSafeGet(ResultSet rs, String[] names, Object owner)

    throws HibernateException,SQLException{

       Clob clob=rs.getClob(names(O]);

       return(clob==null ? null:clob.getSubString(l,  (int) clob.length())):

    }

    public void nullSafeSet(PreparedStatement st Object value, int index) 

    throws HibernateException, SQLException{

       DatabaseMetaData dbMetaData=st.getConnection().getMetaData();

       if (value==null)

           st.setNull(index,  sqiTypes()(0));

       else

//         本實現僅僅適用於Oracle數據序9.0以上版本

           if

           (ORACLE_DRIVER_NAME.equals(dbMetaData.getDriverName( ))(

              if((dbMetaData.getDriverMajorVersion() 

                     >=ORACLE-DRIVER-MAJOR-VERSION)

                  &&(dbMetaData.getDriverMinorVersion()  

                     >=ORACLE-DRIVER-MINOR-VERSION))

                         try

//                通過動態加載方式進免編譯期對Oracle JDBC的依賴

                         Class oracleClobClass=Class.forName('oracle.sgl.CLOB");

//                              動態調用createTemporary方法

                                Class partypes[]=new Class[3];

                                partypes[0]=Connection.class;

                                partypes[1]=Boolean.TYPE;

                                partypes(2]=Integer.TYPE;

                                Method createTemporaryMethod=

                                   oracleClobClass.getDeclaredMethod(

                                          "createTemporaxy “,

                                          partypes);

                                Field durationSessionField=

                                   oracleClobClass.getField("DURATION-SESSION");

                                Object arglist[]=new 0bject[3]:

                                   Connection conn=

                                       st.getConnection().getMetaData().getConnection();

//                              數據庫連接類型必須爲OracleConnection

//                              萊些應用服務器會使用自帶Oracle JDBC Wrapper,如Weblogic

//                              這裏需要特別注意

                                Class oracleConnectionClass=

                                   Class.forName("oracle.jdbc.OracleConnection");

                                if(!oracleConnectionClass

                                       .isAssignableFrom(conn.getClass())){

                                   throw new HibernateException(

                                          "Must be a oracle.jdbc.OracleConnection:.

                                          +conn.getClass().getName());

                                }

                                arglist[0] = conn;

                                arglist(1] = Boolean.TRUE;

                                arolist[2] = durationSessionField.get(null);

                                Object tempClob =createTemporaryMethod.invoke(null,arglist);

                                partypes=new Class[l];

                                partypes[0]=Integer.TYPE;

                                Method openMethod =oracleClobClass.getDeclaredMethod("open",partypes);

                                Field modeReadWriteField =oracleClobClass.getField("MODE_READWRITE");

                                arglist = new Object(l];

                                arglis[0] = modeReadWriteField.get(null);

                                openMethod.invoke(tempClob, arglist);

                                Method getCharacterOutputStreamMethod=oracleClobClass.getDeclaredMethod("getCharacterOutputStream',null) ;

//                                     call the getCharacterOutpitStream method

                                       Writer tempClobWriter =(Writer)getCharacterOutputStreamMethod.invoke(tempClob,null);

//                                     將參數寫入Clob

                                       tempClobwriter.write((String) value);

                                       tempClobWriter.flush();

                                       tempClobWriter.Close();

                                      

//                                     close  clob

                                       Method closeMethod=oracleClobClass.getDeclaredMethod("close", null);

                                       closeMethod.invoke(tempClob, null);

                                       st.setClob(index,  (Clob) tempClob);

                                )catch  (ClassNotFoundException e){

                                   throw new HibernateException("Unable to find a required class./n"+e.getMessage()):

                                }catch (NOSuchMethodException e){

                                   throw new HibernateException("Unable to find a required method./n"+e.getMessage()):

                                }catch (NoSuchFieldException e){

                                   throw new HibernateException("Unable to find a required field./n"+e.getMessage());

                                }catch (IllegalAccessException e){

                                   throw new HibernateException("Unable to access a required method or field./n"+e.getMessage());

                                   catch (InvocationTargetException e){

                                       throw new HibernateException(e.getMessage());

                                       {  catch (IOException e){

                                          throw new HibernateException(e.getMessage());

                                       }

                                       else

                                       throw new HibernateException(

                                              "No CLOBS support.Use driver version"

                                              +ORACLE_DRIVER_MAJOR_VERSION

                                              +" minor"

                                              +ORACLE_DRIVER_MINOR_VERSION);

                                       }

                                   }else

                                   String str = (String)  value;

                                    StrinaReader r = new StringReader(str);

                                   St.setCharacterStream(index, r, str.length());

                                }

    }

    public Object deepCopy(Object value){

       if(value==null)

           return null;

       return new String((String)  value);

    }

    public boolean isMutable(){

       return false

    }

}

上面這段代碼,重點在於nullSafeSet方法的實現,nullSafeSet中通過Java Reflection機制,解除了編譯期的Oralce JDBC原生類依賴。同時,藉助Oracle JDBC提供的原生功能完成了Clob字段的寫入,Clob字段的寫入操作由於涉及特定數據庫內部實現細節,這裏就不多費脣舌,大家可參見Oracle JDBC Java Doc.

這段代碼是由筆者根據Ali Ibrahim, Scott Miller的代碼修改而來的(原版請參見httpJ/www.hibemate org /56.html ),支持Oracle 9以上版本,Oracle 8對應的實現請參見上述網址。

同樣的道理,讀者可以根據以上例程,編寫自己的ByteBlobType以實現byte[]Blob的映射。

另外,此代碼必須運行在最新版的Oracle JDBC Driver(筆者所用版本爲Oracle9i9.2.0.5 for JDK1.4,如果使用9.2.0.3或之前版本則在新建l更卻刪除數據時可能會遇到“nomore data read from socket”錯誤)

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