欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Java使用JDBC或MyBatis框架向Oracle中插入XMLType数据

程序员文章站 2024-03-13 11:05:21
先来了解一下什么是xmltype类型。 xmltype是oracle从9i开始特有的数据类型,是一个继承了blob的强大存在,可以用来存储xml并提供了相当多的操作函数。...

先来了解一下什么是xmltype类型。
xmltype是oracle从9i开始特有的数据类型,是一个继承了blob的强大存在,可以用来存储xml并提供了相当多的操作函数。理论上可以保存2g大小的数据。
那怎么样通过java来插入xmltype类型的数据呢?项目当中采用的是mybatis,总是出现莫名的异常,都搞不清楚到底是mybatis的问题还是jdbc本身的问题,所以打算一步步来,先搞定jdbc,再解决mybatis。

jdbc
在折腾了半天之后,发现jdbc操作主要有3种方法:
一、在java中把xmltype当作字符串string来用,具体创建xmltype的任务完全交给数据库:

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的值:

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的内容头部不能包含以下信息:

<?xml version="1.0" encoding="utf-8"?>

否则会抛出异常:

pi names starting with xml are reserved

先不说少了这个在以后处理xml内容包含中文时会不会遇到蛋疼的乱码问题,光是看着就让人感觉不爽,且需求上也要求保存,没办法,这个方法又行不通了。

三、使用oracle提供的oracle.xdb.xmltype类,客户端创建xmltype后直接把对象传给数据库:

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包,不然会报找不到类的错误:

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左右时,同样抛出异常:

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方法,所以这里只列出该方法,其它方法代码略:

/**
 * 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的实现方式,在这里插入如下代码:

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:

<configuration>
 <typehandlers>
  <typehandler javatype="string" jdbctype="undefined" handler="com.tyyd.dw.context.xmltypetypehandler"/>
 </typehandlers>
</configuration>

在配置文件参数中,使用我们的定义的转换器,这样mybatis就能找到了:

#{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,如下:

class.forname("oracle.jdbc.oracledriver");
connection connection = drivermanager.getconnection(url, username, password);

这个确实能100%解决连接对象不能转换的问题,但是实现方式上,呵呵,还是不做评论了。还有网上在传来传去的,说是可以转换成poolableconnection 对象,再使用getdelegate方法可以获得原始代理链接,这个貌似可行,我们来试试:

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试试:

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方法可以获取它的原始代理连接,柳暗花明啊,赶紧写上测试,终于正常了,不容易啊,最终代码如下:

@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类型的查询操作了。