先來了解一下什么是XMLType類型。
XMLType是Oracle從9i開始特有的數據類型,是一個繼承了Blob的強大存在,可以用來存儲xml并提供了相當多的操作函數。理論上可以保存2G大小的數據。
那怎么樣通過java來插入XMLType類型的數據呢?項目當中采用的是Mybatis,總是出現莫名的異常,都搞不清楚到底是Mybatis的問題還是jdbc本身的問題,所以打算一步步來,先搞定jdbc,再解決Mybatis。
JDBC
在折騰了半天之后,發現jdbc操作主要有3種方法:
一、在Java中把XMLType當作字符串String來用,具體創建XMLType的任務完全交給數據庫:
1
2
3
4
|
String sql = "insert into xmltable (XML) values(sys.xmlType.createXML(?))" ; String xmldata = "<label>This is an XML fragment</label>" ; ps.setString( 1 , xmldata); ps.executeUpdate(); |
此方法會使數據庫的壓力偏大,因為此方法簡單又不需要額外的依賴,在一開始采用此方法,但在實際使用過程中發現,在內容的長度超過4000左右的時候,會拋出:ORA-01461: can bind a LONG value only for insert into a LONG column 異常。一開始以為使用mybatis的原因,使用jdbc測試依然如此,使用諸多方法嘗試無解。在項目中使用該大字段不可能只保存長度在4000以內的數據,這樣使用varchar2足矣,所以該方法淘汰。
二、使用CLOB類型來操作。XMLType是繼承了CLOB的存在,所以是可以通過CLOB來操作的。方法是在客戶端創建好CLOB數據后傳入數據庫通過Oracle的XMLTYPE()函數來構造XMLType的值:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
String sql = "insert into xmltable (XML) values(XMLType(?))" ; String xmldata = "<label>This is an XML fragment</label>" ; //通過conn創建CLOB CLOB tempClob = CLOB.createTemporary(connection, false , CLOB.DURATION_SESSION); //打開CLOB tempClob.open(CLOB.MODE_READWRITE); //獲得writer Writer clobWriter = tempClob.setCharacterStream( 100 ); //寫入數據 clobWriter.write(xmldata); //刷新 clobWriter.flush(); //關閉writer clobWriter.close(); //關閉CLOB tempClob.close(); pst.setObject( 1 , tempClob); |
此方法客戶端和數據庫同時承擔了創建XMLType的任務,因此壓力較平均,也沒有超過長度的問題。但是在實際使用過程中又發現,xml的內容頭部不能包含以下信息:
1
|
<? xml version = "1.0" encoding = "UTF-8" ?> |
否則會拋出異常:
1
|
PI names starting with XML are reserved |
先不說少了這個在以后處理xml內容包含中文時會不會遇到蛋疼的亂碼問題,光是看著就讓人感覺不爽,且需求上也要求保存,沒辦法,這個方法又行不通了。
三、使用Oracle提供的oracle.xdb.XMLType類,客戶端創建XMLType后直接把對象傳給數據庫:
1
2
3
4
5
6
7
8
|
Connection conn = ... ; //獲得Connection PreparedStatement ps = ...; //獲得PreparedSatement String sql = "insert into xmltable (XML) values(?)" ; String xmldata = "<label>This is an XML fragment</label>" ; //創建一個XMLType對象 XMLType xmltype = XMLType.createXML(conn, xmldata); ps.setObject( 1 , xmltype); ps.executeUpdate(); |
此方法將創建XMLType的任務完全交給了客戶端,因此客戶端的壓力大,數據庫壓力小。在實測過程中,需要添加兩個jar包,不然會報找不到類的錯誤:
1
2
|
xdb.jar xmlparserv2.jar |
需要注意這jar包又沒版本標注,很容易弄錯,一開始我下載了個xdb.jar,怎么弄都不對提示找不到某個類,查看之后發現是屬于oracle更早期版本,重新下載了一個xdb.jar后正常。
以上三種方法通過插入20萬條數據測試比較發現:
第一種方法:耗時最短,服務器cpu消耗最大;
第二種方法:耗時最長,服務器cpu消耗居中;
第三種方法:耗時居中,服務器cpu消耗最小.
至此,jdbc操作XMLType類型數據終于算是小小的搞定了,不用說采用了第三種方案,但是項目中基本都不會直接用jdbc來操作,像當前項目中就采用了Mybatis,上面也講到了使用Mybatis總是出現異常,查看了下Mybatis也沒有對XMLType的實現,看來還有的折騰,不過jdbc已經搞定,思路已經清晰了不是?
Mybatis
使用Mybatis操作XMLType,我們同樣在Java端映射為String類型,當直接操作不做任何處理時,和jdbc大體一樣,傳輸的內容長度小于4000時一切正常,當傳輸的內容長度超過4000左右時,同樣拋出異常:
1
|
ORA-01461: can bind a LONG value only for insert into a LONG column |
可見,Mybatis的操作其實和jdbc是一樣的,只不過它在jdbc的外面又封裝了一層,使得我們可以采用配置文件等映射的方式來更方便的訪問數據庫,我們要做的,就是在原有Mybatis便捷性的基礎上實現對XMLType類型數據的插入,這種情況下,實現一個XMLType類型的自定義TypeHandler處理器是最好的選擇。
這里,我們仍然采用前面提到的方案三,自然那兩個jar包:xdb.jar,xmlparserv2.jar也是要加入的。
添加一個XmltypeTypeHandler,實現TypeHandler接口,由于插入數據主要用到setParameter方法,所以這里只列出該方法,其它方法代碼略:
1
2
3
4
5
6
7
8
9
|
/** * oracle SYS.XMLTYPE 類型自定義處理器 */ public class XmltypeTypeHandler implements TypeHandler<String> { @Override public void setParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException { } ... } |
這個setParameter方法就是Mybatis在把數據插入到數據庫時用來設置參數的,至于這個方法的參數相信你看代碼也已經明白了,我們按照前面jdbc的實現方式,在這里插入如下代碼:
1
2
3
4
|
public void setParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException { XMLType xmltype = XMLType.createXML(ps.getConnection(), parameter); ps.setObject(i,xmltype); } |
并在mapper-config.xml中注冊轉換器,因為Mybatis定義的枚舉org.apache.ibatis.type.JdbcType中,沒有我們需要的XMLType類型,在這里我們定義為UNDEFINED:
1
2
3
4
5
|
< configuration > < typeHandlers > < typeHandler javaType = "string" jdbcType = "UNDEFINED" handler = "com.tyyd.dw.context.XmltypeTypeHandler" /> </ typeHandlers > </ configuration > |
在配置文件參數中,使用我們的定義的轉換器,這樣Mybatis就能找到了:
1
|
#{xmlFile,jdbcType=UNDEFINED}, |
當然你也可以更規范一點,完整的寫出它的類型和使用的轉換器:
#{xmlFile,javaType=string,jdbcType=UNDEFINED,typeHandler=com.tyyd.dw.context.XmltypeTypeHandler},
完成上面的步驟,照理說一切都大功告成了,我們來運行一下。
結果拋出了異常:java.lang.ClassCastException: org.apache.commons.dbcp.PoolingDataSource$PoolGuardConnectionWrapper cannot be cast to oracle.jdbc.OracleConnection
不能轉換為Oracle的連接對象OracleConnection,查看一下,發現我們數據源使用的是apache的dbcp,應該是兩者不兼容吧。網上查了一下,有位仁兄說是給了個完美解決文案,就是在setParameter方法內再獨自加載一個Oracle的驅動類來創建一個connection,如下:
1
2
|
Class.forName( "oracle.jdbc.OracleDriver" ); Connection connection = DriverManager.getConnection(url, username, password); |
這個確實能100%解決連接對象不能轉換的問題,但是實現方式上,呵呵,還是不做評論了。還有網上在傳來傳去的,說是可以轉換成PoolableConnection 對象,再使用getDelegate方法可以獲得原始代理鏈接,這個貌似可行,我們來試試:
1
2
3
|
PoolableConnection connection = (PoolableConnection )ps.getConnection(); XMLType xmltype = XMLType.createXML(connection.getDelegate(), parameter); ps.setObject(i,xmltype); |
結果又拋出了異常:
org.apache.commons.dbcp.PoolingDataSource$PoolGuardConnectionWrapper cannot be cast to org.apache.commons.dbcp.PoolableConnection
,不能轉換。
沒辦法,看來網上傳來傳去的文章不怎么可靠,沒捷徑了還是自己看看源代碼吧。
通過查看源代碼,我們發現PoolableConnection繼承了DelegatingConnection類,而DelegatingConnection類實現了Connection接口,我們把它轉換成DelegatingConnection試試:
1
2
3
|
DelegatingConnection connection = (DelegatingConnection )ps.getConnection(); XMLType xmltype = XMLType.createXML(connection.getDelegate(), parameter); ps.setObject(i,xmltype); |
結果又拋出異常:無法構造描述符: Invalid arguments; nested exception is java.sql.SQLException: 無法構造描述符: Invalid arguments,通過斷點調試,發現connection對象居然是null,怎么會是null呢,網上人家都用的好好的,到我這里就都不行了,真是蛋疼,這不會無解吧,難道真要像上面那位仁兄說的獨自加載一個驅動類?沒辦法,再研究研究吧。
最后發現,通過getMetaData方法可以獲取它的原始代理連接,柳暗花明啊,趕緊寫上測試,終于正常了,不容易啊,最終代碼如下:
1
2
3
4
5
6
7
8
|
@Override public void setParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException { DelegatingConnection connection = (DelegatingConnection) ps.getConnection().getMetaData() .getConnection(); XMLType xmltype = XMLType.createXML(connection.getDelegate(), parameter); ps.setObject(i, xmltype); } |
至此,使用Mybatis操作XMLType類型終于是搞定了,過程是一波三折啊。數據有插入當然要有查詢,接下來就要實現XMLType類型的查詢操作了。