作為測試,我們新建一個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.createBlob和Hibemate.createClob創建了對應的Blob和Clob對象。其中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
?
通過上面的代碼,我們完成了針對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”錯誤)。
?