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
映射到数据库的FIELD
,field
映射到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的作者很强啊!这种设计解耦构造函数逻辑,增强容错性和灵活性,确保映射的可靠性和一致性。还是得多看看源码才是。