题目中的显式定义TypeHandler,也包括定义parameterType、parameterMap等,这些都是MyBatis用来找TypeHandler的依据。
我们知道,MyBatis可以一定程度地简化Mapper的xml配置文件的编写,但到底简化到什么程度呢?
最近在写项目时,需要将List序列化后写入数据库中,也即实现一个TypeHandler,用来解析List。
我实现的代码如下:
@MappedTypes(List.class)
@MappedJdbcTypes({JdbcType.VARCHAR})
public class IntegerListTypeHandler extends BaseTypeHandler<List<Integer>>{
@Override
public void setNonNullParameter(PreparedStatement preparedStatement, int i, List<Integer> integers, JdbcType jdbcType) throws SQLException {
// 1. List集合转成字符串
StringBuilder sb = new StringBuilder();
for (int index = 0; index < integers.size(); index++) {
sb.append(integers.get(index));
if (index != integers.size()-1) {
sb.append(",");
}
}
// 2. 设置给ps
preparedStatement.setString(i, sb.toString());
}
@Override
public List<Integer> getNullableResult(ResultSet resultSet, String s) throws SQLException {
String[] split = resultSet.getString(s).split(",");
return Arrays.stream(split)
.map(Integer::parseInt)
.collect(Collectors.toList());
}
@Override
public List<Integer> getNullableResult(ResultSet resultSet, int i) throws SQLException {
String[] split = resultSet.getString(i).split(",");
return Arrays.stream(split)
.map(Integer::parseInt)
.collect(Collectors.toList());
}
@Override
public List<Integer> getNullableResult(CallableStatement callableStatement, int i) throws SQLException {
String[] split = callableStatement.getString(i).split(",");
return Arrays.stream(split)
.map(Integer::parseInt)
.collect(Collectors.toList());
}
}
寄希望于MyBatis的智能程度,对应的xml文件的insert部分我这么写(times是List类型):
<insert id="insert">
insert into gram_model (id, model_name, model_dir, model_file_name, label_file_name, input_tensor_name, output_tensor_name, number_label, times)
values (#{id}, #{modelName}, #{modelDir}, #{modelFileName}, #{labelFileName}, #{inputTensorName}, #{outputTensorName}, #{numberLabel}, #{times})
insert>
可以看到我没有加parameterType或者parameterMap,当然大部分情况这没有什么问题。但当我调用了insert后,遇到了一个异常:
java.sql.SQLException: Incorrect string value
… for column ‘times’ at row 1
这个异常可能是times序列化异常导致的,也可能是mysql编码问题导致的。
我首先查看mysql的编码,确认为utf8mb4后,基本排除了后者的问题,那就来看看前者的问题。
跟踪源码调试下看看typeHandler相关的部分:
跟踪源码是费时的,这里我直接给出关键的部分:
即DefaultParameterHandler的setParameters部分:
可以看到它先去找parameterMappings有没有对应映射,但从Debugger中看到javaType都是Object,jdbcType都为null,这是自然的,因为我的xml文件中没有加parameterMap等映射标识。
继续看下去:
@Override
public void setParameters(PreparedStatement ps) {
ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings != null) {
for (int i = 0; i < parameterMappings.size(); i++) {
ParameterMapping parameterMapping = parameterMappings.get(i);
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
TypeHandler typeHandler = parameterMapping.getTypeHandler();
JdbcType jdbcType = parameterMapping.getJdbcType();
if (value == null && jdbcType == null) {
jdbcType = configuration.getJdbcTypeForNull();
}
try {
typeHandler.setParameter(ps, i + 1, value, jdbcType);
} catch (TypeException e) {
throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
} catch (SQLException e) {
throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
}
}
}
}
}
接下来得到value具体的值,
然后通过得到parameterMapping.getTypeHandler()得到TypeHandler,在我的情况,得到的是UnkownTypeHandler,也就是暂时不知道使用什么TypeHandler。
然后调用了typeHandler.setParameter,
代码如下:
@Override
public void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
if (parameter == null) {
if (jdbcType == null) {
throw new TypeException("JDBC requires that the JdbcType must be specified for all nullable parameters.");
}
try {
ps.setNull(i, jdbcType.TYPE_CODE);
} catch (SQLException e) {
throw new TypeException("Error setting null for parameter #" + i + " with JdbcType " + jdbcType + " . " +
"Try setting a different JdbcType for this parameter or a different jdbcTypeForNull configuration property. " +
"Cause: " + e, e);
}
} else {
try {
setNonNullParameter(ps, i, parameter, jdbcType);
} catch (Exception e) {
throw new TypeException("Error setting non null for parameter #" + i + " with JdbcType " + jdbcType + " . " +
"Try setting a different JdbcType for this parameter or a different configuration property. " +
"Cause: " + e, e);
}
}
}
由于我们的parameter不为null,走UnkownTypeHandler的setNonNullParameter
继续往下跟,就会遇到另一个关键代码:
从这个代码我们就可以看出端倪了:
debugger显示出我们的times的type为ArrayList。
MyBatis先从TYPE_HANDLER_MAP中找有没有对应的TypeHandler。
上面说到,我们实现了一个List类型的TypeHandler,从TYPE_HANDLER_MAP中确实也找到了:
但我们的times的type在这里显示为ArrayList,因为这是MyBatis经由反射得到的对象类型,而在TYPE_HANDLER_MAP是没有ArrayList的TypeHandler的。
查找失败后,会走 getJdbcHandlerMapForSuperclass,具体代码如下:
private Map<JdbcType, TypeHandler<?>> getJdbcHandlerMapForSuperclass(Class<?> clazz) {
Class<?> superclass = clazz.getSuperclass();
if (superclass == null || Object.class.equals(superclass)) {
return null;
}
Map<JdbcType, TypeHandler<?>> jdbcHandlerMap = TYPE_HANDLER_MAP.get(superclass);
if (jdbcHandlerMap != null) {
return jdbcHandlerMap;
} else {
return getJdbcHandlerMapForSuperclass(superclass);
}
}
可以看到这块代码就是不断找type的父类,看看TYPE_HANDLER_MAP是否存在TypeHandler。
但我们的times是List类型,实际上是一个接口,所以这个方法是找不到List的TypeHandler的。
于是最后MyBatic没有找到List的TypeHandler,自然就序列化错误了。
总结一下:(这里忽略了枚举类型的情况)
1. MyBatis先将xml中指定的parameterType,parameterMap等转化为parameterMappings列表
2. 从parameterMappings得到TypeHandler,如果TypeHandler不为UnkownTypeHandler,也就是找到了对应的TypeHandler时,就使用这个TypeHandler。
3. 如果TypeHandler为UnkownTypeHandler,则从TYPE_HANDLER_MAP中找寻要解析的对象类型对应的TypeHandler
4. 若得不到TypeHandler,则不断找父类的TypeHandler,直到找到为止,注意,它不会去找实现接口的TypeHandler。
为什么MyBatis不去找实现接口的TypeHandler,大概是因为一个类可以实现多个接口,无法确定要找哪个接口的TypeHandler,故希望让用户在xml文件中自己指定。
最后的解决方案自然是xml里加上parameterType。
当然也可以加一个ArrayList的TypeHandler,不过这个比起基于接口的TypeHandler,显得比较不优雅了。
由此,回到题目,何时需要显式定义TypeHandler?
我的想法是 当使用了基于接口的TypeHandler,需要显式定义TypeHandler。
本人知识有限,若有错误,望指出,谢谢啦