Mybatis为什么会将DATE映射为java.sql.Date,而不是LocalDate?

1,388 阅读4分钟

起因

我在通过Gson库将Mybatis返回的List<Map<String,Object>>序列化时,由于Gson被setDateFormat("yyyy-MM-dd HH:mm:ss")设置过了格式化的时间,但这个不包含LocalDate,只针对java.util.Date和java.sql.date,而Gson默认对LocalDate会序列化成三个字段,所以想要yyyy-mm-dd这种格式需要注册单独的TypeAdapter,并且也注册了。

而最终json中,date型的列由于通过setDateFormat设置了包含时间的格式,所以会出现00:00:00,但如果去掉,又会影响到java.util.date的数据,二是,既然mybatis对datetime会格式化为LocalDateTime,但为什么不把date格式化为LocalDate,这显然是说不通。

结果

如果Mybatis在被用户指明使用LocalDate去接收DATE类型时,Mybatis是支持自动转换的。

但如果使用List<Map<String,Object>>或者Object类型的字段接收DATE类型数据时,Mybatis会转换成java.sql.Date。

这其实是正确的,因为java.sql.Datejava.util.Date之间的区别就是前者不包括时间,只包括日期,专门用来和JDBC对应。

但历史的车轮滚滚向前,出现了LocalDate,我们更想把Date转换为LocalDate,但在Mybatis自动化的作用下,会把Date推测为java.sql.Date

其实我们可以更改,首先看下Mybatis源码对这部分的实现。


1. JdbcType jdbcType = getJdbcType(columnName);

2. handler = typeHandlerRegistry.getTypeHandler(propertyType, jdbcType);

if (handler == null || handler instanceof UnknownTypeHandler) {
3. final int index = columnNames.indexOf(columnName);
   final Class<?> javaType = resolveClass(classNames.get(index));
  if (javaType != null && jdbcType != null) {
    handler = typeHandlerRegistry.getTypeHandler(javaType, jdbcType);
  } else if (javaType != null) {
    handler = typeHandlerRegistry.getTypeHandler(javaType);
  } else if (jdbcType != null) {
    handler = typeHandlerRegistry.getTypeHandler(jdbcType);
  }
}

先看标记为1的代码,用来获取这个列对应的JDBC下的类型,如VARCHARDATE等.

标记为2的代码,参数propertyType意为存放这个字段的实际java数据类型,是由用户所指定的,如java.lang.String,或者是java.lang.Object。

而整个getTypeHandler表示获取一个能将JDBC下的数据转换为java中的数据类型的转换器,如一个能将VARCHAR转换为String的转换器。

但是,当用户使用Object接收Date类型数据时候,不存在这个转换器,所以会返回一个UnknownTypeHandler,表示未知。

而第三部分,是在没有转换器时,通过ResultSet.ResultSetMetaData.getColumnClassName()这个方法获取到jdbc规范所指定的所要实例化成的类,比如规范的实现mysql驱动中说,Date类型要被实例化成为java.sql.Date,所以现在有了实际的java类,下一步还是通过getTypeHandler获取能将Date类型转换为java.sql.Date的转换器,mybatis所实现的是SqlDateTypeHandler

(但是这里有个疑惑,ResultSet.ResultSetMetaData.getColumnClassName()会对datetime型的列返回LocalDateTime,但是会对date型的列返回java.sql.Date)。

但在SqlDateTypeHandler,也包括所有的TypeHandler实现,大部分都是通过jdbc提供的getObject指明一个具体类型来获取,只有少数是需要二次转换的,这也是mysql实现的驱动库中包含30多种不同类型所提供的便利。

@Override
public LocalDate getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
  return rs.getObject(columnIndex, LocalDate.class);
}

而保存这些转换器的类型名为typeHandlerMap,在TypeHandlerRegistry下。

其key是能处理的java类型

value是jdbc类型,和具体的处理器

而我们只需要使用以下代码,就可以完成让Mybatis使用LocalDate。

configuration.typeHandlerRegistry.register(Any::class.java, JdbcType.DATE,LocalDateTypeHandler())

如果在回到开头,那么流程就变成

  1. 获取能处理Object的类型转换器。
  2. 因为我们添加了一个,然后mybatis会继续获取jdbc为Date的的转换器。
  3. 返回LocalDateTypeHandler。

这里可能有点不好理解,是因为typeHandlerMap是一个嵌套Map,Map<Type, Map<JdbcType, TypeHandler<?>>>

外边的key是java中的类型,他的value表示能把jdbc类型转换为key对象的集合。

比如,用户使用Object接收,而数据库中的数据类型是Date,但是除了Date,还有其他的比如Year,他也可以会转换为一个Object的子类Ingeger,这时候需要的就是IntegerTypeHandler,所以这里设计成一个嵌套Map。

比如mybatis会将下面7种jdbc类型转换为String类型,但使用不同的TypeHandler。

register(String.class, JdbcType.CHAR, new StringTypeHandler());
register(String.class, JdbcType.CLOB, new ClobTypeHandler());
register(String.class, JdbcType.VARCHAR, new StringTypeHandler());
register(String.class, JdbcType.LONGVARCHAR, new StringTypeHandler());
register(String.class, JdbcType.NVARCHAR, new NStringTypeHandler());
register(String.class, JdbcType.NCHAR, new NStringTypeHandler());
register(String.class, JdbcType.NCLOB, new NClobTypeHandler());

再回到标题,这也是因为jdbc的规范,但规范就规范,搞特殊就有点不对劲了。

所以这不是Mybatis的问题,而是Mysql驱动的问题,但现在的功力想不出为什么会这样,如果驱动中返回LocalDate会影响什么?