今天在批量更新Oracle数据库中一个XMLTYPE类型字段时,部分数据更新时抛出了如下异常:
java.sql.SQLException: ORA-31167: XML nodes over 64K in size cannot be inserted
看异常提示,意思是节点大小超过了64K不能插入。
这里采用的是Oracle提供的oracle.xdb.XMLType类的方式,具体请看:java操作Oracle类型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是继承了Clob的存在,没理由这点数据保存不进去啊,难道是这种保存的方式不对么?(实际大小文本复制出来保存后10K不到,不知道怎么更新时会超过64K,可能oracle的算法不一样,但谁让oracle是规则的制定者,我也只能遵守)。
查询了一下相关资料,Oracle官方貌似是真的有这个限制,其目的大意是XMLTYPE类型提供了很多xml操作的函数,为了这些函数的运行而限制了文档大小。
可是数据库中明明有超过我这个大小的数据啊,那原来是怎么插入进去的?
换成Clob的操作方式:
final InputStream is = new ByteArrayInputStream(xml.getBytes()); final Reader reader = new StringReader(xml); jdbcTemplate.execute(sql, new AbstractLobCreatingPreparedStatementCallback( this.lobHandler) { protected void setValues(PreparedStatement ps, LobCreator lobCreator) throws SQLException { ps.setLong(1, finalId); lobCreator.setClobAsCharacterStream(ps, 2, reader, xml.getBytes().length); } });
还是不行,抛出了以下异常:
nested exception is java.sql.SQLException: ORA-01461: can bind a LONG value only for insert into a LONG column
网上有建议先用Clob保存到临时表,再使用sql将数据更新到XMLTYPE中,看到原来代码中也有这个作法,想着应该可行便试了一下,将数据保存到Clob的临时表后用以下sql更新:
update xml_table t1 set t1.xml_file=sys.xmlType.createXML((select t2.CLOB_TYPE from T_XML_CLOB_EXCHENGE t2 where t2.XML_CLOB_EXCHENGE_ID = ?));
Clob数据倒是保存进去了,可是在更新到XMLTYPE中的时候仍然抛出了java.sql.SQLException: ORA-31167: XML nodes over 64K in size cannot be inserted
异常。
将该sql语句复制到Oracle SQL Developer
工具中直接执行,异常仍在。
这使用了官方提供的类,使用了官方的工具都不行,难道这个无解么?那原来的数据是怎么保存进去的?
没办法,找到原来的jar包反编译源码(外包的,源码已不可找),发现有这么一段:
if (contents.getBytes("utf-8").length > 64 * 1024) { result = contentDao.saveXmlToClob(id,xml); } else { result = contentDao.saveXmlData(clazz,"xmltype", xml,id); }
看来对大于64K的数据确实是做了特殊处理的,继续反编译,终于发现关键的代码:
//select t.xmltype.getClobVal() from xml_table t where t.id = ? for update Object result = query.getSingleResult(); if (result != null) { Clob clob = (Clob) result; java.io.BufferedWriter out = new java.io.BufferedWriter(clob.setCharacterStream(1)); out.write(xml); out.flush(); out.close(); session.flush(); }
使用的是Hibernate,先将XMLTYPE通过Clob方式查出来之后,在内存中保存了一个数据库的映射引用,在改变数据后直接弄回去,利用查询来完成了更新。
试了一下果然好使,这是绕过了Oracle的验证了么?
但是大家知道我并不怎么喜欢用Hibernate,这次也不例外使用了Spring JdbcTemplate封装的Dao,看这里:Jdbc通用Dao
JdbcTemplate没有所谓的flush方法啊,那要怎么来完成这个利用查询来更新的操作?
查看JdbcTemplate提供的方法,发现有这么一个:
<T> T execute(ConnectionCallback<T> action) throws DataAccessException;
这里提供了直接能拿到Connection连接,那应该就能完成我们想要的功能,试用如下代码:
jdbcTemplate.execute(new ConnectionCallback<Boolean>() { public Boolean doInConnection(Connection con) throws SQLException, DataAccessException { try { PreparedStatement preparedStatement = con.prepareStatement(sql); preparedStatement.setLong(1, finalId); ResultSet rs = preparedStatement.executeQuery(); if (rs.next()) { Clob clob = rs.getClob(1); BufferedWriter out = new BufferedWriter(clob.setCharacterStream(1)); out.write(xml); out.flush(); out.close(); con.commit(); } rs.close(); } catch (IOException e) { throw new AssistantException(e); } return true; } });
终于更新成功了,激动啊!
到这里差不多折腾完了,虽然成功更新了数据,但缺点也很明显,就是这条记录的更新不能使用事务,因为它单独的commit了。
如果是批量的操作中有这么一个更新操作,还是有一定的数据一致性风险的。
具体如何取舍还要自己权衡!
纠正
这两天又对这功能进行了测试,发现当上下文中存在事务时,去掉con.commit()
这一行,在事务统一提交时仍旧是可以更新进去的。
但当上下文中没有事务时,去掉这一行将会不能更新。
签于这个原因,我们可以在提交时判断当前上下文中是否存在事务,如果不存在则提交,反之则等待外围事务一起提交:
//当上下文中没有事务时为true if (con.getAutoCommit()) { con.commit(); }