最近在项目中使用 Kotlin 的 data class
与 MyBatis 进行数据库映射时,遇到了一个令人费解的异常。经过一番折腾,终于找到了问题的根源,并解决了这个问题。在这里记录一下整个排查和解决的过程。
问题描述 在运行项目时,出现了以下异常:
1 2 org.springframework.dao.DataIntegrityViolationException: Error attempting to get column 'name' from result set. Cause: org.postgresql.util.PSQLException: 不良的类型值 int : 测试工程1 ; 不良的类型值 int : 测试工程1; nested exception is org.postgresql.util.PSQLException: 不良的类型值 int : 测试工程1
从错误信息来看,在尝试从结果集中获取 name
列时出现了问题。数据库返回的 name
值是字符串 “测试工程1”,但程序却提示存在类型 int
的错误。这明显不合理,name
字段在数据库中明明是 varchar
类型,为什么会出现这种错误呢?
问题分析 首先,我检查了数据库中相关表的结构,确认 name
字段的类型确实是 varchar
:
pro_project
表结构1 2 3 4 5 6 7 8 9 create table pro_project( id serial primary key, name varchar not null , voltage_level varchar not null , create_time timestamp default now() not null , manager integer default 1 not null );
接着,查看了 pro_attendance
表的结构:
pro_attendance
表结构1 2 3 4 5 6 7 8 9 10 11 create table pro_attendance( id integer project_id integer not null user_id integer not null , sign_in_time timestamp not null , sign_out_time timestamp , sign_in_coordinate geometry(PointZ, 4326 ) not null , sign_out_coordinate geometry(PointZ, 4326 ), working_hours integer );
然后,查看了 MyBatis 的 ResultMap
和 SQL 查询语句,确认字段的映射关系是正确的。
ResultMap 配置 1 2 3 4 5 6 7 8 9 10 11 12 13 <resultMap id ="AttendanceVOMap" type ="com.heny.server.controller.project.response.AttendanceVO" > <id column ="id" property ="id" /> <result column ="user_id" property ="userId" /> <result column ="project_id" property ="projectId" /> <result column ="name" property ="projectName" /> <result column ="sign_in_time" property ="signInTime" /> <result column ="sign_in_coordinate" property ="signInCoordinate" typeHandler ="com.heny.web.handler.mybatis.PointTypeHandler" /> <result column ="sign_out_time" property ="signOutTime" /> <result column ="sign_out_coordinate" property ="signOutCoordinate" typeHandler ="com.heny.web.handler.mybatis.PointTypeHandler" /> <result column ="working_hours" property ="workingHours" /> </resultMap >
SQL 查询语句 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <select id ="selectCurrentVOByUser" resultMap ="AttendanceVOMap" > SELECT a.id, a.user_id, a.project_id, p.name, a.sign_in_time, a.sign_in_coordinate, a.sign_out_time, a.sign_out_coordinate, a.working_hours FROM pro_attendance a LEFT JOIN pro_project p ON a.project_id = p.id WHERE a.user_id = #{userId} AND sign_out_time IS NULL LIMIT 1 </select >
VO 类定义 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 data class AttendanceVO ( @field:Schema (description = "考勤记录ID" ) val id: Int , @field:Schema (description = "项目ID" ) val projectId: Int , @field:Schema (description = "项目名称" ) val projectName: String, @field:Schema (description = "用户ID" ) val userId: Int , @field:Schema (description = "签到时间" ) val signInTime: LocalDateTime, @field:Schema (description = "签到坐标" ) @field:JsonSerialize (using = GeometrySerializer::class ) val signInCoordinate: Point, @field:Schema (description = "签退时间" ) val signOutTime: LocalDateTime?, @field:Schema (description = "签退坐标" ) @field:JsonSerialize (using = GeometrySerializer::class ) val signOutCoordinate: Point?, @field:Schema (description = "工作时长,秒" ) val workingHours: Int ?, )
从以上代码可以看出,数据库字段类型、SQL 查询、ResultMap 配置、VO 类的属性类型都是对应的,看不出明显的问题。
那么,问题到底出在哪里呢?
解决过程 在仔细检查了所有的配置后,我开始怀疑问题可能出在 Kotlin 的 data class
与 MyBatis 的兼容性上。Kotlin 的 data class
默认是不会生成无参构造函数的,而 MyBatis 在映射结果集时,往往需要通过反射调用无参构造函数来实例化对象。
经过搜索,发现很多人在使用 Kotlin 的 data class
配合 MyBatis 时,都遇到了类似的问题。这是因为 MyBatis 默认需要一个无参构造函数来创建映射对象,如果没有无参构造函数,就可能导致无法实例化对象,进而导致类型不匹配的异常。
方法一:手动添加无参构造函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 data class AttendanceVO ( @field:Schema (description = "考勤记录ID" ) val id: Int = 0 , @field:Schema (description = "项目ID" ) val projectId: Int = 0 , @field:Schema (description = "项目名称" ) val projectName: String = "" , @field:Schema (description = "用户ID" ) val userId: Int = 0 , @field:Schema (description = "签到时间" ) val signInTime: LocalDateTime = LocalDateTime.now(), @field:Schema (description = "签到坐标" ) @field:JsonSerialize (using = GeometrySerializer::class ) val signInCoordinate: Point = Point(0.0 , 0.0 ), @field:Schema (description = "签退时间" ) val signOutTime: LocalDateTime? = null , @field:Schema (description = "签退坐标" ) @field:JsonSerialize (using = GeometrySerializer::class ) val signOutCoordinate: Point? = null , @field:Schema (description = "工作时长,秒" ) val workingHours: Int ? = null , )
通过为每个属性提供默认值,我们的 data class
就拥有了一个无参构造函数。但是我既然使用Kotlin
了,为所有成员变量都指定一个默认值是否有点过于不优雅了呢?我是无法容忍在data class
设置一些莫名其妙的初始值的。
方法二:使用 @NoArg
注解和编译器插件 经过查找,Jetbrains官方似乎也意识到了这个问题,提供了kotlin-noarg
插件。通过注解的方式为类生成空参构造函数。类似于Lombok
中的@NoArgsConstructor
。所以这个方法似乎比上面优雅一些,引入 Kotlin 的 kotlin-noarg
编译器插件的步骤如下。
添加自定义注解
定义一个只能用于类的 @NoArg
注解,当然这个注解的名称可以自定义
1 2 3 @Retention(AnnotationRetention.SOURCE) @Target(AnnotationTarget.CLASS) annotation class NoArg
配置编译器插件
在项目的 build.gradle
中添加插件配置:
1 2 3 4 5 6 plugins { id 'org.jetbrains.kotlin.plugin.noarg' version '1.9.25' // 请使用当前 Kotlin 版本 } noArg { annotation('com.yourpackage.NoArg') }
在 data class
上使用注解
1 2 3 4 @NoArg data class AttendanceVO( // 属性定义... )
编译器插件会自动为被 @NoArg
注解的类生成无参构造函数。
再次运行 在修改了 VO 类后,我再次运行项目,发现之前的异常不再出现,问题成功解决。
思考 这个问题的根源在于 MyBatis 依赖于 Java Bean 的规范,需要无参构造函数和 setter 方法。然而,Kotlin 的 data class
是不可变的,属性都是 val
,而且默认不生成无参构造函数。这种设计上的差异,导致了两者之间的兼容性问题。
MyBatis 作为一款经典的 ORM 框架,却没有很好地支持 Kotlin 的数据类,在面对现代语言的新特性时,显得有些滞后。这着实有些令人失望。
作为一名开发者,我们需要在实际项目中权衡不同技术的特性和限制。在这个例子中,使用 kotlin-noarg 插件为 Kotlin 的 data class 生成无参构造函数,是一种优雅且实用的解决方案。