项目概况

第05篇:Mybatis的SQL执行流程分析

2026-01-2921 min read后端技术
Mybatis

一、前言

前面我们知道了Mybatis是如何进行代理的, 但是最终 PlainMethodInvoker 中是如何将参数转组装成Sql,并执行处理Sql返回值的地方还都没看到。本篇我们就带着如下三个问题开始我们的探索吧。

本篇内容因为涉及跟jdbc的知识,如果对这部分内容有点遗忘,请先JDBC知识复习,另本篇内容知识点较多,目录较复杂,建议根据文字结合 代码在实践的过程中一起学习。最好也可以自己debug一下。会收获更大。做好准备现在发车。

二、流程分析

2.1 Sql是如何组装参数的?

在组装参数之前我们先来提一个小问题,sql的类型是如何判断的。sql类型有增删该查。 除了查询会有结果集外,其他三种都是返回更新行数。他们对应的处理逻辑也是不一样的。 我们要先弄清这个问题。

2.1.1 sql类型如何判断?

我们知道sql的类型是可以通过关键字来判断的,如select/update/delete/insert。那么在Mybatis中哪里能输入sql呢? 一种有2种方式。

  1. 在Mapper.xml中直接编写sql,如下示例。
xml
1<?xml version="1.0" encoding="UTF-8"?> 2<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> 3<mapper namespace="orm.example.dal.mapper.TUserMapper"> 4 <delete id="deleteByPrimaryKey" parameterType="java.lang.String"> 5 delete from T_USER 6 where token_id = #{tokenId,jdbcType=CHAR} 7 </delete> 8 <insert id="insert" parameterType="orm.example.dal.model.TUser"> 9 insert into T_USER (token_id, uid, name) 10 values (#{tokenId,jdbcType=CHAR}, #{uid,jdbcType=INTEGER}, #{name,jdbcType=CHAR}) 11 </insert> 12 <update id="updateByPrimaryKey" parameterType="orm.example.dal.model.TUser"> 13 update T_USER 14 set uid = #{uid,jdbcType=INTEGER}, 15 name = #{name,jdbcType=CHAR} 16 where token_id = #{tokenId,jdbcType=CHAR} 17 </update> 18 <select id="selectAll" resultMap="BaseResultMap"> 19 select token_id, uid, name 20 from T_USER 21 </select> 22 23</mapper>
  1. 在Mapper类中使用注解编写sql
java
1public interface TUserMapper { 2 @Select("select * from t_user where id = #{id}") 3 TUser selectById(Long id); 4}

这些sql信息都保存在 MappedStatement。在PlainMethodInvoker通过SqlCommand进行调用。

  • line(9) 最终通过type = ms.getSqlCommandType() 获取sql的类型
java
1SqlCommand sqlCommand = new SqlCommand(config, mapperInterface, method); 2 3// 构造参数中找MappedStatement 4public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) { 5 final String methodName = method.getName(); 6 final Class<?> declaringClass = method.getDeclaringClass(); 7 MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass, 8 configuration); 9 type = ms.getSqlCommandType(); 10} 11// 寻找方法是接口全路径名.方法名 12private MappedStatement resolveMappedStatement(){ 13 String statementId = mapperInterface.getName() + "." + methodName; 14 configuration.hasStatement(statementId) 15}

那么MappedStatement中的SqlCommandType是如何获取的呢?

2.1.1.1 xml文件方式

解析xml标签来实现

XMLMapperBuilder#parseStatementNode

  • line(11) 通过标签来映射成指定的类型SqlCommandType
xml
1public class XMLStatementBuilder extends BaseBuilder { 2 public void parseStatementNode() { 3 String id = context.getStringAttribute("id"); 4 String databaseId = context.getStringAttribute("databaseId"); 5 6 if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) { 7 return; 8 } 9 10 String nodeName = context.getNode().getNodeName(); 11 SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH)); 12 } 13} 14public enum SqlCommandType { 15 UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH 16}

2.1.1.2 注解方式

一定是解析注解方法 AnnotationWrapper。将不同的注解解析成SqlCommandType。如下伪代码。通过解析方法上的注解,判断注解类型,来确定sql的类型。 MapperAnnotationBuilder#getAnnotationWrapper(method, true, statementAnnotationTypes)

java
1private class AnnotationWrapper { 2 private final Annotation annotation; 3 private final String databaseId; 4 private final SqlCommandType sqlCommandType; 5 6 AnnotationWrapper(Annotation annotation) { 7 super(); 8 this.annotation = annotation; 9 if (annotation instanceof Select) { 10 databaseId = ((Select) annotation).databaseId(); 11 sqlCommandType = SqlCommandType.SELECT; 12 } else if (annotation instanceof Update) { 13 databaseId = ((Update) annotation).databaseId(); 14 sqlCommandType = SqlCommandType.UPDATE; 15 } else if (annotation instanceof Insert) { 16 databaseId = ((Insert) annotation).databaseId(); 17 sqlCommandType = SqlCommandType.INSERT; 18 } else if (annotation instanceof Delete) { 19 databaseId = ((Delete) annotation).databaseId(); 20 sqlCommandType = SqlCommandType.DELETE; 21 } else if (annotation instanceof SelectProvider) { 22 databaseId = ((SelectProvider) annotation).databaseId(); 23 sqlCommandType = SqlCommandType.SELECT; 24 } else if (annotation instanceof UpdateProvider) { 25 databaseId = ((UpdateProvider) annotation).databaseId(); 26 sqlCommandType = SqlCommandType.UPDATE; 27 } else if (annotation instanceof InsertProvider) { 28 databaseId = ((InsertProvider) annotation).databaseId(); 29 sqlCommandType = SqlCommandType.INSERT; 30 } else if (annotation instanceof DeleteProvider) { 31 databaseId = ((DeleteProvider) annotation).databaseId(); 32 sqlCommandType = SqlCommandType.DELETE; 33 } else { 34 sqlCommandType = SqlCommandType.UNKNOWN; 35 if (annotation instanceof Options) { 36 databaseId = ((Options) annotation).databaseId(); 37 } else if (annotation instanceof SelectKey) { 38 databaseId = ((SelectKey) annotation).databaseId(); 39 } else { 40 databaseId = ""; 41 } 42 } 43 } 44 45 Annotation getAnnotation() { 46 return annotation; 47 } 48 49 SqlCommandType getSqlCommandType() { 50 return sqlCommandType; 51 } 52} 53

到这里我们知道了sql类型是如何区分出来的,既然能区分出来,就知道如何去执行sql了。 是不是很简单? 当然看的话很简单,但是如何让你自己来找,你能找到吗? 所以建议在阅读的时候 要自己去源码中找找。

2.1.2 sql参数如何组装?

在mybatis中有两种处理sql参数的地方,第一种是#{} 占位符,第二种是${} 变量符。这两种都是处理参数的方式。那说到这里,不得不提的就是sql注入的黑客技术。 sql注入就是就是利用了变量符。将我们原来的sql进行恶意的修改。举一个例子。下面根据用户id和用户密码查询用户信息。

select * from t_user as u where u.pass = ${user_pass} and u.id = ${user_id}

那么如何在不知道密码只有用户id的情况下查询到用户信息呢? 我们只需要将sql转换成下面这样即可。

select * from t_user as u where u.pass = '' or 1 = 1 and u.id = ${user_id}

那mybatis允许我们这样做吗? 允许,如果我们使用的是 ${} 变量符,那么mybatis只是将参数和变量符进行替换。你输入的参数可能也会被当成sql去执行了。如下代码示例。

java
1public interface T4UserMapper { 2 /** 3 * 获取用户信息 4 * 5 * @param uid 用户id 6 * @param tokenId token 7 * @return TUser 8 */ 9 @Select("select * from t_user where token_id = ${token_id} and uid = ${uid}") 10 TUser queryUserById(@Param("uid") Long uid, @Param("token_id") String tokenId); 11} 12public class Test{ 13 @Test 14 public void sql(){ 15 // 读取配置信息 16 InputStream mapperInputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("example05/mybatisConfig.xml"); 17 // 生成SqlSession工厂,SqlSession从名字上看就是,跟数据库交互的会话信息,负责将sql提交到数据库进行执行 18 SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(mapperInputStream, "development"); 19 // 获取Mybatis配置信息 20 Configuration configuration = sqlSessionFactory.getConfiguration(); 21 SqlSession sqlSession = sqlSessionFactory.openSession(false); 22 // debug 23 T4UserMapper mapper = configuration.getMapper(T4UserMapper.class, sqlSession); 24 // 模拟sql注入 25 System.out.println(mapper.queryUserById(37L,"0 or 1 = 1")); 26 } 27} 28 29Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@62ddbd7e] 30==> Preparing: select * from t_user where token_id = 0 or 1 = 1 and uid = 37 31==> Parameters: 32<== Columns: uid, name, token_id 33<== Row: 37, 无天, 60 34<== Total: 1 35TUser(tokenId=null, uid=37, name=无天)

要想避免这样的问题,我们只需要将${} 变量符,都替换成#{} 占位符就好了。那么Mybatis只会将你的参数当做是参数处理,不会当做是sql执行。如下代码示例。

java
1public interface T4UserMapper { 2 /** 3 * 获取用户信息 4 * 5 * @param uid 用户id 6 * @param tokenId token 7 * @return TUser 8 */ 9 @Select("select * from t_user where token_id = #{token_id} and uid = #{uid}") 10 TUser queryUserById(@Param("uid") Long uid, @Param("token_id") String tokenId); 11} 12public class Test{ 13 @Test 14 public void sql(){ 15 // 读取配置信息 16 InputStream mapperInputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("example05/mybatisConfig.xml"); 17 // 生成SqlSession工厂,SqlSession从名字上看就是,跟数据库交互的会话信息,负责将sql提交到数据库进行执行 18 SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(mapperInputStream, "development"); 19 // 获取Mybatis配置信息 20 Configuration configuration = sqlSessionFactory.getConfiguration(); 21 SqlSession sqlSession = sqlSessionFactory.openSession(false); 22 // debug 23 T4UserMapper mapper = configuration.getMapper(T4UserMapper.class, sqlSession); 24 // 模拟sql注入 => null 25 System.out.println(mapper.queryUserById(37L,"0 or 1 = 1")); 26 } 27} 28 29Created connection 798981583. 30Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@2f9f7dcf] 31==> Preparing: select * from t_user where token_id = ? and uid = ? 32==> Parameters: 0 or 1 = 1(String), 37(Long) 33<== Total: 0 34null

以上演示代码可以在 com.test.example05.SqlParseTest中找到。那么无论是变量符还是占位符,其实都是sql组装,下面我们正式开始学习。

==同样我们先提两个问题==

2.1.2.1 方法参数如何来解析

关键代码就在MapperMethod的execute的入参 Object [] args; 关于参数的处理都在这里处理了。MethodSignature#convertArgsToSqlCommandParam。

java
1public class MapperMethod { 2 public Object execute(SqlSession sqlSession, Object[] args) { 3 Object result; 4 switch (command.getType()) { 5 case INSERT: { 6 Object param = method.convertArgsToSqlCommandParam(args); 7 result = rowCountResult(sqlSession.insert(command.getName(), param)); 8 break; 9 } 10 case UPDATE: { 11 Object param = method.convertArgsToSqlCommandParam(args); 12 result = rowCountResult(sqlSession.update(command.getName(), param)); 13 break; 14 } 15 case DELETE: { 16 Object param = method.convertArgsToSqlCommandParam(args); 17 result = rowCountResult(sqlSession.delete(command.getName(), param)); 18 break; 19 } 20 .... 21 return result; 22 } 23}
java
1public Object convertArgsToSqlCommandParam(Object[] args) { 2 return paramNameResolver.getNamedParams(args); 3}

参数会被解析成什么样呢? 关键代码就在这里。

java
1 public Object getNamedParams(Object[] args) { 2 final int paramCount = names.size(); 3 // 没有参数直接返回 4 if (args == null || paramCount == 0) { 5 return null; 6 } else if (!hasParamAnnotation && paramCount == 1) { 7 // 没有注解只有一个参数 8 Object value = args[names.firstKey()]; 9 return wrapToMapIfCollection(value, useActualParamName ? names.get(0) : null); 10 } else { 11 final Map<String, Object> param = new ParamMap<>(); 12 int i = 0; 13 // names key = 参数下标 value = @Param里面的值 14 for (Map.Entry<Integer, String> entry : names.entrySet()) { 15 // key = @Param里面的值,value = args[index] 真实数据 16 param.put(entry.getValue(), args[entry.getKey()]); 17 // 生成param1,参数 18 final String genericParamName = GENERIC_NAME_PREFIX + (i + 1); 19 // ensure not to overwrite parameter named with @Param 20 if (!names.containsValue(genericParamName)) { 21 param.put(genericParamName, args[entry.getKey()]); 22 } 23 i++; 24 } 25 return param; 26 } 27 }

我们直接说结论,如果方法签名中使用了@Param注解结论,则占位符中的参数名就是注解的值。如果没有注解在就是arg+参数的位置.

com.test.example04.MethodSignatureTest

参数类型方法签名参数值结果
解析单参数不带@ParamTUser queryUserByName(String name)methodSignature.convertArgsToSqlCommandParam(new Object[]{"孙悟空"})孙悟空
解析单参数带@ParamTUser queryUserById(@Param("userId") Long id)methodSignature.convertArgsToSqlCommandParam(new Object[]{1L}){userId=1, param1=1}
解析多参数不带@ParamTUser queryUserByTokenId(Long tokenId,String name)methodSignature.convertArgsToSqlCommandParam(new Object[]{1L, "孙悟空"}){arg0=1, arg1=孙悟空, param1=1, param2=孙悟空}
解析多参数带@ParamTUser queryUserByTokenId(@Param("tokenId") Long tokenId, @Param("name") String name)methodSignature.convertArgsToSqlCommandParam(new Object[]{1L, "孙悟空"}){tokenId=1, name=孙悟空, param1=1, param2=孙悟空}

如果项目编译中设置了编译后保存参数名,那么可以获取代码中编写的参数名。

好了到这里我们知道方法的参数最终都会被Mybatis重新解析,解析后的结果可以看以上的表格。主要就是为拼装参数提前准备数据。下面我们看sql信息最终是如何最终组装的吧。

2.1.2.2 方法参数组装

这里我们思考一下,变量符应该是动态sql,在调用jdbc时候应该是下面的例子。

java
1 PreparedStatement preparedStatement = connection.prepareStatement("select * from t_user where token_id = 0 or 1 = 1 and uid = 37");

那么我们就寻找哪里有这样的代码。

PreparedStatementHandler#instantiateStatement.

java
1@Override 2 protected Statement instantiateStatement(Connection connection) throws SQLException { 3 String sql = boundSql.getSql(); 4 if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) { 5 String[] keyColumnNames = mappedStatement.getKeyColumns(); 6 if (keyColumnNames == null) { 7 return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS); 8 } else { 9 return connection.prepareStatement(sql, keyColumnNames); 10 } 11 } else if (mappedStatement.getResultSetType() == ResultSetType.DEFAULT) { 12 return connection.prepareStatement(sql); 13 } else { 14 return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY); 15 } 16 }

关键的代码就在这里静态sql,直接从MappedStatement#getBoundSql(Object parameterObject)#getSql()获取组装后的代码。

java
1 @Override 2 public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException { 3 Statement stmt = null; 4 try { 5 Configuration configuration = ms.getConfiguration(); 6 StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql); 7 stmt = prepareStatement(handler, ms.getStatementLog()); 8 return handler.query(stmt, resultHandler); 9 } finally { 10 closeStatement(stmt); 11 } 12 } 13 14 // 这里parameterObject就是前面对方法参数的解析返回值。通过mappedStatement.getBoundSql(parameterObject)组装静态sql 15 protected PreparedStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { 16 this.configuration = mappedStatement.getConfiguration(); 17 this.executor = executor; 18 this.mappedStatement = mappedStatement; 19 this.rowBounds = rowBounds; 20 21 this.typeHandlerRegistry = configuration.getTypeHandlerRegistry(); 22 this.objectFactory = configuration.getObjectFactory(); 23 if (boundSql == null) { // issue #435, get the key before calculating the statement 24 generateKeys(parameterObject); 25 boundSql = mappedStatement.getBoundSql(parameterObject); 26 } 27 28 this.boundSql = boundSql; 29 this.parameterHandler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql); 30 this.resultSetHandler = configuration.newResultSetHandler(executor, mappedStatement, rowBounds, parameterHandler, resultHandler, boundSql); 31 }

好了,到这里我们就知道静态sql是哪里组装的了。关键点就在BoundSql这个类是如何构建的。我们以注解方式举例。

在构建MappedStatement的时候,MapperBuilderAssistant#parse会解析Mapper类所有的方法,获取方法上的注解,生成Sql的信息。 判断sql类型,如果是${}变量符,Sql资源就是DynamicSqlSource动态Sql。如果是#{}占位符就是RawSqlSource会将占位符替换成?,同时生成ParameterMapping信息 用于方法执行时候使用PreparedStatement去set参数信息。

下面我们以示例中的代码来看下BoundSql中究竟有什么信息。

那么对于第一种DynamicSqlSource动态sql,参数信息是如何组装的呢?

java
1public class DynamicSqlSource implements SqlSource { 2 3 private final Configuration configuration; 4 private final SqlNode rootSqlNode; 5 6 public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) { 7 this.configuration = configuration; 8 this.rootSqlNode = rootSqlNode; 9 } 10 11 @Override 12 public BoundSql getBoundSql(Object parameterObject) { 13 DynamicContext context = new DynamicContext(configuration, parameterObject); 14 // 处理sql中如果有<if><where><Trim>等自带标签的情况,同时处理将变量符提供换成真正的参数。 15 rootSqlNode.apply(context); 16 // 当执行完上面的流程变量符就被替换成真正的参数了。下面在看是否同时也包含了#{}占位符,如果包含就替换成? 17 // 在调换成?的同时新增一个ParameterMapping对象 18 SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration); 19 Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass(); 20 SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings()); 21 BoundSql boundSql = sqlSource.getBoundSql(parameterObject); 22 context.getBindings().forEach(boundSql::setAdditionalParameter); 23 return boundSql; 24 } 25 26}

核心的方法就是变量符替换,下面直接将核心的代码展示出来。

java
1 @Test 2 public void dynamicSql() throws Exception { 3 // 读取配置信息(为什么路径前不用加/,因为是相对路径。maven编译后的资源文件和class文件都是在一个包下,所以不用加/就是当前包目录) 4 InputStream mapperInputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("example05/mybatisConfig.xml"); 5 // 生成SqlSession工厂,SqlSession从名字上看就是,跟数据库交互的会话信息,负责将sql提交到数据库进行执行 6 SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(mapperInputStream, "development"); 7 // 获取Mybatis配置信息 8 Configuration configuration = sqlSessionFactory.getConfiguration(); 9 // 生成动态Sql 10 TextSqlNode textSqlNode = new TextSqlNode("select * from t_user where token_id = ${token_id} and uid = ${uid}"); 11 DynamicSqlSource dynamicSqlSource = new DynamicSqlSource(configuration, textSqlNode); 12 13 // 装参数 14 MapperMethod.ParamMap<Object> paramMap = new MapperMethod.ParamMap<Object>(); 15 paramMap.put("uid",37L); 16 paramMap.put("token_id","0 or 1 = 1"); 17 BoundSql boundSql = dynamicSqlSource.getBoundSql(paramMap); 18 System.out.println(boundSql.getSql()); 19 } 20 21 @Test 22 public void dynamicSql2(){ 23 // 读取配置信息(为什么路径前不用加/,因为是相对路径。maven编译后的资源文件和class文件都是在一个包下,所以不用加/就是当前包目录) 24 InputStream mapperInputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("example05/mybatisConfig.xml"); 25 // 生成SqlSession工厂,SqlSession从名字上看就是,跟数据库交互的会话信息,负责将sql提交到数据库进行执行 26 SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(mapperInputStream, "development"); 27 // 获取Mybatis配置信息 28 Configuration configuration = sqlSessionFactory.getConfiguration(); 29 30 // 装参数 31 MapperMethod.ParamMap<Object> paramMap = new MapperMethod.ParamMap<Object>(); 32 paramMap.put("uid",37L); 33 paramMap.put("token_id","0 or 1 = 1"); 34 DynamicContext context = new DynamicContext(configuration, paramMap); 35 36 // 生成动态Sql 37 TextSqlNode textSqlNode = new TextSqlNode("select * from t_user where token_id = ${token_id} and uid = ${uid}"); 38 textSqlNode.apply(context); 39 System.out.println(context.getSql()); 40 }

好了,我们知道动态sql其实就是${},变量符号替换。 下面我们看静态sql是如何处理占位符的吧。

前面我们说了静态sql,在初始化时候就会将占位符替换成? 同时生成一个ParameterMapping对象,然后在执行sql时候通过PreparedStatement进行set参数信息。 那么我们先看占位符如何替换成?的吧。实现逻辑其实就在RawSqlSource的构造方法中。

  • line(1-5) 在Mybatis初始化时候,会生成RawSqlSource。在构造中去调换占位符
  • line(8-19) 占位符替换的实现方式,最终生成StaticSqlSource
  • line(22-28) 占位符返回?的同时,生成一个ParameterMapping对象
java
1public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) { 2 SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration); 3 Class<?> clazz = parameterType == null ? Object.class : parameterType; 4 sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<>()); 5} 6 7// sql = select * from t_user where token_id = #{token_id} and uid = #{uid} 8public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) { 9 ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters); 10 // 对 11 GenericTokenParser parser = new GenericTokenParser("#{", "}", handler); 12 String sql; 13 if (configuration.isShrinkWhitespacesInSql()) { 14 sql = parser.parse(removeExtraWhitespaces(originalSql)); 15 } else { 16 sql = parser.parse(originalSql); 17 } 18 return new StaticSqlSource(configuration, sql, handler.getParameterMappings()); 19 } 20 21 // 会将占位符号#{token_id}替换成 ?同时生成一个ParameterMapping对象。 22 private static class ParameterMappingTokenHandler extends BaseBuilder implements TokenHandler { 23 // content = token_id 24 @Override 25 public String handleToken(String content) { 26 parameterMappings.add(buildParameterMapping(content)); 27 return "?"; 28 } 29 }

到这里占位符的解析已经很清楚了。BoundSql中的数据我们也知道了,我们直接看参数组装的逻辑吧。

  1. 从boundSql中获取占位符信息。
  2. 根据占位符获取参数信息
  3. 根据参数类型确定使用那个TypeHandler,如果都没有指定就用UnknownTypeHandler
  4. UnknownTypeHandler会根据参数的类型,从默认配置中找到要用的类型,如果是Long类型就是PreparedStatement#setLong,如果是String类型就是PreparedStatement#setString
java
1public class DefaultParameterHandler implements ParameterHandler { 2 @Override 3 public void setParameters(PreparedStatement ps) { 4 ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId()); 5 List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); 6 if (parameterMappings != null) { 7 for (int i = 0; i < parameterMappings.size(); i++) { 8 ParameterMapping parameterMapping = parameterMappings.get(i); 9 if (parameterMapping.getMode() != ParameterMode.OUT) { 10 Object value; 11 String propertyName = parameterMapping.getProperty(); 12 if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params 13 value = boundSql.getAdditionalParameter(propertyName); 14 } else if (parameterObject == null) { 15 value = null; 16 } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) { 17 value = parameterObject; 18 } else { 19 MetaObject metaObject = configuration.newMetaObject(parameterObject); 20 value = metaObject.getValue(propertyName); 21 } 22 TypeHandler typeHandler = parameterMapping.getTypeHandler(); 23 JdbcType jdbcType = parameterMapping.getJdbcType(); 24 if (value == null && jdbcType == null) { 25 jdbcType = configuration.getJdbcTypeForNull(); 26 } 27 try { 28 typeHandler.setParameter(ps, i + 1, value, jdbcType); 29 } catch (TypeException | SQLException e) { 30 throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e); 31 } 32 } 33 } 34 } 35 } 36 37} 38

好了到这里我们就搞清楚Mybatis中的参数是如何组装的了。 以及Jdbc是如何执行sql的了。 这部分内容比较复杂,仅仅通过看是看不明白的,建议根据文中的代码自己走一边。加深理解。

下面我们看Mybatis是如何处理返回值的吧。

2.2 Sql结果集是如何转换方法返回值的?

我们重新回到PreparedStatementHandler中跟数据库打交道的地方,当PreparedStatement#execute发送sql给数据库后,最终处理结果集的类是 ResultHandler,下面我们就围绕这个类做分析。

java
1 @Override 2 public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException { 3 PreparedStatement ps = (PreparedStatement) statement; 4 ps.execute(); 5 return resultSetHandler.handleResultSets(ps); 6 }

ResultSetHandler,我们看接口定义,处理结果集就在这里了。我们再来看实现。

java
1public interface ResultSetHandler { 2 3 <E> List<E> handleResultSets(Statement stmt) throws SQLException; 4 5 <E> Cursor<E> handleCursorResultSets(Statement stmt) throws SQLException; 6 7 void handleOutputParameters(CallableStatement cs) throws SQLException; 8 9}

默认的实现DefaultResultSetHandler。Mybatis实现较为复杂,我们一开始可能看不懂。我们先用原生的jdbc来自己实现一边。 然后脑子里有一个思路,然后在根据思路来看DefaultResultSetHandler的实现吧。

2.2.1 JDBC提供的结果处理API

思路是statement执行完后会返回结果集ResultSet。 结果集包含了返回的数据及这些数据对应的字段信息。 然后拿到这些字段信息分别从结果集中获取数据。下面的代码如果明白了,我们就去看Mybatis中的源码

java
1 @Test 2 public void resultMetaData() throws Exception { 3 String dbUrl = "jdbc:mysql://127.0.0.1:3306/test"; 4 String user = "root"; 5 String pass = "123456"; 6 // 1. 获取数据库连接 7 Connection connection = DriverManager.getConnection(dbUrl, user, pass); 8 Statement statement = connection.createStatement(); 9 // 2. 执行sql语句获取结果集 10 ResultSet resultSet = statement.executeQuery("select uid,name,token_id as tokenId from T_User"); 11 // 3. 从结果集中,获取数据库返回的数据列名 12 ResultSetMetaData metaData = resultSet.getMetaData(); 13 int columnCount = metaData.getColumnCount(); 14 // 所有的列名 15 List<String> columnNames = new ArrayList<>(); 16 // 列名对应的java类型 17 Map<String, Class<?>> column2JavaTypeAsMap = new HashMap<>(); 18 for (int i = 1; i <= columnCount; i++) { 19 System.out.println("字段:" + metaData.getColumnName(i) + "是否自增:" + metaData.isAutoIncrement(i)); 20 System.out.println("字段名:" + metaData.getColumnName(i)); 21 System.out.println("字段别名:" + metaData.getColumnLabel(i)); 22 System.out.println("MySql字段类型:" + metaData.getColumnTypeName(i)); 23 // Java 类的完全限定名称 24 System.out.println("Java字段类型:" + metaData.getColumnClassName(i)); 25 // 获取指定列的指定列大小。 26 System.out.println("字段长度:" + metaData.getPrecision(i)); 27 System.out.println("字段保留小数位:" + metaData.getScale(i)); 28 System.out.println("字段属于的表名:" + metaData.getTableName(i)); 29 System.out.println("是否可为空:" + metaData.isNullable(i)); 30 // 这里使用别名,如果没有别名的情况,别名跟字段名是一样的。 31 columnNames.add(metaData.getColumnLabel(i)); 32 column2JavaTypeAsMap.put(metaData.getColumnLabel(i), Class.forName(metaData.getColumnClassName(i))); 33 } 34 int row = 1; 35 while (resultSet.next()) { 36 System.out.println("----------第" + row + "行数据开始----------"); 37 for (String columnName : columnNames) { 38 Object columnValue = getValue(columnName, resultSet, column2JavaTypeAsMap); 39 System.out.println("列:" + columnName + ":value:" + columnValue); 40 } 41 System.out.println("----------第" + row + "行数据结束----------"); 42 row++; 43 } 44 resultSet.close(); 45 statement.close(); 46 connection.close(); 47 } 48 49 /** 50 * 根据不同的字段类型,调用不同的方法获取数据 51 * 52 * @param columnName 列名 53 * @param resultSet 集合集 54 * @param column2JavaTypeAsMap 字段对应的Java类型 55 * @return 结果值 56 * @throws Exception 未知异常 57 */ 58 public Object getValue(String columnName, ResultSet resultSet, Map<String, Class<?>> column2JavaTypeAsMap) throws Exception { 59 Class<?> column2JavaType = column2JavaTypeAsMap.get(columnName); 60 Object value = null; 61 if (column2JavaType.equals(Integer.class)) { 62 value = resultSet.getInt(columnName); 63 } else if (column2JavaType.equals(String.class)) { 64 value = resultSet.getString(columnName); 65 } 66 return value; 67 } 68 69字段:uid是否自增:true 70字段名:uid 71字段别名:uid 72MySql字段类型:INT 73Java字段类型:java.lang.Integer 74字段长度:11 75字段保留小数位:0 76字段属于的表名:t_user 77是否可为空:0 78字段:name是否自增:false 79字段名:name 80字段别名:name 81MySql字段类型:CHAR 82Java字段类型:java.lang.String 83字段长度:32 84字段保留小数位:0 85字段属于的表名:t_user 86是否可为空:1 87字段:token_id是否自增:false 88字段名:token_id 89字段别名:tokenId 90MySql字段类型:CHAR 91Java字段类型:java.lang.String 92字段长度:64 93字段保留小数位:0 94字段属于的表名:t_user 95是否可为空:0 96----------1行数据开始---------- 97:uid:value:37 98:name:value:无天 99:tokenId:value:60 100----------1行数据结束---------- 101----------2行数据开始---------- 102:uid:value:9846 103:name:value:斗战胜佛 104:tokenId:value:80 105----------2行数据结束---------- 106----------3行数据开始---------- 107:uid:value:9847 108:name:value:净坛使者 109:tokenId:value:90 110----------3行数据结束---------- 111----------4行数据开始---------- 112:uid:value:9848 113:name:value:无量功德佛祖 114:tokenId:value:100 115----------4行数据结束----------

ResultSetMetaData 方法是比较重要的,这里把他常用的api方法及解释以表格形式列举一下。 当我们拿到返回的列名,就可以直接根据列名来返回数据了。

方法含义示例
ResultSetMetaData#getColumnName获取数据库字段名name
ResultSetMetaData#getColumnLabel查询语句中字段别名,如果没有保持跟字段名一致user_id as userId,这里就是userId
ResultSetMetaData#getColumnTypeName返回Sql字段类型INT、CHAR
ResultSetMetaData#getColumnClassName返回Java字段类型的完整限定名java.lang.String、java.lang.Integer
ResultSetMetaData#getPrecision获取定义的字段长度int(11),返回11
ResultSetMetaData#getScale获取字段定义的保留小数位-
ResultSetMetaData#getTableName字段对应的表-
ResultSetMetaData#isNullable字段是否可以为空-
ResultSetMetaData#isAutoIncrement是否数据库自增字段-
ResultSetMetaData#isAutoIncrement是否数据库自增字段-

2.2.2 Mybatis获取结果集

思考下结果集可能是什么?

  1. 场景一: 可能返回的是List
java
1 @Select("select * from t_user") 2 List<TUser> queryAllUsers();
  1. 场景二: 可能返回的是单个对象
java
1 @Select("select * from t_user where uid = #{uid}") 2 TUser queryUserByPlaceholderId(@Param("uid") Long uid);
  1. 场景三: 更新语句返回结果集是条数。
java
1 @Update("update t_user set name = #{name}") 2 int updateName(@Param("uid") Long uid, @Param("name") String name);
  1. 场景四: 更新语句返回boolean
java
1 @Update("update t_user set name = #{name} where uid = #{uid}") 2 boolean updateNameById(@Param("uid") Long uid, @Param("name") String name);

分别来分析。

场景一:

java
1public class MapperMethod { 2 private final MethodSignature method; 3 public Object execute(SqlSession sqlSession, Object[] args) { 4 Object result; 5 switch (command.getType()) { 6 case SELECT: 7 if (method.returnsVoid() && method.hasResultHandler()) { 8 executeWithResultHandler(sqlSession, args); 9 result = null; 10 } else if (method.returnsMany()) { 11 result = executeForMany(sqlSession, args); 12 } else if (method.returnsMap()) { 13 result = executeForMap(sqlSession, args); 14 } else if (method.returnsCursor()) { 15 result = executeForCursor(sqlSession, args); 16 } else { 17 Object param = method.convertArgsToSqlCommandParam(args); 18 result = sqlSession.selectOne(command.getName(), param); 19 if (method.returnsOptional() 20 && (result == null || !method.getReturnType().equals(result.getClass()))) { 21 result = Optional.ofNullable(result); 22 } 23 } 24 break; 25 case FLUSH: 26 result = sqlSession.flushStatements(); 27 break; 28 default: 29 throw new BindingException("Unknown execution method for: " + command.getName()); 30 } 31 if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) { 32 throw new BindingException("Mapper method '" + command.getName() 33 + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ")."); 34 } 35 return result; 36 } 37}

可以看到这里对于方法的返回值判断是根据MethodSignature,MethodSignature不仅提供了对参数的解析,同时也是对方法的分析。 包括判断方法的返回值,我们看它的内部属性。

java
1 public static class MethodSignature { 2 // 是否返回集合 3 private final boolean returnsMany; 4 // 是否返回是map结构 5 private final boolean returnsMap; 6 // 是否没有返回值 7 private final boolean returnsVoid; 8 // 是否返回的是游标 9 private final boolean returnsCursor; 10 // 是否返回的是Optional对象 11 private final boolean returnsOptional; 12 // 返回值类型 13 private final Class<?> returnType; 14 // 返回map结构使用的key字段 15 private final String mapKey; 16 // 如果入参是ResultHandler 记录器下标 17 private final Integer resultHandlerIndex; 18 // 如果参数是RowBounds,记录其下标 19 private final Integer rowBoundsIndex; 20 // 参数处理 21 private final ParamNameResolver paramNameResolver; 22 23 }

如果发现是返回List。则MethodSignature#returnsMany=true。直接调用SqlSession#selectList

java
1private <E> Object executeForMany(SqlSession sqlSession, Object[] args) { 2 List<E> result; 3 Object param = method.convertArgsToSqlCommandParam(args); 4 // 方法中是否包含逻辑分页参数RowBounds 5 if (method.hasRowBounds()) { 6 // 如果有就获取逻辑分页参数 7 RowBounds rowBounds = method.extractRowBounds(args); 8 // 执行sql 9 result = sqlSession.selectList(command.getName(), param, rowBounds); 10 } else { 11 result = sqlSession.selectList(command.getName(), param); 12 } 13 // issue #510 Collections & arrays support 14 if (!method.getReturnType().isAssignableFrom(result.getClass())) { 15 if (method.getReturnType().isArray()) { 16 return convertToArray(result); 17 } else { 18 return convertToDeclaredCollection(sqlSession.getConfiguration(), result); 19 } 20 } 21 return result; 22 }

最终在DefaultResultSetHandler#handleResultSets处理返回值。下面的代码看了先不要害怕,其实 思路跟我们用jdbc来处理是一样的。第一要拿到返回的数据信息。第二要将返回的数据信息包装成方法的返回值。 只不过Mybatis将上面的两个能力,都提供成了对应的接口。其中数据的返回集就是ResultSetWrapper,从返回集中获取数据是TypeHandler。 而将数据库返回的行数据,转换成方法的返回值就要用到ResultMap。

java
1 @Override 2 public List<Object> handleResultSets(Statement stmt) throws SQLException { 3 ErrorContext.instance().activity("handling results").object(mappedStatement.getId()); 4 5 final List<Object> multipleResults = new ArrayList<>(); 6 7 int resultSetCount = 0; 8 // 读取返回的数据信息(jdbcType,javaType,列名和别名) 9 ResultSetWrapper rsw = getFirstResultSet(stmt); 10 // Mapper签名中找到返回集应该信息 11 List<ResultMap> resultMaps = mappedStatement.getResultMaps(); 12 int resultMapCount = resultMaps.size(); 13 // 做个校验,如果sql执行后没有任何返回信息,但是Mapper签名中却指定了返回映射信息。则会报错告警 A query was run and no Result Maps were found for the Mapped Statement 14 validateResultMapsCount(rsw, resultMapCount); 15 while (rsw != null && resultMapCount > resultSetCount) { 16 ResultMap resultMap = resultMaps.get(resultSetCount); 17 // 处理返回集 18 handleResultSet(rsw, resultMap, multipleResults, null); 19 rsw = getNextResultSet(stmt); 20 cleanUpAfterHandlingResultSet(); 21 resultSetCount++; 22 } 23 24 String[] resultSets = mappedStatement.getResultSets(); 25 if (resultSets != null) { 26 while (rsw != null && resultSetCount < resultSets.length) { 27 ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]); 28 if (parentMapping != null) { 29 String nestedResultMapId = parentMapping.getNestedResultMapId(); 30 ResultMap resultMap = configuration.getResultMap(nestedResultMapId); 31 handleResultSet(rsw, resultMap, null, parentMapping); 32 } 33 rsw = getNextResultSet(stmt); 34 cleanUpAfterHandlingResultSet(); 35 resultSetCount++; 36 } 37 } 38 39 return collapseSingleResultList(multipleResults); 40 }

下面我们看这几个关键类。ResultSetWrapper。这个的源码是不是有点想我们前面自己写的原生jdbc的方法了? 拿到返回的列名和对应的java类型。

java
1 public ResultSetWrapper(ResultSet rs, Configuration configuration) throws SQLException { 2 super(); 3 this.typeHandlerRegistry = configuration.getTypeHandlerRegistry(); 4 this.resultSet = rs; 5 final ResultSetMetaData metaData = rs.getMetaData(); 6 final int columnCount = metaData.getColumnCount(); 7 for (int i = 1; i <= columnCount; i++) { 8 columnNames.add(configuration.isUseColumnLabel() ? metaData.getColumnLabel(i) : metaData.getColumnName(i)); 9 jdbcTypes.add(JdbcType.forCode(metaData.getColumnType(i))); 10 classNames.add(metaData.getColumnClassName(i)); 11 } 12 }

TypeHandler 是从jdbc中获取数据的接口,这个功能就跟前面我们用原生API实现时候的getValue方法类似。 主要是根据数据的类型,来确定是调用ResultSet#getString还是调用ResultSet#getInt等方法。

java
1public interface TypeHandler<T> { 2 3 void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException; 4 5 T getResult(ResultSet rs, String columnName) throws SQLException; 6 7 T getResult(CallableStatement cs, int columnIndex) throws SQLException; 8}

ResultMap 是返回数据对应的Java对象。会在生成MappedStatement时候构建完成。如果是在xml中定义了就是 <resultMap/> 标签,如果没有就是 根据返回类自动生成一个resultMap。可以看到这个类属性其实跟他的标签是一样的。

java
1public class ResultMap { 2 private Configuration configuration; 3 4 // 如果配置了<resultMap id="BaseResultMap" ,就是类全路径名+BaseResultMap。如果没有就是类名加方法名+Inline 5 private String id; 6 private Class<?> type; 7 private List<ResultMapping> resultMappings; 8 private List<ResultMapping> idResultMappings; 9 private List<ResultMapping> constructorResultMappings; 10 private List<ResultMapping> propertyResultMappings; 11 private Set<String> mappedColumns; 12 private Set<String> mappedProperties; 13 private Discriminator discriminator; 14 private boolean hasNestedResultMaps; 15 private boolean hasNestedQueries; 16 private Boolean autoMapping; 17}

ResultMap的标签功能比较强大,我们深入研究下。举一个例子。

java
1/** 2 * 一个学校,一个校长,多个学生 3 * name,headMaster(id,name),users() 4 * 2022/4/10 22:07 5 */ 6@Data 7public class School { 8 9 private Long id; 10 11 private String name; 12 13 private SchoolHeadMaster schoolHeadMaster; 14 15 private List<Student> students; 16 17} 18@Data 19public class SchoolHeadMaster { 20 21 private Long id; 22 23 private String name; 24} 25 26@Data 27public class Student { 28 29 private Long id; 30 31 private String name; 32}

配置文件如下

xml
1<mapper namespace="orm.example.dal.mapper.SchoolMapper"> 2 <resultMap id="BaseResultMap" type="orm.example.dal.model.TUser"> 3 <id column="token_id" jdbcType="CHAR" property="tokenId"/> 4 <result column="uid" jdbcType="INTEGER" property="uid"/> 5 <result column="name" jdbcType="CHAR" property="name"/> 6 </resultMap> 7 8 <resultMap id="schoolResultMap" type="orm.example.dal.model.School"> 9 <result column="schoolId" jdbcType="CHAR" property="id"/> 10 <result column="schoolName" jdbcType="CHAR" property="name"/> 11 <!-- 学校校长跟学校关系1对1--> 12 <association property="schoolHeadMaster" javaType="orm.example.dal.model.SchoolHeadMaster"> 13 <id column="hmId" property="id"/> 14 <result column="schoolHeadName" jdbcType="CHAR" property="name"/> 15 </association> 16 <!-- 学生关系是1对n--> 17 <collection property="students" javaType="list" ofType="orm.example.dal.model.Student"> 18 <id column="studentId" property="id"/> 19 <result column="studentName" jdbcType="CHAR" property="name"/> 20 </collection> 21 </resultMap> 22 23 24 <select id="selectSchool" resultMap="schoolResultMap"> 25 select school.id as 'schoolId', school.name as 'schoolName', hm.id as 'hmId', hm.name as 'schoolHeadName', s.name as 'studentName', s.id as 'studentId' 26 from school 27 left join head_master hm on hm.id = school.head_master_id 28 left join student s on school.id = s.school_id 29 </select> 30</mapper>

执行数据验证 com.test.example05.ResultMapTest#parseResultMap

  • line(11-22) 获取MappedStatement观察复杂对象ResultMap是什么样。

  • line(25-26) 观察mybatis如何填充数据。
java
1 @Test 2 public void parseResultMap() { 3 // 读取配置信息(为什么路径前不用加/,因为是相对路径。maven编译后的资源文件和class文件都是在一个包下,所以不用加/就是当前包目录) 4 InputStream mapperInputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("example05/mybatisConfig-ResultMap.xml"); 5 // 生成SqlSession工厂,SqlSession从名字上看就是,跟数据库交互的会话信息,负责将sql提交到数据库进行执行 6 SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(mapperInputStream, "development"); 7 // 获取Mybatis配置信息 8 Configuration configuration = sqlSessionFactory.getConfiguration(); 9 10 // 只要看这个复杂对象如何映射。 11 MappedStatement selectSchool = configuration.getMappedStatement("orm.example.dal.mapper.SchoolMapper.selectSchool"); 12 ResultMap resultMap = selectSchool.getResultMaps().get(0); 13 // 确定是一个复杂对象,规则是XMLMapperBuilder#processNestedResultMappings,只要发现查询语句对象的结果中有以下标签"association", "collection", "case"。就是复杂sql 14 System.out.println("是否复杂对象:" + resultMap.hasNestedResultMaps()); 15 List<ResultMapping> propertyResultMappings = resultMap.getPropertyResultMappings(); 16 for (ResultMapping propertyResultMapping : propertyResultMappings) { 17 // 1. 属性:id,db列名:schoolId,JavaType:class java.lang.Long 18 // 2. 属性:name,db列名:schoolName,JavaType:class java.lang.String 19 // 3. 属性:schoolHeadMaster,db列名:null,JavaType:class orm.example.dal.model.SchoolHeadMaster,映射NestedResultMapId 20 // 4. 属性:students,db列名:null,JavaType:interface java.util.List,映射NestedResultMapId 21 printResultMapping(propertyResultMapping, configuration); 22 } 23 24 // [School(id=1, name=西天小学, schoolHeadMaster=SchoolHeadMaster(id=1, name=如来), students=[Student(id=1, name=孙悟空), Student(id=2, name=猪八戒), Student(id=3, name=唐三藏)])] 25 List<School> schools = configuration.getMapper(SchoolMapper.class, sqlSessionFactory.openSession(false)).selectSchool(); 26 System.out.println(schools); 27 } 28 29 private static void printResultMapping(ResultMapping propertyResultMapping, Configuration configuration) { 30 String property = propertyResultMapping.getProperty(); 31 System.out.println("属性:" + property + ",db列名:" + propertyResultMapping.getColumn() + ",JavaType:" + propertyResultMapping.getJavaType() + ",映射NestedResultMapId:" + propertyResultMapping.getNestedResultMapId()); 32 String nestedResultMapId = propertyResultMapping.getNestedResultMapId(); 33 // 如果不等于空,说明是复杂对象。从配置文件中获取复杂属性的映射集合 34 if (Objects.nonNull(nestedResultMapId)) { 35 ResultMap nestedResultMap = configuration.getResultMap(nestedResultMapId); 36 System.out.println(nestedResultMap.getType()); 37 System.out.println("是否复杂对象:" + nestedResultMap.hasNestedResultMaps()); 38 List<ResultMapping> propertyResultMappings = nestedResultMap.getPropertyResultMappings(); 39 for (ResultMapping resultMapping : propertyResultMappings) { 40 printResultMapping(resultMapping, configuration); 41 } 42 } 43 }

下面我们就看如何填充数据了。同样我们直接手撸代码。

schoolIdschoolNamehmIdschoolHeadNamestudentNamestudentId
1西天小学1如来孙悟空1
1西天小学1如来猪八戒2
1西天小学1如来唐三藏3
2湖畔大学2马云马化腾4
2湖畔大学2马云谢霆锋5
2湖畔大学2马云张学友6

Mybatis中处理返回值,分一下基础。简单对象和复杂对象这里我们直接用复杂对象距离。 可以看到School中有2个基本属性和1个对象属性还有一个集合属性。

看这个图。

这部分示例代码在 com.test.example05.ResultMapTest#handlerResultSet

  • line(26) 首先我们要获取数据库返回列信息
  • line(30) 一行一行读取数据,每次执行ResultSet#next就是下一行
  • line(41) 因为我们School中有一个是集合属性,需要将多行数据转换成一行。此时我们执行完getRowValue 会生成一个数据。但是这个数据不能直接就用, 还需要将第二行的数据也赋值到第一行的返回值中,这是我们就将 第一行的数据返回值,带进去。
  • line(41) 我们如何知道这6行数据如何合并。规则: 简单对象进行拼接,School中简单对象是id,和name。
  • line(44) getRowValue中的每个方法都要注意看
  • line(93-99) 主要处理是否需要合并行,合并行的时候直接填充数据接口。而不是合并则缓存中查不到数据,就重新生成一个结果。
  • line(104) 判断ResultMap是否是一个复杂对象,这里School是一个复杂对象,因为不仅有一个HeadMaster还有一个List的学生集合。
  • line(109) 第一次进去这里会有4个对象,id,name,schoolHeadMaster,students
  • line(115-118) 对于School中的id和name都会在这几行被执行了。可以看到根据javaType找到了TypeHandler,然后TypeHandler负责取值。
  • line(141) 对于schoolHeadMaster这个属性,是复杂对象,School中的Java类型是SchoolHeadMaster和他对应的ResultMap中的类型是一样的, 则递归去获取数据,因为SchoolHeadMaster中也是都简单类型的id和name,所以最终也会在line(115-118)被执行了。
  • line(125-138) School中的students,java类型是List,ResultMap中类型是Student,所以要先从第一行的数据去获取这个属性 看List是否被实例化了,如果没有就实例化。然后执行add操作给list中数据追加值。

主要这里我们使用了MetaObject这个工具,是一个包装方法。不详细介绍了,如果还不清楚请跳转

第03篇:Mybatis核心类详细介绍

java
1 @Test 2 public void handlerResultSet() throws Exception { 3 4 // 读取配置信息(为什么路径前不用加/,因为是相对路径。maven编译后的资源文件和class文件都是在一个包下,所以不用加/就是当前包目录) 5 InputStream mapperInputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("example05/mybatisConfig-ResultMap.xml"); 6 // 生成SqlSession工厂,SqlSession从名字上看就是,跟数据库交互的会话信息,负责将sql提交到数据库进行执行 7 SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(mapperInputStream, "development"); 8 // 获取Mybatis配置信息 9 Configuration configuration = sqlSessionFactory.getConfiguration(); 10 11 // 只要看这个复杂对象如何映射。 12 MappedStatement selectSchool = configuration.getMappedStatement("orm.example.dal.mapper.SchoolMapper.selectSchool"); 13 ResultMap resultMap = selectSchool.getResultMaps().get(0); 14 15 PreparedStatement preparedStatement = execute("select school.id as 'schoolId', 16" + 17 " school.name as 'schoolName', 18" + 19 " hm.id as 'hmId', 20" + 21 " hm.name as 'schoolHeadName', 22" + 23 " s.name as 'studentName', 24" + 25 " s.id as 'studentId' 26" + 27 "from school 28" + 29 " left join head_master hm on hm.id = school.head_master_id 30" + 31 " left join student s on school.id = s.school_id"); 32 // 2. 执行sql语句获取结果集 33 preparedStatement.execute(); 34 ResultSetWrapper firstResultSet = getFirstResultSet(preparedStatement, configuration); 35 ResultSet resultSet = firstResultSet.getResultSet(); 36 Map<String, Object> one2ManyAsMap = new HashMap<>(); 37 // 3. 处理结果转换,一行一行读取数据 38 while (resultSet.next()) { 39 // 3.1 用于判断多行数据是否要合并 规则: 简单对象属性,如果一样则可以合并。 40 // 如: 下面数据返回值是 List<School> schools;School(Long id,String name,SchoolHeadMaster schoolHeadMaster,List<Student> students) 41 //INSERT INTO MY_TABLE(schoolId, schoolName, hmId, schoolHeadName, studentName, studentId) VALUES (1, '西天小学', 1, '如来', '孙悟空', 1); 42 //INSERT INTO MY_TABLE(schoolId, schoolName, hmId, schoolHeadName, studentName, studentId) VALUES (1, '西天小学', 1, '如来', '猪八戒', 2); 43 //INSERT INTO MY_TABLE(schoolId, schoolName, hmId, schoolHeadName, studentName, studentId) VALUES (1, '西天小学', 1, '如来', '唐三藏', 3); 44 //INSERT INTO MY_TABLE(schoolId, schoolName, hmId, schoolHeadName, studentName, studentId) VALUES (2, '湖畔大学', 2, '马云', '马化腾', 4); 45 //INSERT INTO MY_TABLE(schoolId, schoolName, hmId, schoolHeadName, studentName, studentId) VALUES (2, '湖畔大学', 2, '马云', '谢霆锋', 5); 46 //INSERT INTO MY_TABLE(schoolId, schoolName, hmId, schoolHeadName, studentName, studentId) VALUES (2, '湖畔大学', 2, '马云', '张学友', 6); 47 // 我们如何知道这6行数据如何合并。规则: 简单对象进行拼接,School中简单对象是id,和name。 48 // 所以这里构建的缓存key就是 id + name。相同就不新建返回值,而是对返回值二次赋值 49 String cacheKey = getCacheKey(resultMap, resultSet, configuration); 50 Object parentObject = one2ManyAsMap.get(cacheKey); 51 // 3.2 开始填充数据 52 parentObject = getRowValue(resultMap, firstResultSet, configuration, parentObject); 53 one2ManyAsMap.put(cacheKey, parentObject); 54 } 55 for (Object value : one2ManyAsMap.values()) { 56 System.out.println(value); 57 } 58 } 59 60 private PreparedStatement execute(String sql) throws Exception { 61 String dbUrl = "jdbc:mysql://127.0.0.1:3306/test"; 62 String user = "root"; 63 String pass = "123456"; 64 // 1. 获取数据库连接 65 Connection connection = DriverManager.getConnection(dbUrl, user, pass); 66 return connection.prepareStatement(sql); 67 } 68 69 private ResultSetWrapper getFirstResultSet(Statement stmt, Configuration configuration) throws SQLException { 70 ResultSet rs = stmt.getResultSet(); 71 while (rs == null) { 72 if (stmt.getMoreResults()) { 73 rs = stmt.getResultSet(); 74 } else { 75 if (stmt.getUpdateCount() == -1) { 76 break; 77 } 78 } 79 } 80 return rs != null ? new ResultSetWrapper(rs, configuration) : null; 81 } 82 83 private static String getCacheKey(ResultMap resultMap, ResultSet resultSet, Configuration configuration) throws Exception { 84 StringBuffer sb = new StringBuffer(); 85 sb.append(resultMap.getId()); 86 List<ResultMapping> propertyResultMappings = resultMap.getPropertyResultMappings(); 87 for (ResultMapping propertyResultMapping : propertyResultMappings) { 88 if (propertyResultMapping.isSimple()) { 89 Class<?> javaType = propertyResultMapping.getJavaType(); 90 TypeHandler<?> typeHandler = configuration.getTypeHandlerRegistry().getTypeHandler(javaType); 91 sb.append(propertyResultMapping.getProperty()); 92 Object propertyValue = typeHandler.getResult(resultSet, propertyResultMapping.getColumn()); 93 sb.append(propertyValue); 94 } 95 } 96 return sb.toString(); 97 } 98 99 private static Object getRowValue(ResultMap resultMap, ResultSetWrapper firstResultSet, Configuration configuration, Object rowValue) throws Exception { 100 // 获取返回值的实体类 101 Object returnValue = null; 102 // 如果不等于空说明是处理合并,那么不构建新对象,只在合并的对象上重新赋值。 103 if (Objects.nonNull(rowValue)) { 104 returnValue = rowValue; 105 } else { 106 // 等于空说明是第一次进入,直接构建返回值示例。 107 returnValue = configuration.getObjectFactory().create(resultMap.getType()); 108 } 109 // 下面对实例方法进行赋值,利用工具类MetaObject包装提供统一的赋属性方法 110 MetaObject metaObject = configuration.newMetaObject(returnValue); 111 // 判断是否是嵌套对象 112 boolean nestedFlag = resultMap.hasNestedResultMaps(); 113 ResultSet resultSet = firstResultSet.getResultSet(); 114 // 判断是否简单对象 115 if (nestedFlag) { 116 // 非简单对象,说明需要判断属性各自需要的映射对象 117 List<ResultMapping> propertyResultMappings = resultMap.getPropertyResultMappings(); 118 for (ResultMapping propertyResultMapping : propertyResultMappings) { 119 Class<?> javaType = propertyResultMapping.getJavaType(); 120 String nestedResultMapId = propertyResultMapping.getNestedResultMapId(); 121 Object propertyValue; 122 // 是空说明,当前属性是基本属性 123 if (Objects.isNull(nestedResultMapId)) { 124 // 获取当前属性的Java类型,从配置中获取该类型,读取ResultSet要使用的方法。eg:StringTypeHandler 使用ResultSet#getString 125 TypeHandler<?> typeHandler = configuration.getTypeHandlerRegistry().getTypeHandler(javaType); 126 propertyValue = typeHandler.getResult(resultSet, propertyResultMapping.getColumn()); 127 } else { 128 // 不等于空说明是嵌套对象,从配置中读取嵌套对象的映射信息 129 ResultMap nestedResultMap = configuration.getResultMap(nestedResultMapId); 130 // 嵌套对象的java类型。eg: School(students),这里的Java类型就是Student 131 Class<?> nestedJavaType = nestedResultMap.getType(); 132 // 若果是list方式,外面的javaType=list,里面是真实java对象 133 if (!javaType.equals(nestedJavaType) && Collection.class.isAssignableFrom(javaType)) { 134 propertyValue = getRowValue(nestedResultMap, firstResultSet, configuration, null); 135 MetaObject parentMetaObject = configuration.newMetaObject(returnValue); 136 // 获取父对象School 获取students的List 137 Object collect = parentMetaObject.getValue(propertyResultMapping.getProperty()); 138 if (Objects.isNull(collect)) { 139 // 如果是null,则将list实例化 140 collect = configuration.getObjectFactory().create(javaType); 141 parentMetaObject.setValue(propertyResultMapping.getProperty(), collect); 142 } 143 // 给list中添加信息 144 MetaObject metaCollectObject = configuration.newMetaObject(collect); 145 metaCollectObject.add(propertyValue); 146 propertyValue = collect; 147 } else { 148 // 简单对象 149 propertyValue = getRowValue(nestedResultMap, firstResultSet, configuration, null); 150 } 151 } 152 metaObject.setValue(propertyResultMapping.getProperty(), propertyValue); 153 } 154 } else { 155 List<ResultMapping> propertyResultMappings = resultMap.getPropertyResultMappings(); 156 for (ResultMapping propertyResultMapping : propertyResultMappings) { 157 Class<?> javaType = propertyResultMapping.getJavaType(); 158 TypeHandler<?> typeHandler = configuration.getTypeHandlerRegistry().getTypeHandler(javaType); 159 Object propertyValue = typeHandler.getResult(resultSet, propertyResultMapping.getColumn()); 160 metaObject.setValue(propertyResultMapping.getProperty(), propertyValue); 161 } 162 } 163 return returnValue; 164 }

好了到这里对于场景1中,返回list中的数据就处理好了。

java
1 private <E> Object executeForMany(SqlSession sqlSession, Object[] args) { 2 List<E> result; 3 Object param = method.convertArgsToSqlCommandParam(args); 4 if (method.hasRowBounds()) { 5 RowBounds rowBounds = method.extractRowBounds(args); 6 result = sqlSession.selectList(command.getName(), param, rowBounds); 7 } else { 8 result = sqlSession.selectList(command.getName(), param); 9 } 10 // issue #510 Collections & arrays support 11 if (!method.getReturnType().isAssignableFrom(result.getClass())) { 12 if (method.getReturnType().isArray()) { 13 return convertToArray(result); 14 } else { 15 return convertToDeclaredCollection(sqlSession.getConfiguration(), result); 16 } 17 } 18 return result; 19 }

场景二:

如果是单个对象,在基于场景一的返回值上加一个判断,如果结果只要1个就只取第一个。 如果是多个,则报错。

java
1 public <T> T selectOne(String statement, Object parameter) { 2 // Popular vote was to return null on 0 results and throw exception on too many. 3 List<T> list = this.selectList(statement, parameter); 4 if (list.size() == 1) { 5 return list.get(0); 6 } else if (list.size() > 1) { 7 throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size()); 8 } else { 9 return null; 10 } 11 }

场景三:

更新语句直接 Statement#getUpdateCount 获取更新数量

java
1 public int update(Statement statement) throws SQLException { 2 PreparedStatement ps = (PreparedStatement) statement; 3 ps.execute(); 4 int rows = ps.getUpdateCount(); 5 Object parameterObject = boundSql.getParameterObject(); 6 KeyGenerator keyGenerator = mappedStatement.getKeyGenerator(); 7 keyGenerator.processAfter(executor, mappedStatement, ps, parameterObject); 8 return rows; 9 }

场景四:

排除查询,其他语句返回都是int类型的更新成数量。那么假如方法是boolean类型,或者Long和Void呢

java
1public class MapperMethod { 2 private Object rowCountResult(int rowCount) { 3 final Object result; 4 if (method.returnsVoid()) { 5 result = null; 6 } else if (Integer.class.equals(method.getReturnType()) || Integer.TYPE.equals(method.getReturnType())) { 7 result = rowCount; 8 } else if (Long.class.equals(method.getReturnType()) || Long.TYPE.equals(method.getReturnType())) { 9 result = (long) rowCount; 10 } else if (Boolean.class.equals(method.getReturnType()) || Boolean.TYPE.equals(method.getReturnType())) { 11 result = rowCount > 0; 12 } else { 13 throw new BindingException("Mapper method '" + command.getName() + "' has an unsupported return type: " + method.getReturnType()); 14 } 15 return result; 16 } 17}