文章目录
一、前言
本篇讲解mybatis中如何将数据库中带下划线的字段user_name自动映射为实体类中驼峰命名的字段userName,以及Mybatis底层映射原理。
仔细阅读本篇文章,你将弄懂以下疑问:
- 数据库字段与实体类字段是如何匹配上的,如数据库user_name对应实体类userName
- 当数据库字段类型与实体类属性不匹配时如何转换的,如数据库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入参不管,出参管)
六、总结
通过本篇博文,我们可以得出以下结论。
- 入参时只跟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