起因
我在通过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.Date
和java.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下的类型,如VARCHAR
、DATE
等.
标记为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())
如果在回到开头,那么流程就变成
- 获取能处理Object的类型转换器。
- 因为我们添加了一个,然后mybatis会继续获取jdbc为Date的的转换器。
- 返回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会影响什么?