关于MyBatis中Mapper的XML中何时需要显式定义TypeHandler

题目中的显式定义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部分:
关于MyBatis中Mapper的XML中何时需要显式定义TypeHandler_第1张图片
可以看到它先去找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
关于MyBatis中Mapper的XML中何时需要显式定义TypeHandler_第2张图片

继续往下跟,就会遇到另一个关键代码:

关于MyBatis中Mapper的XML中何时需要显式定义TypeHandler_第3张图片

从这个代码我们就可以看出端倪了:

debugger显示出我们的times的type为ArrayList。
MyBatis先从TYPE_HANDLER_MAP中找有没有对应的TypeHandler。
上面说到,我们实现了一个List类型的TypeHandler,从TYPE_HANDLER_MAP中确实也找到了:
关于MyBatis中Mapper的XML中何时需要显式定义TypeHandler_第4张图片

但我们的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。

本人知识有限,若有错误,望指出,谢谢啦

你可能感兴趣的:(mybatis)