一、前言

本篇讲解mybatis中如何将数据库中带下划线的字段user_name自动映射为实体类中驼峰命名的字段userName,以及Mybatis底层映射原理。

仔细阅读本篇文章,你将弄懂以下疑问:

  1. 数据库字段与实体类字段是如何匹配上的,如数据库user_name对应实体类userName
  2. 当数据库字段类型与实体类属性不匹配时如何转换的,如数据库int怎么转换成实体类String

二、开启自动驼峰命名转换

将mapUnderscoreToCamelCase配置为true即可自动将数据库中的user_name转换为实体类中的userName

方式一: mybatis-config.xml文件中添加以下配置


<?xml version="1.0" encoding="UTF-8" ?>  
<!DOCTYPE configuration  
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"  
"http://mybatis.org/dtd/mybatis-3-config.dtd">  
<configuration>  
    <settings>  
        <setting name="mapUnderscoreToCamelCase" value="true" />  
    </settings>  
</configuration>

方式二: application.properties文件中开启配置

mybatis.configuration.mapUnderscoreToCamelCase=true
或
mybatis.configuration.map-underscore-to-camel-case=true

方式三: yml中开启配置

# Mybatis开启驼峰映射
mybatis:
  configuration:
    mapUnderscoreToCamelCase: true

方式四: SpringBoot中还可以使用自定义配置类的方式配置;给容器中添加一个ConfigurationCustomizer;

@Configuration
public class MyBatisConfig {

    @Bean
    public ConfigurationCustomizer configurationCustomizer() {
        return new ConfigurationCustomizer() {

            @Override
            public void customize(org.apache.ibatis.session.Configuration configuration) {
                configuration.setMapUnderscoreToCamelCase(true);
            }
        };
    }
}

三、案例讲解下划线映射为驼峰原理

新建dto实体类

/**
 * @author 
 */
@Data
public class TestDto {
    String aabb;
    String user;
    String userName;
}

mapper层查询并返回dto类

public interface TestMapper {
	
	@Select("select '1' as aa_BB,'2' as user, '3' as user_name")
	TestDto test();
}
相当于mapper.xml文件中的
<select id="test" resultType="com.demo.TestDto">
	select '1' as aa_BB,'2' as user, '3' as user_name
</select>

3.1、自动映射原理

1.mybatis会根据返回实体类维护一个map集合,key为实体类属性大写,value为实体类中的属性。

Map集合
AABB aabb;
USER user;
USERNAME userName;

2.若mapUnderscoreToCamelCase=true ,则会把sql中的下划线全部去除,然后转换为大写后去map中匹配,若命中则将值赋予对应的属性

四、自动映射的坑

4.1、mapUnderscoreToCamelCase=true的坑

由于mapUnderscoreToCamelCase=true是把下划线去除,然后转大写跟实体类进行匹配,所以数据库中aab_b也能命中实体类中的aaBb

五、源码讲解结果集映射为实体类原理

此处以mybatis-3.5.6的源码结合druid-1.1.16、mysql为例,讲解mybatis入参映射以及mybatis中resultType配合mapUnderscoreToCamelCase=true实现出参自动映射原理
在这里插入图片描述
在这里插入图片描述
看上面图片实体类类型,aaBb与数据库中字段名称不一致且类型也不一致,id是int类型但是在mapper中传入了String类型,实体类中userName是驼峰模式表中是user_name。那么mybtis是如何实现入参映射,以及sql执行后的出参映射呢?

首先进行执行
在这里插入图片描述
底层会调用MapperProxy中的invoke方法
在这里插入图片描述
下面跳过缓存,直接进入SimpleExecutor的doQuery里面的stmt = prepareStatement方法,此方法是进行 sql的占位符拼接。prepareStatement方法执行结束后,执行handler.query(stmt,resultHandler)方法,这个方法是执行sql,以及sql和实体类的映射。
在这里插入图片描述

引申说明:
上图boundSql中的sql及parameterMappings是在项目启动时就加载出来的,项目启动时底层会解析mapper.xml文件,其中javaType对应mapper.xml中的parameterType属性,jdbcType对应#{id,jdbcType=INTEGER}

我们先看一下这里的handler对象,这里创建了一个RoutingStatementHandler对象,然后赋值对象内的delegate属性为PreparedStatementHandler
在这里插入图片描述

扩展引申:
SimpleStatementHandler 和 PreparedStatementHandler 的区别是 SQL 语句是否包含变量,是否通过外部进行参数传入。SimpleStatementHandler 用于执行没有任何参数传入的 SQL,PreparedStatementHandler 需要对外部传入的变量和参数进行提前参数绑定和赋值。

  • SimpleStatementHandler: 管理 Statement 对象并向数据库中推送不需要预编译的SQL语句。
  • PreparedStatementHandler: 管理 Statement 对象并向数据中推送需要预编译的SQL语句。
  • CallableStatementHandler:管理 Statement 对象并调用数据库中的存储过程。

我们看一下prepareStatement方法,看看入参是怎么映射的,为什么jdbcType=INTEGER,但是传入了String类型也可以。
下面stmt = prepareStatement方法
在这里插入图片描述
下面是重点,是参数的处理。以及stme:PreparedStatementProxyImpl类去设置sql。
在这里插入图片描述
经过SimpleExecutor.prepareStatement方法—>PreparedStatementHandler.parameterize方法,最后调用了DefaultParameterHandler.parameterize方法
在这里插入图片描述
在这里插入图片描述
继续进入发现最后调用了StringTypeHandler完成了参数的映射,jdbcType没有用到,之后就是PreparedStatementProxyImpl去setString(1,”1”)。占位符设置。
在这里插入图片描述
可以发现直到sql占位符结束都是String类型,所以,Mybatis不会对入参的数据进行类型转换,只会去映射对应sql的Varcher类型。但是sql的那一列类型是INT类型,不过依然能查出来,因为Mysql里面有隐式转换。但是有时候转换的话,sql列的数据类型发生变化会造成索引失效。

执行handler.query(stmt,resultHandler)方法
在这里插入图片描述
在这里插入图片描述
上面我们已经讲过了,本篇文章的示例创建的delegate对象是PreparedStatementHandler,所以delegate.query会调用PreparedStatementHandler中的query方法。

ps .execute()先去执行,之后进行映射实体类。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

然后执行preparedStatement_execute
在这里插入图片描述
之后执行chain.preparedStatement_execute();
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
上面执行结束后,所有的结果就已经查出来了。结果集开始映射实体类
在这里插入图片描述

public List<Object> handleResultSets(Statement stmt) throws SQLException {
	ErrorContext.instance().activity("handling results").object(this.mappedStatement.getId());
	// 用于记录每个ResultSet映射出来的Java对象
	List<Object> multipleResults = new ArrayList();
	int resultSetCount = 0;
	// 从Statement中获取第一个ResultSet,其中对不同的数据库有兼容处理逻辑,
    // 这里拿到的ResultSet会被封装成ResultSetWrapper对象返回
	ResultSetWrapper rsw = this.getFirstResultSet(stmt);
	// 获取这条SQL语句关联的全部ResultMap规则。如果一条SQL语句能够产生多个ResultSet,
    // 那么在编写Mapper.xml映射文件的时候,我们可以在SQL标签的resultMap属性中配置多个
    // <resultMap>标签的id,它们之间通过","分隔,实现对多个结果集的映射
	List<ResultMap> resultMaps = this.mappedStatement.getResultMaps();
	int resultMapCount = resultMaps.size();
	this.validateResultMapsCount(rsw, resultMapCount);

	while(rsw != null && resultMapCount > resultSetCount) { // 遍历ResultMap集合
		ResultMap resultMap = (ResultMap)resultMaps.get(resultSetCount);
		// 根据ResultMap中定义的映射规则处理ResultSet,并将映射得到的Java对象添加到
	    // multipleResults集合中保存
		this.handleResultSet(rsw, resultMap, multipleResults, (ResultMapping)null);
		//获取下一个resultSet
		rsw = this.getNextResultSet(stmt);
		 // 清理nestedResultObjects集合,这个集合是用来存储中间数据的
		this.cleanUpAfterHandlingResultSet();
		// 递增ResultSet编号
		++resultSetCount;
	}
	// 下面这段逻辑是根据ResultSet的名称处理嵌套映射
	String[] resultSets = this.mappedStatement.getResultSets();
	if (resultSets != null) {
		while(rsw != null && resultSetCount < resultSets.length) {
			ResultMapping parentMapping = (ResultMapping)this.nextResultMaps.get(resultSets[resultSetCount]);
			if (parentMapping != null) {
				String nestedResultMapId = parentMapping.getNestedResultMapId();
				ResultMap resultMap = this.configuration.getResultMap(nestedResultMapId);
				this.handleResultSet(rsw, resultMap, (List)null, parentMapping);
			}

			rsw = this.getNextResultSet(stmt);
			this.cleanUpAfterHandlingResultSet();
			++resultSetCount;
		}
	}
	// 返回全部映射得到的Java对象
	return this.collapseSingleResultList(multipleResults);
}

ResultSetWrapper 主要用于封装 ResultSet 的一些元数据,其中记录了 ResultSet 中每列的名称、对应的 Java 类型、JdbcType 类型以及每列对应的 TypeHandler。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
这里我们重点来看 handleRowValuesForSimpleResultMap() 方法如何映射一个 ResultSet 的,该方法的核心步骤可总结为如下。

private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
    DefaultResultContext<Object> resultContext = new DefaultResultContext();
    ResultSet resultSet = rsw.getResultSet();
    //跳过RowBounds设置的offset值
    this.skipRows(resultSet, rowBounds);
    //判断数据是否小于limit,如果小于limit的话就不断的循环取值
    while(this.shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {
        ResultMap discriminatedResultMap = this.resolveDiscriminatedResultMap(resultSet, resultMap, (String)null);
        //在此处完成结果集与实体类的映射
        Object rowValue = this.getRowValue(rsw, discriminatedResultMap, (String)null);
        this.storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
    }

}

private boolean shouldProcessMoreRows(ResultContext<?> context, RowBounds rowBounds) throws SQLException {
	//判断数据是否小于limit,小于返回true
    return !context.isStopped() && context.getResultCount() < rowBounds.getLimit();
}

//跳过不需要的行,应该就是rowbounds设置的limit和offset
private void skipRows(ResultSet rs, RowBounds rowBounds) throws SQLException {
	if (rs.getType() != ResultSet.TYPE_FORWARD_ONLY) {
	  if (rowBounds.getOffset() != RowBounds.NO_ROW_OFFSET) {
		rs.absolute(rowBounds.getOffset());
	  }
	} else {
	  //跳过RowBounds中设置的offset条数据,只能逐条滚动到指定位置
	  for (int i = 0; i < rowBounds.getOffset(); i++) {
		rs.next();
	  }
	}
}

1.执行 skipRows() 方法跳过多余的记录,定位到指定的行。
2.通过 shouldProcessMoreRows() 方法,检测是否还有需要映射的数据记录。
3.如果存在需要映射的记录,则先通过 resolveDiscriminatedResultMap() 方法处理映射中用到的 Discriminator,决定此次映射实际使用的 ResultMap。
4.通过 getRowValue() 方法对 ResultSet 中的一行记录进行映射,映射规则使用的就是步骤 3 中确定的 ResultMap。
5.执行 storeObject() 方法记录步骤 4 中返回的、映射好的 Java 对象。

在这里插入图片描述
当前方法去执行映射的applyAutomaticMappings()去了。执行结束就映射完成了。返回rowValue结果。
在这里插入图片描述
下面执行applyAutomaticMappings(),此方法作用是将sql返回值映射到实体类。
在这里插入图片描述
createAutomaticMappings方法返回结果集与实体类的映射关系。即维护数据库aab_b字段对应实体类aaBb字段,user_name对应userName;

在这里插入图片描述
在这里插入图片描述

然后就是根据数据库与实体类的映射关系循环设置值
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
当所有字段都赋值完毕后返回结果
在这里插入图片描述
总结:mybatis是根据java的入参类型进行sql查询的,(最好规范使用),如当前传入的是一个String类型的参数,那么sql里面就是Varcher类型,但是数据库里面的如果是int类型,有可能造成当前列的索引失效(隐式转换)。sql执行后返回结果后的时候,返回的是数据库类型,但是映射到实体类的时候,mybatis会自动给转成需要的类型,如返回的是int类型,但是实体类是String类型,那么就会自动转成String类型映射到实体类。(mybatis入参不管,出参管)



六、总结

通过本篇博文,我们可以得出以下结论。

  1. 入参时只跟parameterType属性有关,jdbcType用不上,所以当两者类型冲突时也可以拼接sql, 只是最后执行时可能会有问题。

UserMapper.java

public interface UserMapper {

    User selectById(String id);
}

UserMapper.xml

<select id="selectById" resultType="com.example.demo.entity.User"
            parameterType="java.lang.String">
  select id,user_name,aab_b from user where id = #{id,jdbcType=INTEGER}
</select>





参考文章:https://learn.lianglianglee.com/%E4%B8%93%E6%A0%8F/%E6%B7%B1%E5%85%A5%E5%89%96%E6%9E%90%20MyBatis%20%E6%A0%B8%E5%BF%83%E5%8E%9F%E7%90%86-%E5%AE%8C/14%20%20%E6%8E%A2%E7%A9%B6%20MyBatis%20%E7%BB%93%E6%9E%9C%E9%9B%86%E6%98%A0%E5%B0%84%E6%9C%BA%E5%88%B6%E8%83%8C%E5%90%8E%E7%9A%84%E7%A7%98%E5%AF%86%EF%BC%88%E4%B8%8A%EF%BC%89.md
https://blog.csdn.net/m0_65789764/article/details/130795359


创作不易,欢迎打赏,你的鼓励将是我创作的最大动力。

在这里插入图片描述

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐