作爲測試,我們新建一個TUser對象,其image屬性中,保存了一個圖片文件的二進制內容。而其resume屬性,我們以一個簡單的字符串作爲填充。
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.createBlob和Hibemate.createClob創建了對應的Blob和Clob對象。其中Blob對象基於一個FileInputStream構建,而Clob對象基於一個字符串構建。
完成了寫入操作,對應的讀取操作代碼如下:
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
通過上面的代碼,我們完成了針對SQLServer的Blob/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:58一Io異常:Connection reset by peer: socket wr主to 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]
...
爲什麼會出現這樣的情況?
問題出在Oracce中Blob/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():
上面的代碼說明了Oracle中Blob/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 Driver對Clob字段支持尚有欠缺,當Clob內容超出4000字節時將無法讀取,而Oracle OCI Driver(需要在本地安裝Oracle客戶端組件)則可以成功地完成大容量Clob字段的操作。
上面的代碼中,我們通過Session.save/flush/refresh方法的組合使用,實現了上面JDBC代碼中的Blob/Clob訪問邏輯。
Blob/Clob 字段的Hibernate保存實現如上所述,相對來講,讀取則沒有太多的障礙,之前的讀取代碼依然可以正常運行。
對於上面的實現,相信大家都感覺到了一些Bad Smell,如果Blob/Clob字段普遍存在,那麼我們的持久層邏輯中可能遍佈如此複雜的數據存儲邏輯、並與數據庫原生類緊密禍
如何解決這些問題?
回憶之前關於自定義數據類型的討論。通過自定義數據類型我們可以對數據的通用特徵進行抽象,那麼,對於Oracle的Blob/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”錯誤)。