一、前言
今天调试分页查询代码的时候遇到一个奇葩的问题,该问题后来排查下来跟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}
二、问题排查
首先定位到问题出现在哪里
- 在这里插入图片描述
由于 columnName
是total_increase_percent
,该列不是timestamp
,类型不一致,所以报异常。所以要看卡rs
是如何获取到的以及为何使用了org.apache.ibatis.type.DateTypeHandler
转换该字段。
org.apache.ibatis.type.DateTypeHandler的获取
- 在这里插入图片描述
点击调用栈的红框出,跳转到rs
和org.apache.ibatis.type.DateTypeHandler
的获取处。
- 在这里插入图片描述
在createUsingConstructor
方法这里既取到了typeHandler
,又获取到了rs
。
我们可以推断出,应该是通过columnName
和parameterType
获取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
三个值的类型:
- 在这里插入图片描述
ok,这几个值都吻合了,根据代码1-3行
我们看出parameterType
和columnName
没对上导致的。这里可以推断出constructor
出现了问题,为啥不怀疑是columnName
出现了问题了呢?因为rsw
是获取到sql返回结果构造的,是mybatis
的代码,大概率不会出现问题。
constructor分析
这是constructor
的parameterTypes
:
- 在这里插入图片描述
这里出现了4个字段,但是没有id
, gmtUpdate
,gmtCreate
这几个字段,而看看rsw
的columnNames
的值:
- 在这里插入图片描述
这里却多了id
, gmtUpdate
,gmtCreate
这几列,问题有进一步定位到了,原来是构造器的字段和rsw
的columnNames
不一一对应导致的。可以从上面代码for
循环得知,都是根据索引一一获取,这里个数都对不上,肯定有问题了。
在这里基本上已经定位到问题了,就是构造器只接受了3个参数导致的。
如何获取到的constructor
把方法栈在往上移一个,就能找到constructor
的具体获取处:
- 在这里插入图片描述
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
方法就找到了正主。
- 在这里插入图片描述
由于!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
的代码:真正牛的代码不需要注释。很容易就找到了问题。