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 分钟前
Nuxt3 功能篇
前端·javascript·后端
得物技术25 分钟前
营销会场预览直通车实践|得物技术
后端·架构·测试
Ice__Cai1 小时前
Flask 入门详解:从零开始构建 Web 应用
后端·python·flask·数据类型
武子康1 小时前
大数据-74 Kafka 核心机制揭秘:副本同步、控制器选举与可靠性保障
大数据·后端·kafka
紫穹1 小时前
006.LangChain Prompt Template
后端
whitepure1 小时前
万字详解JavaObject类方法
java·后端
切克呦1 小时前
通过 Cursor CLI 使用 GPT-5 的教程
前端·后端·程序员
Ice__Cai1 小时前
Flask 之 Request 对象详解:全面掌握请求数据处理
后端·python·flask·request·python web框架
gitboyzcf1 小时前
Git 常用命令
前端·git·后端
灵犀海棠1 小时前
FLASK项目快速构建
后端·python·flask