MyBatis 数据表与实体映射的隐藏陷阱

ops/2024/10/11 2:35:59/

这两天在处理一个线上问题时,发现Mybatis数据表和实体映射的时候会埋一个坑。这个问题看似微小,但却可能在关键时刻给我们带来不小的困扰。接下来,让我们深入剖析这个问题,并探究其发生的根源。

一、问题描述

我们在使用 Mybatis或者Mybatis-plus时,通常会创建一个数据传输对象(DTO)来封装数据库查询结果。假设我们有一个 EmployeeDTO 类,用于存储员工信息:

java">public class EmployeeDTO{private Integer employeeNo;private Long employeeId;// 其它字段忽略
}

同时,我们有一段 SQL 语句,用于查询员工列表:

<select id="getEmployeeList" resultType="com.demo.api.vo.EmployeeVo">selectemployee_id as employeeId,employee_no as employeeNofrom employee<where><if test="req.employeeId != null">ra.employee_id = #{req.employeeId}</if>and id > 0 and deleted = 0</where>order by id desclimit #{pageNo}, #{pageSize}</select>

最初,数据表中employee_id属性的类型是int, DTO中的employeeId属性类型是Integer。随着业务发展,employeeId 的值超过了 int 类型的最大值,于是我们将 employee_id 字段类型修改为 bigint,DTO 中 employeeId 属性类型也对应修改为 Long。然而,这个看似简单的修改却导致了程序抛出 NumberOutOfRange 异常:

java">org.springframework.dao.DataIntegrityViolationException: Error attempting to get column 'EmployeeId' from result set.  Cause: java.sql.SQLDataException: Value '2536800000' is outside of valid range for type java.lang.Integer
; Value '2536800000' is outside of valid range for type java.lang.Integer; nested exception is java.sql.SQLDataException: Value '2536800000' is outside of valid range for type java.lang.Integer

接下来,让我们一起剖析Mybatis的关于resultSet处理的源码,探究问题发生的本源。

二、MyBatis 核心方法分析

1. selectList(String statement, Object parameter, RowBounds rowBounds)

  • 作用
    selectList方法主要用于执行数据库查询操作,并返回一个包含查询结果的列表。这个方法在很多场景下都非常有用,例如获取数据库中的多条记录,实现数据的批量查询等。
  • 实现
    在DefaultSqlSession中,selectList方法会调用query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER)方法执行数据库查询操作。
    (中间会有很多拦截器做相关的逻辑处理,如分页的拦截器,sql验证的拦截器等等,后续有时间了出一篇博客来讲解。)

2. query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)方法

  • 作用
    主要用于执行数据库查询操作,并根据特定的条件决定是从缓存中获取查询结果还是直接从数据库中进行查询。它通过CacheKey是否存在来判断执行哪种查询方式,确保在合适的情况下利用缓存以提高查询效率
  • 实现
    在BaseExecutor类中,query方法的实现逻辑如下:
java">	list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;if (list != null) {handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);} else {list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);}

这里先判断结果处理器resultHandler是否为null,如果是,则尝试从本地缓存localCache中根据CacheKey获取查询结果列表。

如果获取到的列表不为null,说明缓存中有可用的结果,此时会调用handleLocallyCachedOutputParameters方法来处理本地缓存中的输出参数。

这里假定list为null。因此,该方法会调用queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql)方法执行数据库的查询操作。

3. queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)方法

  • 作用
    这个方法的主要作用是在缓存中没有可用结果时,发起对数据库的查询操作。它协调了各个组件之间的交互,确保能够从数据库中获取到所需的数据。
  • 实现
    该方法会调用SimpleExecutor#doQuery(ms, parameter, rowBounds, resultHandler, boundSql)方法执行具体的查询逻辑。
    SimpleExecutor是 MyBatis 中的一个执行器,负责实际执行数据库查询操作。

4. doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) 方法

在 MyBatis 框架中,doQuery方法无疑是最核心的方法之一,在数据库查询操作中发挥着至关重要的作用。

  • 作用
    执行数据库查询操作,并返回查询结果的列表。
    它接受多个参数,包括映射语句(MappedStatement)、查询参数、结果集范围、结果处理器和绑定的 SQL 对象等,通过一系列操作创建语句处理器(StatementHandler)、准备数据库语句(Statement)(这个是重中之重,后面所有的方法都会用到这个作为参数),然后执行查询并处理结果。最后,无论查询是否成功,都会确保关闭数据库语句以释放资源。
  • 实现
java">public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {Statement stmt = null;try {Configuration configuration = ms.getConfiguration();StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);stmt = prepareStatement(handler, ms.getStatementLog());return handler.query(stmt, resultHandler);} finally {closeStatement(stmt);}}

该方法会调用PreparedStatementHandler类的query(stmt, resultHandler)方法执行具体的查询操作,具体的处理逻辑在handleResultSets(ps)方法中。

5. handleResultSets(Statement stmt)方法

在 MyBatis 框架中,handleResultSets方法也是最核心的方法之一,在数据库查询结果的处理过程中起着至关重要的作用。

  • 作用
    这个方法主要负责处理数据库查询返回的结果集。它的任务是数据库中的数据转换为 Java 对象,以便在应用程序中进行进一步的处理和使用
  • 实现
    该方法会调用handleRowValues方法来处理具体每一行的数据。这个设计体现了 MyBatis 在处理复杂数据结构时的精细分工和模块化设计理念。

6. handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping)方法

  • 作用
    这个方法主要负责处理结果集中的行数据,将其转换为 Java 对象的属性值
    它根据传入的参数,如ResultSetWrapper(封装了结果集的相关信息)、ResultMap(描述了数据库结果集与 Java 对象之间的映射关系)、ResultHandler(用于处理查询结果的处理器)等,来确定如何将数据库中的每一行数据映射到 Java 对象中。
  • 实现
java">public void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {if (resultMap.hasNestedResultMaps()) {ensureNoRowBounds();checkResultHandler();handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);} else {handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);}}

该方法首先判断结果映射中是否存在嵌套的结果映射。如果存在嵌套结果映射,会进行一些特殊的处理,如确保没有行范围限制、检查结果处理器等,并调用handleRowValuesForNestedResultMap方法来处理嵌套结果映射的情况。

如果不是嵌套查询,就会调用handleRowValuesForSimpleResultMap方法来处理具体的结果映射。

7. handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping)方法

  • 作用
    这个方法专注于处理没有嵌套结构的简单结果映射。其主要任务是数据库查询结果集中的每一行数据,准确地映射到 Java 对象的相应属性上,以实现数据从数据库到应用程序对象的转换。
  • 实现
    调用getRowValue(rsw, discriminatedResultMap, null)方法获取当前行的 Java 对象值。这个方法根据结果映射规则从结果集中读取数据,并进行必要的类型转换和数据验证,将数据转换为 Java 对象的属性值

8. getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix)方法

getRowValue方法也是Mybatis框架中DefaultResultSetHandler类中最核心的方法之一。

  • 作用
    从给定的ResultSetWrapper(包含SQL的相关信息)和ResultMap(包含java DTO的信息)中,根据特定的列前缀,获取结果集中一行数据的值,并进行适当的处理和转换,以便后续能够准确地映射到 Java 对象的属性中。
  • 实现
    调用createResultObject方法,根据ResultSetWrapper、ResultMap和懒加载映射对象以及列前缀,创建一个结果对象。这个步骤是获取行值的核心,它将从结果集中读取数据,并根据结果映射规则进行类型转换和数据验证,创建一个 Java 对象实例。

9. createResultObject(ResultSetWrapper rsw, ResultMap resultMap, List<Class<?>> constructorArgTypes, List constructorArgs, String columnPrefix)方法

  • 作用
    通过综合考虑结果映射、结果对象类型、构造函数映射等多种因素,以确定最佳的方式来实例化结果对象,为后续的数据处理和业务逻辑提供准确的对象表示。
  • 实现
java">private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, List<Class<?>> constructorArgTypes, List<Object> constructorArgs, String columnPrefix)throws SQLException {final Class<?> resultType = resultMap.getType();final MetaClass metaType = MetaClass.forClass(resultType, reflectorFactory);final List<ResultMapping> constructorMappings = resultMap.getConstructorResultMappings();if (hasTypeHandlerForResultObject(rsw, resultType)) {return createPrimitiveResultObject(rsw, resultMap, columnPrefix);} else if (!constructorMappings.isEmpty()) {return createParameterizedResultObject(rsw, resultType, constructorMappings, constructorArgTypes, constructorArgs, columnPrefix);} else if (resultType.isInterface() || metaType.hasDefaultConstructor()) {return objectFactory.create(resultType);} else if (shouldApplyAutomaticMappings(resultMap, false)) {return createByConstructorSignature(rsw, resultType, constructorArgTypes, constructorArgs);}throw new ExecutorException("Do not know how to create an instance of " + resultType);}

会分情况创建结果对象,这里命中的是自动映射,所以会调用createByConstructorSignature方法,这个方法会通过反射根据构造器签名来创建对象,确保数据可以准确地填充到对象的属性中。

10. createByConstructorSignature(ResultSetWrapper rsw, Class<?> resultType, List

  • 作用
    通过反射机制,根据给定的构造器签名来创建 Java 对象。在处理数据库查询结果集到 Java 对象的映射过程中,这个方法确保了能够正确地实例化 Java 对象,使得数据可以准确地填充到对象的属性中。
  • 实现
java">private Object createByConstructorSignature(ResultSetWrapper rsw, Class<?> resultType, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) throws SQLException {// 使用反射创建Employee对象final Constructor<?>[] constructors = resultType.getDeclaredConstructors();final Constructor<?> defaultConstructor = findDefaultConstructor(constructors);if (defaultConstructor != null) {return createUsingConstructor(rsw, resultType, constructorArgTypes, constructorArgs, defaultConstructor);} else {for (Constructor<?> constructor : constructors) {if (allowedConstructorUsingTypeHandlers(constructor, rsw.getJdbcTypes())) {return createUsingConstructor(rsw, resultType, constructorArgTypes, constructorArgs, constructor);}}}throw new ExecutorException("No constructor found in " + resultType.getName() + " matching " + rsw.getClassNames());}

通过反射获取给定结果类型resultType(Employee类)的所有声明构造器,默认取第一个。然后会调用createUsingConstructor方法来创建对象。

11. createUsingConstructor(ResultSetWrapper rsw, Class<?> resultType, List

createUsingConstructor方法也是 MyBatis 中最核心的方法之一。

  • 作用
    使用特定的构造函数来创建 Java 对象,确保从数据库查询结果集中准确地提取数据并填充到对象的属性中。
  • 实现
java">private Object createUsingConstructor(ResultSetWrapper rsw, Class<?> resultType, List<Class<?>> constructorArgTypes, List<Object> constructorArgs, Constructor<?> constructor) throws SQLException {boolean foundValues = false;for (int i = 0; i < constructor.getParameterTypes().length; i++) {// parameterType.name = Java.lang.Integer;Class<?> parameterType = constructor.getParameterTypes()[i];// columnName = employeeId// 所以会对不上。String columnName = rsw.getColumnNames().get(i);TypeHandler<?> typeHandler = rsw.getTypeHandler(parameterType, columnName);Object value = typeHandler.getResult(rsw.getResultSet(), columnName);constructorArgTypes.add(parameterType);constructorArgs.add(value);foundValues = value != null || foundValues;}return foundValues ? objectFactory.create(resultType, constructorArgTypes, constructorArgs) : null;}

首先,会遍历给定构造器类的参数列表。对于每个参数,确定其参数类型和对应的数据库列名。在这个过程中,通过反射获取到的Employee类的构造器类,构造器中每个参数的顺序和Employee定义时一致。而rsw就是第4个核心方法中提到的Statement,rsw.getColumnNames()[i]获取的就是SQL中写的字段顺序。(到这里其实已经找到问题所在了)。

在这个过程中,还通过获取构造器类的参数类型和对应的数据库列名,找到合适的类型处理器(employeeId的类型处理器是java.lang.Integer)。类型处理器负责将数据库中的数据转换为 Java 对象参数所需的类型。

然后,将参数类型和转换后的值分别添加到构造器类的参数类型列表和参数值列表中,并更新是否找到值的标志。根据是否找到值的情况,决定是否创建对象并返回。

三、解决方案

找到了问题根源,解决方案也就呼之欲出,解决方案很简单。
调整 SQL 语句中列的顺序: 将 employee_no 列放在 employee_id 列之前,确保 MyBatis 能够按照正确的顺序进行映射,或者调整Employee类中属性定义的顺序。

四、总结

通过深入 Mybatis关于resultSet处理的源码 ,我们发现,Mybatis在处理结果集ResultSet时,会按照构造器类的参数顺序和ResultSet的列顺序构造对象。在上面的情况下,SQL查询结果的列顺序和DTO中属性顺序不匹配,导致类型处理器使用了错误的类型即尝试把一个很大的bigint赋值给Integer类型

解决这个问题的方法很简单,但更重要的是,我们应该从中吸取教训,在日常开发中更加注重细节,避免类似问题的发生。同时,深入理解 Mybatis 的工作原理,也有助于我们更好地掌握和使用 Mybatis 框架。

本文期望能够帮助大家避开 Mybatis数据表与实体映射的 Unexpected 坑的同时,提供一种解决问题的排查思路,这种软实力才是最重要的


http://www.ppmy.cn/ops/123764.html

相关文章

Springboo通过http请求下载文件到服务器

这个方法将直接处理从URL下载数据并将其保存到文件的整个过程。下面是一个这样的方法示例&#xff1a; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.HttpURLConnection…

git fetch 和 git pull 的区别

git fetch 和 git pull 的区别 git fetch 功能&#xff1a;git fetch 用于从远程仓库获取最新的代码和提交信息&#xff0c;并将其保存到本地仓库的相应远程跟踪分支中&#xff0c;不会自动合并或修改当前的工作目录或当前分支。 合并&#xff1a;此命令不会自动合并获取的更新…

vue双向绑定/小程序双向绑定区别

Vue双向绑定与小程序双向绑定在实现方式、语法差异以及功能特性上均存在显著区别。以下是对这两者的详细比较&#xff1a; 一、实现方式 Vue双向绑定 Vue的双向绑定主要通过其响应式数据系统实现。Vue使用Object.defineProperty()方法&#xff08;或在Vue 3中使用Proxy对象&am…

使用axios封装AJAX

一 、Http 请求报文 包括了三部分: 求情行、请求头,请求体。 1、请求行: 是HTTP请求的第一行,包含了请求方法、请求目标和HTTP协议版本。常用的请求方法有GET、POST、PUT、DELETE等,用于指定客户端希望服务器执行的操作。请求目标是指请求的资源路径,可以是一个具体的…

大贤3D家谱-一键寻找家谱本源

点击“溯”&#xff0c;您可以追溯到当前节点的家谱本源。 这一功能将帮助您更深入地了解家族的历史和背景。 通过这一操作&#xff0c;系统会自动标注出与您当前节点相关的祖先信息&#xff0c;并以金色字体突出显示&#xff0c;便于您快速识别和查看。 演示如下&#xff1…

matlab初学习记录

文章目录 内置函数与变量matlab 编辑器数组等间距向量数组函数数组索引提取多个元素 对向量执行数组计算查看文档 画图添加注释 实践导入数据关系运算符分支恒星运动 matlab 学习看入门之旅 先计算等号右边再计算等号左边。 工作区记录等号右边的变量。 ; 表示的是抑制输出。…

基于 CSS Grid 的简易拖拉拽 Vue3 组件,从代码到NPM发布(1)- 拖拉拽交互

基于特定的应用场景&#xff0c;需要在页面中以网格的方式&#xff0c;实现目标组件在网格中可以进行拖拉拽、修改大小等交互。本章开始分享如何一步步从代码设计&#xff0c;最后到如何在 NPM 上发布。 请大家动动小手&#xff0c;给我一个免费的 Star 吧~ 大家如果发现了 Bug…

基于深度学习的不遗忘训练

基于深度学习的不遗忘训练&#xff08;也称为抗遗忘训练或持久性学习&#xff09;是针对模型在学习新任务时可能会忘记已学习内容的一种解决方案。该方法旨在使深度学习模型在不断接收新信息的同时&#xff0c;保持对旧知识的记忆。以下是这一领域的主要内容和方法&#xff1a;…