access sql 序列 sql序列查询_构造器

一、前言

今天调试分页查询代码的时候遇到一个奇葩的问题,该问题后来排查下来跟lombok的使用有关。我们在使用mybatis或者mybatis-plus的时候一般会定义一个类对应表的每个字段,一个成熟的java程序员喜欢使用lombok把代码简洁点。这是大前提,我直接说结论吧:

实体类最好都加上@Data,@AllArgsConstructor,@NoArgsConstructor才能避免我现在遇到的问题

二、我的问题

我的表:

1create table unimall.industry_output_value
 2(
 3    id bigint auto_increment
 4        primary key,
 5    month_output_value decimal(20,10) null,
 6    total_output_value decimal(20,10) null,
 7    total_increase_percent decimal(20,10) null,
 8    gmt_create datetime null,
 9    gmt_update datetime null,
10    month datetime null
11);

我的实体类:

1@Data
 2@TableName("industry_output_value")
 3@Builder
 4public class OutputValueDO extends SuperDO {
 5    private BigDecimal monthOutputValue;
 6    private BigDecimal totalOutputValue;
 7    private BigDecimal totalIncreasePercent;
 8    @TableField("`month`")
 9    private Date month;
10}
11@Data
12public class SuperDO {
13
14    private Long id;
15
16    @TableField("gmt_update")
17    private Date gmtUpdate;
18
19    @TableField("gmt_create")
20    private Date gmtCreate;
21
22}

然后一个普通的select查询的时候报了下面这个错误:

1Caused by: org.springframework.dao.DataIntegrityViolationException: Error attempting to get column 'total_increase_percent' from result set.  Cause: java.sql.SQLDataException: Unsupported conversion from DECIMAL to java.sql.Timestamp
 2; Unsupported conversion from DECIMAL to java.sql.Timestamp; nested exception is java.sql.SQLDataException: Unsupported conversion from DECIMAL to java.sql.Timestamp
 3    at org.springframework.jdbc.support.SQLExceptionSubclassTranslator.doTranslate(SQLExceptionSubclassTranslator.java:84) ~[spring-jdbc-5.2.4.RELEASE.jar:5.2.4.RELEASE]
 4    at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:72) ~[spring-jdbc-5.2.4.RELEASE.jar:5.2.4.RELEASE]
 5    at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:81) ~[spring-jdbc-5.2.4.RELEASE.jar:5.2.4.RELEASE]
 6    at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:88) ~[mybatis-spring-2.0.3.jar:2.0.3]
 7    at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:440) ~[mybatis-spring-2.0.3.jar:2.0.3]
 8    at com.sun.proxy.$Proxy84.selectList(Unknown Source) ~[na:na]
 9  ...
10Caused by: java.sql.SQLDataException: Unsupported conversion from DECIMAL to java.sql.Timestamp
11    ...
12    at org.apache.ibatis.type.DateTypeHandler.getNullableResult(DateTypeHandler.java:39) ~[mybatis-3.5.3.jar:3.5.3]
13    at org.apache.ibatis.type.DateTypeHandler.getNullableResult(DateTypeHandler.java:28) ~[mybatis-3.5.3.jar:3.5.3]
14    at org.apache.ibatis.type.BaseTypeHandler.getResult(BaseTypeHandler.java:81) ~[mybatis-3.5.3.jar:3.5.3]
15  ...
16Caused by: com.mysql.cj.exceptions.DataConversionException: Unsupported conversion from DECIMAL to java.sql.Timestamp
17    at com.mysql.cj.result.DefaultValueFactory.unsupported(DefaultValueFactory.java:47) ~[mysql-connector-java-8.0.15.jar:8.0.15]
18    ...

可以看到异常很奇怪,我的total_increase_percent明明是BigDecimal,为何要被反序列化为java.util.Date呢?\
后面修改为如下代码就正常了:

1@Data
 2@TableName("industry_output_value")
 3@Builder
 4@AllArgsConstructor
 5@NoArgsConstructor
 6public class OutputValueDO extends SuperDO {
 7    private BigDecimal monthOutputValue;
 8    private BigDecimal totalOutputValue;
 9    private BigDecimal totalIncreasePercent;
10    @TableField("`month`")
11    private Date month;
12}

二、问题排查

首先定位到问题出现在哪里

  1. 在这里插入图片描述

由于 columnNametotal_increase_percent,该列不是timestamp,类型不一致,所以报异常。所以要看卡rs是如何获取到的以及为何使用了org.apache.ibatis.type.DateTypeHandler转换该字段。

org.apache.ibatis.type.DateTypeHandler的获取

  1. 在这里插入图片描述

点击调用栈的红框出,跳转到rsorg.apache.ibatis.type.DateTypeHandler的获取处。

  1. 在这里插入图片描述

createUsingConstructor方法这里既取到了typeHandler,又获取到了rs
我们可以推断出,应该是通过columnNameparameterType获取typeHandler出错了。
我们看看这里的代码:

1  for(int i = 0; i 2    Class> parameterType = constructor.getParameterTypes()[i];
3    String columnName = (String)rsw.getColumnNames().get(i);
4    TypeHandler> typeHandler = rsw.getTypeHandler(parameterType, columnName);
5    Object value = typeHandler.getResult(rsw.getResultSet(), columnName);
6    ...
7  }

看看parameterType,columnName,typeHandler三个值的类型:

  1. 在这里插入图片描述

ok,这几个值都吻合了,根据代码1-3行我们看出parameterTypecolumnName没对上导致的。这里可以推断出constructor出现了问题,为啥不怀疑是columnName出现了问题了呢?因为rsw是获取到sql返回结果构造的,是mybatis的代码,大概率不会出现问题。

constructor分析

这是constructorparameterTypes:

  1. 在这里插入图片描述

这里出现了4个字段,但是没有id, gmtUpdate,gmtCreate这几个字段,而看看rswcolumnNames的值:

  1. 在这里插入图片描述

这里却多了id, gmtUpdate,gmtCreate这几列,问题有进一步定位到了,原来是构造器的字段和rswcolumnNames不一一对应导致的。可以从上面代码for循环得知,都是根据索引一一获取,这里个数都对不上,肯定有问题了。
 在这里基本上已经定位到问题了,就是构造器只接受了3个参数导致的。

如何获取到的constructor

把方法栈在往上移一个,就能找到constructor的具体获取处:

  1. 在这里插入图片描述

resultType就是我的实体类,defaultConstructor通过findDefaultConstructor获取到,这个方法就不细看了,里面逻辑就是:如果只有一个构造器,那就使用该构造器,获取寻找被标记了AutomapConstructor注解的构造器。由于@Data注解只能生成一个构造器OutputValueDO(java.math.BigDecimal,java.math.BigDecimal,java.math.BigDecimal,java.util.Date),没有id, gmtUpdate,gmtCreate
此时就有一个解决方案了,去掉@Data,自己写一个完整的构造器,包括继承的所有字段,但是这样是不太好的,从上面for循环代码可知,要数据库的表的列的顺序要和实体类的构造器的参数的顺序一致,不然还是出现问题。那就在往上个方法栈看看为什么使用了该处理逻辑,印象中的mybatis没这么坑。

更优雅的解决方案

再往上到上一个方法栈,   createResultObject方法就找到了正主。

  1. 在这里插入图片描述

由于!resultType.isInterface() && !metaType.hasDefaultConstructor()判断为真就走到了通过构造器反射得到结果的逻辑。接下来分别分析这个 if...else...

  • hasTypeHandlerForResultObject明显我们没有定义自己的typeHandler,故忽略
  • constructorMappings表示在mapper.xml中定义了实体类的字段和表字段的映射关系,但是我们没有定义,忽略
  • 我的实体类OutputValueDO不是接口,并且没有无参构造器,我的代码正好适合这个判断,所以我要想办法是这个判断为假。
  • 最后一个就是默认的,我要改成走到这个方法来。
    解决方案方向:加个默认构造器。加@NoArgsConstructor即可解决,该注解就能生成无参构造器,由于我加了@Builder,所以我还必须加@AllArgsConstructor,稳了,解决了。

三、总结

我一般使用lombok最好加上这几个注解:

1@Data
2@Builder
3@AllArgsConstructor
4@NoArgsConstructor

但是这个代码一开始不是我写的,而且该实体类没有继承的话也不会出问题,巧了。以前也看过mybatis的代码,但是没有翻过映射这块。今天再一次体味到了mybatis的代码:真正牛的代码不需要注释。很容易就找到了问题。