Mybatis Generator + Mysql存储byte数组时与DDB不兼容的问题解决

叁叁肆2018-09-19 14:34

本文来自网易云社区

作者:孙波


最近在使用Mybatis的过程中,无意中踩到了一个坑,导致生成的sql语句不能在DDB上正确的执行并抛出异常:

心下认为mybatis已是比较成熟的框架,不应该出现出现这种SQL拼写的低级错误,那么问题可能就出现在DDB的兼容性方面,毕竟DDB只是实现了MySQL的基本功能,子查询什么的都只能呵呵,于是乎仔细看了下异常,最明显的不同点就是“_binary'**'”,猜测这是Mybatis或者MySQL对于byte[]类型的数据转换,Debug时跟踪堆栈到 PreparedStatementHandler类:
public int update(Statement statement) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
ps.execute();
int rows = ps.getUpdateCount();
Object parameterObject = boundSql.getParameterObject();
KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();
keyGenerator.processAfter(executor, mappedStatement, ps, parameterObject);
return rows;
}
Statement是专门用于执行SQL语句的接口,这里的实例则用的是最基本的 DelegatingPreparedStatement,在ps对象中,基本数据类型存储在
parameterValues中,而byte[]则存储在parameterStreams中,通过:
new String(((ByteArrayInputStream)((java.io.InputStream[])((JDBC4PreparedStatement)((DelegatingPreparedStatement)((DelegatingPreparedStatement)ps)._stmt)._stmt).parameterStreams)[9]).buf)
最终获取到的值为


说明到这里时仍没有被加上“_binary”标签,再往下走便是mysql包中的内容,继续跟进堆栈,在DelegatingPreparedStatement的execute()方法中进入到SQL的执行流程:
public boolean execute() throws SQLException {
checkOpen();
if (getConnectionInternal() != null) {
getConnectionInternal().setLastUsed();
}
try {
return ((PreparedStatement) getDelegate()).execute();
} catch (SQLException e) {
handleException(e);
return false;
}
}
这里getDelegate()的调用者是 JDBC4PreparedStatement,实际上执行的类是其父类 PreparedStatement,该方法开始进行发送package的组装,而后在一个for循环中依次拼接SQL参数:
for (int i = 0; i < batchedParameterStrings.length; i++) {
checkAllParametersSet(batchedParameterStrings[i], batchedParameterStreams[i], i);
sendPacket.writeBytesNoNull(this.staticSqlStrings[i]);
if (batchedIsStream[i]) {
streamToBytes(sendPacket, batchedParameterStreams[i], true, batchedStreamLengths[i], useStreamLengths);
} else {
sendPacket.writeBytesNoNull(batchedParameterStrings[i]);
}
}
可以看到其中有一步是判断是否为byte[],并且对其进行处理:
if (hexEscape) {
packet.writeStringNoNull("x");
} else if (this.connection.getIO().versionMeetsMinimum(4, 1, 0)) {
packet.writeStringNoNull("_binary");
}
方法内的这一步判断完数据库服务器的SQL版本之后,便在byte[]前加上了“_binary”,所以说这块异常SQL是Mysql对byte[]数据进行了处理,并且DDB对此不兼容造成的。问题到此已经查出原因,剩下的就改寻找合适的解决方案了。
由于数据库中字段的类型是MediumBlob,实际上就是Blob的一种,相对应的类型JavaType也是Blob,在Mybatis的处理流程中,需要通过BaseTypeHandler<T>的子类向 PreparedStatement中依次注入数据,不同的数据类型有对应的TypeHandler,映射关系基本上从名称就能看出来。
BlobTypeHandler中,是这样实现数据注入的:
public void setNonNullParameter(PreparedStatement ps, int i, byte[] parameter, JdbcType jdbcType)
throws SQLException {
ByteArrayInputStream bis = new ByteArrayInputStream(parameter);
ps.setBinaryStream(i, bis, parameter.length);
}
于是PreparedStatement中对应的数据类型就是byte[],自然到了Mysql中会被加上“_binary”。
事实上DDB中的Blob是能直接接收String类型的数据的,所以这里的策略是在Mybatis中将byte[]类型的数据直接转为String进行存储,直接在Mybatis的配置中这样修改:
<table tableName="A_Table" domainObjectName="table">
<property name="useActualColumnNames" value="true"/>
<columnOverride column="B_column" javaType="String" jdbcType="BLOB"/>
</table>
再次启动服务,发现生成的SQL语句已经不带“_binary”,并且可以正常存储到DDB中。
问题到这里并没有完全解决,使用时发现虽然存储正常,但是如果数据中包含中文,则取出之后必定乱码,由于 jdbcType= "BLOB",考虑之后觉得问题一定出在byte[]到String的转化编码上。
于是尝试用几种通用的编码来对byte[]进行转换,确定编码为ISO-8859-1,但是MyBatis和Mysql使用的默认编码都是UTF-8,并且DDB中的中文显示正常,好吧,只能继续debug来定位问题。
因为 javaType= "String",所以TypeHandler的实例是StringTypeHandler,该类中用于获取参数内容的方法:
public String getNullableResult(ResultSet rs, String columnName)
throws SQLException {
return rs.getString(columnName);
}
rs的实例是 DelegatingResultSet,并且最终调用ResultSetImpl的getString()方法,继续向里跟踪,进入到 getStringInternal方法,这个方法的返回值就是Mysql从DDB获取并向上返回的参数值,虽然也经过了一些处理(麻烦的就是这些处理)。
Field metadata = this.fields[internalColumnIndex];
String stringVal = null;
if (metadata.getMysqlType() == MysqlDefs.FIELD_TYPE_BIT) {
if (metadata.isSingleBit()) {
byte[] value = this.thisRow.getColumnValue(internalColumnIndex);
if (value.length == 0) {
return String.valueOf(convertToZeroWithEmptyCheck());
}
return String.valueOf(value[0]);
}
return String.valueOf(getNumericRepresentationOfSQLBitType(columnIndex));
}
String encoding = metadata.getEncoding();
stringVal = this.thisRow.getString(internalColumnIndex, encoding, this.connection);
这是该方法中关于获取参数值的关键代码, stringVal就是返回值,encoding是byte[]转换String时使用的编码,而这里的编码则是从Field中获取,Field则又是 MySQLConnection中关于DDB相应数据库表里列的说明,看下参数
果然是ISO-8859-1,但是数据库中
列中没有单独作出编码的声明,所以理论上整张表的编码类型应该都是UTF-8,这个问题咨询了一下DBA,那边也没给出答案,所以只能自己想办法解决了。
StringTypeHandler中获取参数值使用的是getString()方法,所以byte[]到String这一步的转换交给了MySql来做,而这里有用了ISO编码所以才会导致中文乱码。
解决思路就是TypeHandler中直接获取Blob也就是byte[]数据,然后自己用UTF-8编码进行转化,具体方法是:
public String getNullableResult(ResultSet rs, String columnName)
throws SQLException {
Blob blob = rs.getBlob(columnName);
byte[] returnValue = null;
if (null != blob) {
returnValue = blob.getBytes(1, (int) blob.length());
}
return geneValue(returnValue);
}
private String geneValue(byte[] returnValue) {
try {
if (returnValue != null) {
//把byte转化成string
return new String(returnValue, DEFAULT_CHARSET);
} else {
return null;
}
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("Blob Encoding Error!");
}
}
这里的 DEFAULT_CHARSET就是UTF-8,这一步处理类似BlobTypeHandler中对于Blob数据的处理,也就是说存储数据时按照String类型,读取时按照Blob类型,现成的TypeHandler没有满足条件的,那么就自己实现一个,并且在Mybatis相关配置中修改:
<table tableName="A_Table" domainObjectName="table">
<property name="useActualColumnNames" value="true"/>
<columnOverride column="B_column" javaType="String" jdbcType="BLOB"
typeHandler="customTypeHandler"/>
</table>
至此,这个不兼容问题得到解决。
问题的解决方案并不麻烦,只需要自定义一个TypeHandler并实现 BaseTypeHandler<T>的相关接口,但是查找问题原因并寻求解决方案的过程,才是这里面最有价值的地方吧,希望能给遇到类似问题的同事一点点启发和帮助。

网易云免费体验馆0成本体验20+款云产品!

更多网易研发、产品、运营经验分享请访问网易云社区