背景
在最近的一个项目中,我遇到了一个由 MyBatis 引发的异常,异常堆栈信息如下:
1 | org.springframework.dao.DataIntegrityViolationException: Error attempting to get column 'remark' from result set. Cause: java.sql.SQLDataException: Cannot convert string '[process:下单成功通知][result:fail][error:syntax error, unexpect token error];' to java.sql.Timestamp value ; Cannot convert string '[process:下单成功通知][result:fail][error:syntax error, unexpect token error];' to java.sql.Timestamp value; nested exception is java.sql.SQLDataException: Cannot convert string '[process:下单成功通知][result:fail][error:syntax error, unexpect token error];' to java.sql.Timestamp value org.springframework.dao.DataIntegrityViolationException: Error attempting to get column 'remark' from result set. Cause: java.sql.SQLDataException: Cannot convert string '[process:下单成功通知][result:fail][error:syntax error, unexpect token error];' to java.sql.Timestamp value ; Cannot convert string '[process:下单成功通知][result:fail][error:syntax error, unexpect token error];' to java.sql.Timestamp value; nested exception is java.sql.SQLDataException: Cannot convert string '[process:下单成功通知][result:fail][error:syntax error, unexpect token error];' to java.sql.Timestamp value at org.springframework.jdbc.support.SQLExceptionSubclassTranslator.doTranslate(SQLExceptionSubclassTranslator.java:84) |
该异常表明,MyBatis 在尝试将数据库中的字符串值转换为 java.sql.Timestamp
时失败,导致 DataIntegrityViolationException
异常。
问题分析
首先,我检查了相关的代码,包括 Mapper 文件、Java Bean 以及 SQL 查询语句。
mapper.xml文件
1 | <select id="selectById" resultType="com.tem.car.domestic.model.didi.DidiMockRecord" parameterType="java.lang.Long"> |
Mapper接口
1 | public interface DidiMockRecordMapper { |
Java Bean
1 | @Data |
从代码中可以看出,remark
字段是一个 String
类型,而异常信息却显示 MyBatis 试图将其转换为 Timestamp
类型。显然,问题并不在于 remark
字段的类型定义,而是 MyBatis 在处理结果集时出现了问题。
源码分析
通过调试 MyBatis 源码,我发现问题的根源在于 DefaultResultSetHandler
类的 createUsingConstructor
方法。该方法通过构造函数来实例化对象,而构造函数的参数顺序与 SQL 查询结果的列顺序不一致,导致了类型转换错误。
1 | private Object createUsingConstructor(ResultSetWrapper rsw, Class<?> resultType, List<Class<?>> constructorArgTypes, List<Object> constructorArgs, Constructor<?> constructor) throws SQLException { |
关键代码在于最后一行:
1 | return foundValues ? objectFactory.create(resultType, constructorArgTypes, constructorArgs) : null; |
该方法通过 resultType
的构造函数来实例化对象。具体来说:
resultType
是DidiMockRecord。
constructorArgTypes
是构造函数的参数类型,依次为Long, String, String, BigDecimal, String, String, Date, Date, String
。constructorArgs
是 SQL 查询结果中的数据,对应数据库中的id, callback_info, supplier_order_id, refund_amount, status, scene_type, remark, create_time, update_time
。- 对应关系如下
Long String String BigDecimal String String Date Date String id callback_info supplier_order_id refund_amount status scene_type remark create_time update_time
通过对比可以发现,构造函数的参数顺序与 SQL 查询结果的列顺序不一致,导致 remark 字段被错误地解释为 Date 类型,从而引发了异常。
问题根源
问题的根源在于 MyBatis 默认使用构造函数来实例化对象,而构造函数的参数顺序与 SQL 查询结果的列顺序不一致。特别是在使用 Lombok 的 @Builder
注解时,生成的构造函数参数顺序可能与数据库列顺序不一致,导致类型转换错误。
解决方案
为了避免这种问题,我认为更好的方式是不使用构造函数来实例化对象,而是通过反射或工厂方法来创建对象。这样可以确保对象的属性与数据库列的顺序无关,从而避免类型转换错误。
结论
MyBatis 默认使用构造函数来实例化对象的设计存在缺陷,特别是在构造函数参数顺序与 SQL 查询结果列顺序不一致时,容易导致 DataIntegrityViolationException
异常。为了避免这种问题,建议使用反射或工厂方法来创建对象,而不是依赖构造函数。