Mybatis为何能正确映射数据

Mybatis为何总能正确映射数据?

我知道你可能看到这个标题很疑惑,mybatis就该正确的映射数据,只是我在工作中遇到了一个构造函数导致的困惑,我将带你看看我的疑惑。。。

背景:字段映射的疑问

数据库中存在以下表结构:

ID FIELD TYPE DESCRIBE
633 table_name FP 发票来源

使用以下Java对象接收数据:

java 复制代码
@Data
@EqualsAndHashCode(callSuper = false)
@NoArgsConstructor
@AllArgsConstructor
public class Field implements Serializable {

    private static final long serialVersionUID = 1L;

    @TableId(value = "ID", type = IdType.INPUT)
    private BigDecimal id;

    @TableField("FIELD")
    private String field;

    @TableField("TYPE")
    private String type;

    @TableField("DESCRIBE")
    private String describe;

    // 构造函数,注意type和field参数顺序与数据库字段顺序不一致
    public Field(BigDecimal id, String type, String field, String describe) {
        this.id = id;
        this.type = type;
        this.field = field;
        this.describe = describe;
        System.out.println(JSONUtil.toJsonStr(this));
    }
}

预期JSON输出为:

json 复制代码
{
  "id": 633,
  "type": "table_name",
  "field": "FP",
  "describe": "发票来源"
}

由于构造函数参数顺序导致type映射到数据库的FIELDfield映射到TYPE,本应出现错误。 然而,在我用mybatis plus调完selectList之后其中的对象实际是正确的:

json 复制代码
{
  "id": 633,
  "type": "FP",
  "field": "table_name",
  "describe": "发票来源"
}

这背后的原因是什么?

源码分析:Mybatis的映射机制

通过分析Mybatis源码,问题的答案在org.apache.ibatis.executor.resultset.DefaultResultSetHandler类的执行流程中:

1 执行流程

handleResultSets开始,依次调用handleResultSet -> handleRowValues -> handleRowValuesForSimpleResultMap,最终到达核心方法 getRowValue

2 核心逻辑

getRowValue方法中,Mybatis首先通过createResultObject调用构造函数创建对象,随后通过applyAutomaticMappings根据数据库字段和 @TableField注解的映射关系重新赋值。关键源码如下:

java 复制代码
private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException {
    final ResultLoaderMap lazyLoader = new ResultLoaderMap();
    Object rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix); // 创建对象
    if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
        final MetaObject metaObject = configuration.newMetaObject(rowValue);
        boolean foundValues = this.useConstructorMappings;
        if (shouldApplyAutomaticMappings(resultMap, false)) {
            foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues; // 重新赋值
        }
        foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues;
        foundValues = lazyLoader.size() > 0 || foundValues;
        rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
    }
    return rowValue;
}

因此,无论构造函数如何定义,Mybatis都会根据数据库字段和注解的映射关系重新设置对象属性,确保映射结果正确。

注意事项

为避免潜在问题,注意以下几点:

1 提供默认构造函数

Mybatis优先使用无参构造函数创建对象。建议使用Lombok的@NoArgsConstructor@AllArgsConstructor注解,确保生成默认构造函数和全参构造函数。

java 复制代码
@NoArgsConstructor
@AllArgsConstructor
public class Field implements Serializable { ...
}

2 避免多个自定义构造函数

若无默认构造函数,Mybatis尝试匹配其他构造函数。若定义多个构造函数,可能导致Mybatis无法确定使用哪个构造函数,抛出异常。如下代码会导致问题:

java 复制代码
public Field(BigDecimal id) {
    System.out.println(JSONUtil.toJsonStr(this, JSONConfig.create().setIgnoreNullValue(false)));
}

public Field(BigDecimal id, String type) {
    System.out.println(JSONUtil.toJsonStr(this, JSONConfig.create().setIgnoreNullValue(false)));
}

相关源码:

java 复制代码
private Object createByConstructorSignature(ResultSetWrapper rsw, Class<?> resultType, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) throws SQLException {
    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()); // !!!跑到这里
}

多个构造函数可能触发ExecutorException,提示未找到匹配的构造函数。

为何如此设计?

Mybatis的这种设计有其深层原因,主要体现在以下几个方面:

  • 解耦构造函数逻辑 构造函数可能包含复杂逻辑或副作用(如日志打印)。通过applyAutomaticMappings重新赋值,Mybatis确保映射结果仅依赖数据库字段和注解,避免构造函数干扰。
  • 支持复杂对象创建 某些对象需通过有参构造函数初始化,但Mybatis仍需保证字段映射正确。重新赋值的机制兼容各种构造函数,同时确保数据一致性。
  • 提升容错性 即使构造函数参数顺序错误,Mybatis的重新赋值机制能纠正错误,降低调试成本,提高开发效率。
  • 性能与一致性 使用MetaObject和反射机制,Mybatis高效处理字段映射,确保在复杂对象、嵌套映射等场景下的一致性。

总结

这次就是感到很困惑,没有对我实际的业务造成影响,在看完源码后感觉茅塞顿开,Mybatis通过applyAutomaticMappings根据@TableField注解重新赋值,确保数据映射与数据库字段一致,构造函数的定义不影响结果。但是在开发过程中还是得提供默认构造函数并避免多个自定义构造函数导致异常。不得不说mybatis的作者很强啊!这种设计解耦构造函数逻辑,增强容错性和灵活性,确保映射的可靠性和一致性。还是得多看看源码才是。

相关推荐
郝学胜-神的一滴1 小时前
SpringBoot实战指南:从快速入门到生产级部署(2025最新版)
java·spring boot·后端·程序人生
爷_6 小时前
字节跳动震撼开源Coze平台!手把手教你本地搭建AI智能体开发环境
前端·人工智能·后端
不过普通话一乙不改名9 小时前
第一章:Go语言基础入门之函数
开发语言·后端·golang
豌豆花下猫10 小时前
Python 潮流周刊#112:欢迎 AI 时代的编程新人
后端·python·ai
Electrolux10 小时前
你敢信,不会点算法没准你赛尔号都玩不明白
前端·后端·算法
whhhhhhhhhw11 小时前
Go语言-fmt包中Print、Println与Printf的区别
开发语言·后端·golang
ん贤11 小时前
Zap日志库指南
后端·go
Spliceㅤ11 小时前
Spring框架
java·服务器·后端·spring·servlet·java-ee·tomcat
IguoChan12 小时前
10. Redis Operator (3) —— 监控配置
后端
Micro麦可乐13 小时前
前端与 Spring Boot 后端无感 Token 刷新 - 从原理到全栈实践
前端·spring boot·后端·jwt·refresh token·无感token刷新