第05篇:Mybatis的SQL執(zhí)行流程分析


breadcrumb: false
navbar: true
sidebar: true
pageInfo: true
contributor: true
editLink: true
updateTime: true
prev: true
next: true
comment: true
footer: true
password: 111
backtotop: true
title: 第05篇:Mybatis的SQL執(zhí)行流程分析
category: Mybatis


<PageBanner/>

一、前言

前面我們知道了Mybatis是如何進(jìn)行代理的, 但是最終 PlainMethodInvoker 中是如何將參數(shù)轉(zhuǎn)組裝成Sql,并執(zhí)行處理Sql返回值的地方還都沒(méi)看到。本篇我們就帶著如下三個(gè)問(wèn)題開(kāi)始我們的探索吧。

本篇內(nèi)容因?yàn)樯婕案鷍dbc的知識(shí),如果對(duì)這部分內(nèi)容有點(diǎn)遺忘,請(qǐng)先JDBC知識(shí)復(fù)習(xí),另本篇內(nèi)容知識(shí)點(diǎn)較多,目錄較復(fù)雜,建議根據(jù)文字結(jié)合
代碼在實(shí)踐的過(guò)程中一起學(xué)習(xí)。最好也可以自己debug一下。會(huì)收獲更大。做好準(zhǔn)備現(xiàn)在發(fā)車(chē)。

二、流程分析

2.1 Sql是如何組裝參數(shù)的?

在組裝參數(shù)之前我們先來(lái)提一個(gè)小問(wèn)題,sql的類(lèi)型是如何判斷的。sql類(lèi)型有增刪該查。
除了查詢(xún)會(huì)有結(jié)果集外,其他三種都是返回更新行數(shù)。他們對(duì)應(yīng)的處理邏輯也是不一樣的。
我們要先弄清這個(gè)問(wèn)題。

2.1.1 sql類(lèi)型如何判斷?

我們知道sql的類(lèi)型是可以通過(guò)關(guān)鍵字來(lái)判斷的,如select/update/delete/insert。那么在Mybatis中哪里能輸入sql呢?
一種有2種方式。

  1. 在Mapper.xml中直接編寫(xiě)sql,如下示例。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="orm.example.dal.mapper.TUserMapper">
    <delete id="deleteByPrimaryKey" parameterType="java.lang.String">
        delete from T_USER
        where token_id = #{tokenId,jdbcType=CHAR}
    </delete>
    <insert id="insert" parameterType="orm.example.dal.model.TUser">
        insert into T_USER (token_id, uid, name)
        values (#{tokenId,jdbcType=CHAR}, #{uid,jdbcType=INTEGER}, #{name,jdbcType=CHAR})
    </insert>
    <update id="updateByPrimaryKey" parameterType="orm.example.dal.model.TUser">
        update T_USER
        set uid = #{uid,jdbcType=INTEGER},
        name = #{name,jdbcType=CHAR}
        where token_id = #{tokenId,jdbcType=CHAR}
    </update>
    <select id="selectAll" resultMap="BaseResultMap">
        select token_id, uid, name
        from T_USER
    </select>

</mapper>
  1. 在Mapper類(lèi)中使用注解編寫(xiě)sql
public interface TUserMapper {
    @Select("select * from t_user where id = #{id}")
    TUser selectById(Long id);
}    

這些sql信息都保存在 MappedStatement。在PlainMethodInvoker通過(guò)SqlCommand進(jìn)行調(diào)用。

  • line(9) 最終通過(guò)type = ms.getSqlCommandType() 獲取sql的類(lèi)型
SqlCommand sqlCommand = new SqlCommand(config, mapperInterface, method);

// 構(gòu)造參數(shù)中找MappedStatement
public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
      final String methodName = method.getName();
      final Class<?> declaringClass = method.getDeclaringClass();
      MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass,
          configuration);
      type = ms.getSqlCommandType();
}          
// 尋找方法是接口全路徑名.方法名
private MappedStatement resolveMappedStatement(){
    String statementId = mapperInterface.getName() + "." + methodName;
    configuration.hasStatement(statementId)
}

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

2.1.1.1 xml文件方式

解析xml標(biāo)簽來(lái)實(shí)現(xiàn)

XMLMapperBuilder#parseStatementNode

  • line(11) 通過(guò)標(biāo)簽來(lái)映射成指定的類(lèi)型SqlCommandType
public class XMLStatementBuilder extends BaseBuilder {
 public void parseStatementNode() {
    String id = context.getStringAttribute("id");
    String databaseId = context.getStringAttribute("databaseId");

    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
      return;
    }

    String nodeName = context.getNode().getNodeName();
    SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
  }
}  
public enum SqlCommandType {
  UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH
}    

2.1.1.2 注解方式

一定是解析注解方法 AnnotationWrapper。將不同的注解解析成SqlCommandType。如下偽代碼。通過(guò)解析方法上的注解,判斷注解類(lèi)型,來(lái)確定sql的類(lèi)型。
MapperAnnotationBuilder#getAnnotationWrapper(method, true, statementAnnotationTypes)

private class AnnotationWrapper {
    private final Annotation annotation;
    private final String databaseId;
    private final SqlCommandType sqlCommandType;

    AnnotationWrapper(Annotation annotation) {
      super();
      this.annotation = annotation;
      if (annotation instanceof Select) {
        databaseId = ((Select) annotation).databaseId();
        sqlCommandType = SqlCommandType.SELECT;
      } else if (annotation instanceof Update) {
        databaseId = ((Update) annotation).databaseId();
        sqlCommandType = SqlCommandType.UPDATE;
      } else if (annotation instanceof Insert) {
        databaseId = ((Insert) annotation).databaseId();
        sqlCommandType = SqlCommandType.INSERT;
      } else if (annotation instanceof Delete) {
        databaseId = ((Delete) annotation).databaseId();
        sqlCommandType = SqlCommandType.DELETE;
      } else if (annotation instanceof SelectProvider) {
        databaseId = ((SelectProvider) annotation).databaseId();
        sqlCommandType = SqlCommandType.SELECT;
      } else if (annotation instanceof UpdateProvider) {
        databaseId = ((UpdateProvider) annotation).databaseId();
        sqlCommandType = SqlCommandType.UPDATE;
      } else if (annotation instanceof InsertProvider) {
        databaseId = ((InsertProvider) annotation).databaseId();
        sqlCommandType = SqlCommandType.INSERT;
      } else if (annotation instanceof DeleteProvider) {
        databaseId = ((DeleteProvider) annotation).databaseId();
        sqlCommandType = SqlCommandType.DELETE;
      } else {
        sqlCommandType = SqlCommandType.UNKNOWN;
        if (annotation instanceof Options) {
          databaseId = ((Options) annotation).databaseId();
        } else if (annotation instanceof SelectKey) {
          databaseId = ((SelectKey) annotation).databaseId();
        } else {
          databaseId = "";
        }
      }
    }

    Annotation getAnnotation() {
      return annotation;
    }

    SqlCommandType getSqlCommandType() {
      return sqlCommandType;
    }
}    

到這里我們知道了sql類(lèi)型是如何區(qū)分出來(lái)的,既然能區(qū)分出來(lái),就知道如何去執(zhí)行sql了。
是不是很簡(jiǎn)單? 當(dāng)然看的話(huà)很簡(jiǎn)單,但是如何讓你自己來(lái)找,你能找到嗎? 所以建議在閱讀的時(shí)候
要自己去源碼中找找。

2.1.2 sql參數(shù)如何組裝?

在mybatis中有兩種處理sql參數(shù)的地方,第一種是#{} 占位符,第二種是${} 變量符。這兩種都是處理參數(shù)的方式。那說(shuō)到這里,不得不提的就是sql注入的黑客技術(shù)。
sql注入就是就是利用了變量符。將我們?cè)瓉?lái)的sql進(jìn)行惡意的修改。舉一個(gè)例子。下面根據(jù)用戶(hù)id和用戶(hù)密碼查詢(xún)用戶(hù)信息。

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

那么如何在不知道密碼只有用戶(hù)id的情況下查詢(xún)到用戶(hù)信息呢? 我們只需要將sql轉(zhuǎn)換成下面這樣即可。

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

那mybatis允許我們這樣做嗎? 允許,如果我們使用的是 ${} 變量符,那么mybatis只是將參數(shù)和變量符進(jìn)行替換。你輸入的參數(shù)可能也會(huì)被當(dāng)成sql去執(zhí)行了。如下代碼示例。

public interface T4UserMapper {
    /**
     * 獲取用戶(hù)信息
     *
     * @param uid     用戶(hù)id
     * @param tokenId token
     * @return TUser
     */
    @Select("select * from t_user where token_id = ${token_id} and uid = ${uid}")
    TUser queryUserById(@Param("uid") Long uid, @Param("token_id") String tokenId);
}
public class Test{
    @Test
    public void sql(){
        // 讀取配置信息
        InputStream mapperInputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("example05/mybatisConfig.xml");
        // 生成SqlSession工廠,SqlSession從名字上看就是,跟數(shù)據(jù)庫(kù)交互的會(huì)話(huà)信息,負(fù)責(zé)將sql提交到數(shù)據(jù)庫(kù)進(jìn)行執(zhí)行
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(mapperInputStream, "development");
        // 獲取Mybatis配置信息
        Configuration configuration = sqlSessionFactory.getConfiguration();
        SqlSession sqlSession = sqlSessionFactory.openSession(false);
        // debug
        T4UserMapper mapper = configuration.getMapper(T4UserMapper.class, sqlSession);
        // 模擬sql注入
        System.out.println(mapper.queryUserById(37L,"0 or 1 = 1"));
    }
}    

Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@62ddbd7e]
==>  Preparing: select * from t_user where token_id = 0 or 1 = 1 and uid = 37
==> Parameters: 
<==    Columns: uid, name, token_id
<==        Row: 37, 無(wú)天, 60
<==      Total: 1
TUser(tokenId=null, uid=37, name=無(wú)天)

要想避免這樣的問(wèn)題,我們只需要將${} 變量符,都替換成#{} 占位符就好了。那么Mybatis只會(huì)將你的參數(shù)當(dāng)做是參數(shù)處理,不會(huì)當(dāng)做是sql執(zhí)行。如下代碼示例。

public interface T4UserMapper {
    /**
     * 獲取用戶(hù)信息
     *
     * @param uid     用戶(hù)id
     * @param tokenId token
     * @return TUser
     */
    @Select("select * from t_user where token_id = #{token_id} and uid = #{uid}")
    TUser queryUserById(@Param("uid") Long uid, @Param("token_id") String tokenId);
}
public class Test{
    @Test
    public void sql(){
        // 讀取配置信息
        InputStream mapperInputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("example05/mybatisConfig.xml");
        // 生成SqlSession工廠,SqlSession從名字上看就是,跟數(shù)據(jù)庫(kù)交互的會(huì)話(huà)信息,負(fù)責(zé)將sql提交到數(shù)據(jù)庫(kù)進(jìn)行執(zhí)行
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(mapperInputStream, "development");
        // 獲取Mybatis配置信息
        Configuration configuration = sqlSessionFactory.getConfiguration();
        SqlSession sqlSession = sqlSessionFactory.openSession(false);
        // debug
        T4UserMapper mapper = configuration.getMapper(T4UserMapper.class, sqlSession);
        // 模擬sql注入 => null
        System.out.println(mapper.queryUserById(37L,"0 or 1 = 1"));
    }
}  

Created connection 798981583.
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@2f9f7dcf]
==>  Preparing: select * from t_user where token_id = ? and uid = ?
==> Parameters: 0 or 1 = 1(String), 37(Long)
<==      Total: 0
null

以上演示代碼可以在 com.test.example05.SqlParseTest中找到。那么無(wú)論是變量符還是占位符,其實(shí)都是sql組裝,下面我們正式開(kāi)始學(xué)習(xí)。

==同樣我們先提兩個(gè)問(wèn)題==

2.1.2.1 方法參數(shù)如何來(lái)解析

關(guān)鍵代碼就在MapperMethod的execute的入?yún)?Object [] args;
關(guān)于參數(shù)的處理都在這里處理了。MethodSignature#convertArgsToSqlCommandParam。

public class MapperMethod {
  public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) {
      case INSERT: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.insert(command.getName(), param));
        break;
      }
      case UPDATE: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.update(command.getName(), param));
        break;
      }
      case DELETE: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.delete(command.getName(), param));
        break;
      }
    ....  
    return result;
  }
}
public Object convertArgsToSqlCommandParam(Object[] args) {
    return paramNameResolver.getNamedParams(args);
}

參數(shù)會(huì)被解析成什么樣呢? 關(guān)鍵代碼就在這里。

 public Object getNamedParams(Object[] args) {
    final int paramCount = names.size();
    // 沒(méi)有參數(shù)直接返回
    if (args == null || paramCount == 0) {
      return null;
    } else if (!hasParamAnnotation && paramCount == 1) {
      // 沒(méi)有注解只有一個(gè)參數(shù)
      Object value = args[names.firstKey()];
      return wrapToMapIfCollection(value, useActualParamName ? names.get(0) : null);
    } else {
      final Map<String, Object> param = new ParamMap<>();
      int i = 0;
      // names key = 參數(shù)下標(biāo) value = @Param里面的值
      for (Map.Entry<Integer, String> entry : names.entrySet()) {
        // key = @Param里面的值,value = args[index] 真實(shí)數(shù)據(jù)
        param.put(entry.getValue(), args[entry.getKey()]);
        // 生成param1,參數(shù)
        final String genericParamName = GENERIC_NAME_PREFIX + (i + 1);
        // ensure not to overwrite parameter named with @Param
        if (!names.containsValue(genericParamName)) {
          param.put(genericParamName, args[entry.getKey()]);
        }
        i++;
      }
      return param;
    }
  }

我們直接說(shuō)結(jié)論,如果方法簽名中使用了@Param注解結(jié)論,則占位符中的參數(shù)名就是注解的值。如果沒(méi)有注解在就是arg+參數(shù)的位置.

com.test.example04.MethodSignatureTest

參數(shù)類(lèi)型 方法簽名 參數(shù)值 結(jié)果
解析單參數(shù)不帶@Param TUser queryUserByName(String name) methodSignature.convertArgsToSqlCommandParam(new Object[]{"孫悟空"}) 孫悟空
解析單參數(shù)帶@Param TUser queryUserById(@Param("userId") Long id) methodSignature.convertArgsToSqlCommandParam(new Object[]{1L}) {userId=1, param1=1}
解析多參數(shù)不帶@Param TUser queryUserByTokenId(Long tokenId,String name) methodSignature.convertArgsToSqlCommandParam(new Object[]{1L, "孫悟空"}) {arg0=1, arg1=孫悟空, param1=1, param2=孫悟空}
解析多參數(shù)帶@Param TUser queryUserByTokenId(@Param("tokenId") Long tokenId, @Param("name") String name) methodSignature.convertArgsToSqlCommandParam(new Object[]{1L, "孫悟空"}) {tokenId=1, name=孫悟空, param1=1, param2=孫悟空}

如果項(xiàng)目編譯中設(shè)置了編譯后保存參數(shù)名,那么可以獲取代碼中編寫(xiě)的參數(shù)名。

好了到這里我們知道方法的參數(shù)最終都會(huì)被Mybatis重新解析,解析后的結(jié)果可以看以上的表格。主要就是為拼裝參數(shù)提前準(zhǔn)備數(shù)據(jù)。下面我們看sql信息最終是如何最終組裝的吧。

2.1.2.2 方法參數(shù)組裝

這里我們思考一下,變量符應(yīng)該是動(dòng)態(tài)sql,在調(diào)用jdbc時(shí)候應(yīng)該是下面的例子。

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

那么我們就尋找哪里有這樣的代碼。

PreparedStatementHandler#instantiateStatement.

@Override
  protected Statement instantiateStatement(Connection connection) throws SQLException {
    String sql = boundSql.getSql();
    if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) {
      String[] keyColumnNames = mappedStatement.getKeyColumns();
      if (keyColumnNames == null) {
        return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);
      } else {
        return connection.prepareStatement(sql, keyColumnNames);
      }
    } else if (mappedStatement.getResultSetType() == ResultSetType.DEFAULT) {
      return connection.prepareStatement(sql);
    } else {
      return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY);
    }
  }

關(guān)鍵的代碼就在這里靜態(tài)sql,直接從MappedStatement#getBoundSql(Object parameterObject)#getSql()獲取組裝后的代碼。

  @Override
  public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
      Configuration configuration = ms.getConfiguration();
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      stmt = prepareStatement(handler, ms.getStatementLog());
      return handler.query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }
  
  // 這里parameterObject就是前面對(duì)方法參數(shù)的解析返回值。通過(guò)mappedStatement.getBoundSql(parameterObject)組裝靜態(tài)sql
  protected PreparedStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    this.configuration = mappedStatement.getConfiguration();
    this.executor = executor;
    this.mappedStatement = mappedStatement;
    this.rowBounds = rowBounds;

    this.typeHandlerRegistry = configuration.getTypeHandlerRegistry();
    this.objectFactory = configuration.getObjectFactory();
    if (boundSql == null) { // issue #435, get the key before calculating the statement
      generateKeys(parameterObject);
      boundSql = mappedStatement.getBoundSql(parameterObject);
    }

    this.boundSql = boundSql;
    this.parameterHandler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql);
    this.resultSetHandler = configuration.newResultSetHandler(executor, mappedStatement, rowBounds, parameterHandler, resultHandler, boundSql);
  }

好了,到這里我們就知道靜態(tài)sql是哪里組裝的了。關(guān)鍵點(diǎn)就在BoundSql這個(gè)類(lèi)是如何構(gòu)建的。我們以注解方式舉例。

在構(gòu)建MappedStatement的時(shí)候,MapperBuilderAssistant#parse會(huì)解析Mapper類(lèi)所有的方法,獲取方法上的注解,生成Sql的信息。
判斷sql類(lèi)型,如果是${}變量符,Sql資源就是DynamicSqlSource動(dòng)態(tài)Sql。如果是#{}占位符就是RawSqlSource會(huì)將占位符替換成?,同時(shí)生成ParameterMapping信息
用于方法執(zhí)行時(shí)候使用PreparedStatement去set參數(shù)信息。

下面我們以示例中的代碼來(lái)看下BoundSql中究竟有什么信息。

那么對(duì)于第一種DynamicSqlSource動(dòng)態(tài)sql,參數(shù)信息是如何組裝的呢?

public class DynamicSqlSource implements SqlSource {

  private final Configuration configuration;
  private final SqlNode rootSqlNode;

  public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
    this.configuration = configuration;
    this.rootSqlNode = rootSqlNode;
  }

  @Override
  public BoundSql getBoundSql(Object parameterObject) {
    DynamicContext context = new DynamicContext(configuration, parameterObject);
    // 處理sql中如果有<if><where><Trim>等自帶標(biāo)簽的情況,同時(shí)處理將變量符提供換成真正的參數(shù)。
    rootSqlNode.apply(context);
    // 當(dāng)執(zhí)行完上面的流程變量符就被替換成真正的參數(shù)了。下面在看是否同時(shí)也包含了#{}占位符,如果包含就替換成?
    // 在調(diào)換成?的同時(shí)新增一個(gè)ParameterMapping對(duì)象
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
    SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    context.getBindings().forEach(boundSql::setAdditionalParameter);
    return boundSql;
  }

}

核心的方法就是變量符替換,下面直接將核心的代碼展示出來(lái)。

    @Test
    public void dynamicSql() throws Exception {
        // 讀取配置信息(為什么路徑前不用加/,因?yàn)槭窍鄬?duì)路徑。maven編譯后的資源文件和class文件都是在一個(gè)包下,所以不用加/就是當(dāng)前包目錄)
        InputStream mapperInputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("example05/mybatisConfig.xml");
        // 生成SqlSession工廠,SqlSession從名字上看就是,跟數(shù)據(jù)庫(kù)交互的會(huì)話(huà)信息,負(fù)責(zé)將sql提交到數(shù)據(jù)庫(kù)進(jìn)行執(zhí)行
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(mapperInputStream, "development");
        // 獲取Mybatis配置信息
        Configuration configuration = sqlSessionFactory.getConfiguration();
        // 生成動(dòng)態(tài)Sql
        TextSqlNode textSqlNode = new TextSqlNode("select * from t_user where token_id = ${token_id} and uid = ${uid}");
        DynamicSqlSource dynamicSqlSource = new DynamicSqlSource(configuration, textSqlNode);

        // 裝參數(shù)
        MapperMethod.ParamMap<Object> paramMap = new MapperMethod.ParamMap<Object>();
        paramMap.put("uid",37L);
        paramMap.put("token_id","0 or 1 = 1");
        BoundSql boundSql = dynamicSqlSource.getBoundSql(paramMap);
        System.out.println(boundSql.getSql());
    }
    
    @Test
    public void dynamicSql2(){
        // 讀取配置信息(為什么路徑前不用加/,因?yàn)槭窍鄬?duì)路徑。maven編譯后的資源文件和class文件都是在一個(gè)包下,所以不用加/就是當(dāng)前包目錄)
        InputStream mapperInputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("example05/mybatisConfig.xml");
        // 生成SqlSession工廠,SqlSession從名字上看就是,跟數(shù)據(jù)庫(kù)交互的會(huì)話(huà)信息,負(fù)責(zé)將sql提交到數(shù)據(jù)庫(kù)進(jìn)行執(zhí)行
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(mapperInputStream, "development");
        // 獲取Mybatis配置信息
        Configuration configuration = sqlSessionFactory.getConfiguration();

        // 裝參數(shù)
        MapperMethod.ParamMap<Object> paramMap = new MapperMethod.ParamMap<Object>();
        paramMap.put("uid",37L);
        paramMap.put("token_id","0 or 1 = 1");
        DynamicContext context = new DynamicContext(configuration, paramMap);

        // 生成動(dòng)態(tài)Sql
        TextSqlNode textSqlNode = new TextSqlNode("select * from t_user where token_id = ${token_id} and uid = ${uid}");
        textSqlNode.apply(context);
        System.out.println(context.getSql());
    }

好了,我們知道動(dòng)態(tài)sql其實(shí)就是${},變量符號(hào)替換。
下面我們看靜態(tài)sql是如何處理占位符的吧。

前面我們說(shuō)了靜態(tài)sql,在初始化時(shí)候就會(huì)將占位符替換成? 同時(shí)生成一個(gè)ParameterMapping對(duì)象,然后在執(zhí)行sql時(shí)候通過(guò)PreparedStatement進(jìn)行set參數(shù)信息。
那么我們先看占位符如何替換成?的吧。實(shí)現(xiàn)邏輯其實(shí)就在RawSqlSource的構(gòu)造方法中。

  • line(1-5) 在Mybatis初始化時(shí)候,會(huì)生成RawSqlSource。在構(gòu)造中去調(diào)換占位符
  • line(8-19) 占位符替換的實(shí)現(xiàn)方式,最終生成StaticSqlSource
  • line(22-28) 占位符返回?的同時(shí),生成一個(gè)ParameterMapping對(duì)象
public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class<?> clazz = parameterType == null ? Object.class : parameterType;
    sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<>());
}
  
// sql = select * from t_user where token_id = #{token_id} and uid = #{uid}
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
    ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
    // 對(duì)
    GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
    String sql;
    if (configuration.isShrinkWhitespacesInSql()) {
      sql = parser.parse(removeExtraWhitespaces(originalSql));
    } else {
      sql = parser.parse(originalSql);
    }
    return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
  }
 
 // 會(huì)將占位符號(hào)#{token_id}替換成 ?同時(shí)生成一個(gè)ParameterMapping對(duì)象。
 private static class ParameterMappingTokenHandler extends BaseBuilder implements TokenHandler {
    // content = token_id
    @Override
    public String handleToken(String content) {
      parameterMappings.add(buildParameterMapping(content));
      return "?";
    }
 }  

到這里占位符的解析已經(jīng)很清楚了。BoundSql中的數(shù)據(jù)我們也知道了,我們直接看參數(shù)組裝的邏輯吧。

  1. 從boundSql中獲取占位符信息。
  2. 根據(jù)占位符獲取參數(shù)信息
  3. 根據(jù)參數(shù)類(lèi)型確定使用那個(gè)TypeHandler,如果都沒(méi)有指定就用UnknownTypeHandler
  4. UnknownTypeHandler會(huì)根據(jù)參數(shù)的類(lèi)型,從默認(rèn)配置中找到要用的類(lèi)型,如果是Long類(lèi)型就是PreparedStatement#setLong,如果是String類(lèi)型就是PreparedStatement#setString
public class DefaultParameterHandler implements ParameterHandler {
  @Override
  public void setParameters(PreparedStatement ps) {
    ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings != null) {
      for (int i = 0; i < parameterMappings.size(); i++) {
        ParameterMapping parameterMapping = parameterMappings.get(i);
        if (parameterMapping.getMode() != ParameterMode.OUT) {
          Object value;
          String propertyName = parameterMapping.getProperty();
          if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
            value = boundSql.getAdditionalParameter(propertyName);
          } else if (parameterObject == null) {
            value = null;
          } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
            value = parameterObject;
          } else {
            MetaObject metaObject = configuration.newMetaObject(parameterObject);
            value = metaObject.getValue(propertyName);
          }
          TypeHandler typeHandler = parameterMapping.getTypeHandler();
          JdbcType jdbcType = parameterMapping.getJdbcType();
          if (value == null && jdbcType == null) {
            jdbcType = configuration.getJdbcTypeForNull();
          }
          try {
            typeHandler.setParameter(ps, i + 1, value, jdbcType);
          } catch (TypeException | SQLException e) {
            throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
          }
        }
      }
    }
  }

}

好了到這里我們就搞清楚Mybatis中的參數(shù)是如何組裝的了。 以及Jdbc是如何執(zhí)行sql的了。
這部分內(nèi)容比較復(fù)雜,僅僅通過(guò)看是看不明白的,建議根據(jù)文中的代碼自己走一邊。加深理解。

下面我們看Mybatis是如何處理返回值的吧。

2.2 Sql結(jié)果集是如何轉(zhuǎn)換方法返回值的?

我們重新回到PreparedStatementHandler中跟數(shù)據(jù)庫(kù)打交道的地方,當(dāng)PreparedStatement#execute發(fā)送sql給數(shù)據(jù)庫(kù)后,最終處理結(jié)果集的類(lèi)是
ResultHandler,下面我們就圍繞這個(gè)類(lèi)做分析。

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

ResultSetHandler,我們看接口定義,處理結(jié)果集就在這里了。我們?cè)賮?lái)看實(shí)現(xiàn)。

public interface ResultSetHandler {

  <E> List<E> handleResultSets(Statement stmt) throws SQLException;

  <E> Cursor<E> handleCursorResultSets(Statement stmt) throws SQLException;

  void handleOutputParameters(CallableStatement cs) throws SQLException;

}

默認(rèn)的實(shí)現(xiàn)DefaultResultSetHandler。Mybatis實(shí)現(xiàn)較為復(fù)雜,我們一開(kāi)始可能看不懂。我們先用原生的jdbc來(lái)自己實(shí)現(xiàn)一邊。
然后腦子里有一個(gè)思路,然后在根據(jù)思路來(lái)看DefaultResultSetHandler的實(shí)現(xiàn)吧。

2.2.1 JDBC提供的結(jié)果處理API

思路是statement執(zhí)行完后會(huì)返回結(jié)果集ResultSet。
結(jié)果集包含了返回的數(shù)據(jù)及這些數(shù)據(jù)對(duì)應(yīng)的字段信息。
然后拿到這些字段信息分別從結(jié)果集中獲取數(shù)據(jù)。下面的代碼如果明白了,我們就去看Mybatis中的源碼

    @Test
    public void resultMetaData() throws Exception {
        String dbUrl = "jdbc:mysql://127.0.0.1:3306/test";
        String user = "root";
        String pass = "123456";
        // 1. 獲取數(shù)據(jù)庫(kù)連接
        Connection connection = DriverManager.getConnection(dbUrl, user, pass);
        Statement statement = connection.createStatement();
        // 2. 執(zhí)行sql語(yǔ)句獲取結(jié)果集
        ResultSet resultSet = statement.executeQuery("select uid,name,token_id as tokenId from T_User");
        // 3. 從結(jié)果集中,獲取數(shù)據(jù)庫(kù)返回的數(shù)據(jù)列名
        ResultSetMetaData metaData = resultSet.getMetaData();
        int columnCount = metaData.getColumnCount();
        // 所有的列名
        List<String> columnNames = new ArrayList<>();
        // 列名對(duì)應(yīng)的java類(lèi)型
        Map<String, Class<?>> column2JavaTypeAsMap = new HashMap<>();
        for (int i = 1; i <= columnCount; i++) {
            System.out.println("字段:" + metaData.getColumnName(i) + "是否自增:" + metaData.isAutoIncrement(i));
            System.out.println("字段名:" + metaData.getColumnName(i));
            System.out.println("字段別名:" + metaData.getColumnLabel(i));
            System.out.println("MySql字段類(lèi)型:" + metaData.getColumnTypeName(i));
            // Java 類(lèi)的完全限定名稱(chēng)
            System.out.println("Java字段類(lèi)型:" + metaData.getColumnClassName(i));
            // 獲取指定列的指定列大小。
            System.out.println("字段長(zhǎng)度:" + metaData.getPrecision(i));
            System.out.println("字段保留小數(shù)位:" + metaData.getScale(i));
            System.out.println("字段屬于的表名:" + metaData.getTableName(i));
            System.out.println("是否可為空:" + metaData.isNullable(i));
            // 這里使用別名,如果沒(méi)有別名的情況,別名跟字段名是一樣的。
            columnNames.add(metaData.getColumnLabel(i));
            column2JavaTypeAsMap.put(metaData.getColumnLabel(i), Class.forName(metaData.getColumnClassName(i)));
        }
        int row = 1;
        while (resultSet.next()) {
            System.out.println("----------第" + row + "行數(shù)據(jù)開(kāi)始----------");
            for (String columnName : columnNames) {
                Object columnValue = getValue(columnName, resultSet, column2JavaTypeAsMap);
                System.out.println("列:" + columnName + ":value:" + columnValue);
            }
            System.out.println("----------第" + row + "行數(shù)據(jù)結(jié)束----------");
            row++;
        }
        resultSet.close();
        statement.close();
        connection.close();
    }

    /**
     * 根據(jù)不同的字段類(lèi)型,調(diào)用不同的方法獲取數(shù)據(jù)
     *
     * @param columnName           列名
     * @param resultSet            集合集
     * @param column2JavaTypeAsMap 字段對(duì)應(yīng)的Java類(lèi)型
     * @return 結(jié)果值
     * @throws Exception 未知異常
     */
    public Object getValue(String columnName, ResultSet resultSet, Map<String, Class<?>> column2JavaTypeAsMap) throws Exception {
        Class<?> column2JavaType = column2JavaTypeAsMap.get(columnName);
        Object value = null;
        if (column2JavaType.equals(Integer.class)) {
            value = resultSet.getInt(columnName);
        } else if (column2JavaType.equals(String.class)) {
            value = resultSet.getString(columnName);
        }
        return value;
    }
    
字段:uid是否自增:true
字段名:uid
字段別名:uid
MySql字段類(lèi)型:INT
Java字段類(lèi)型:java.lang.Integer
字段長(zhǎng)度:11
字段保留小數(shù)位:0
字段屬于的表名:t_user
是否可為空:0
字段:name是否自增:false
字段名:name
字段別名:name
MySql字段類(lèi)型:CHAR
Java字段類(lèi)型:java.lang.String
字段長(zhǎng)度:32
字段保留小數(shù)位:0
字段屬于的表名:t_user
是否可為空:1
字段:token_id是否自增:false
字段名:token_id
字段別名:tokenId
MySql字段類(lèi)型:CHAR
Java字段類(lèi)型:java.lang.String
字段長(zhǎng)度:64
字段保留小數(shù)位:0
字段屬于的表名:t_user
是否可為空:0
----------第1行數(shù)據(jù)開(kāi)始----------
列:uid:value:37
列:name:value:無(wú)天
列:tokenId:value:60
----------第1行數(shù)據(jù)結(jié)束----------
----------第2行數(shù)據(jù)開(kāi)始----------
列:uid:value:9846
列:name:value:斗戰(zhàn)勝佛
列:tokenId:value:80
----------第2行數(shù)據(jù)結(jié)束----------
----------第3行數(shù)據(jù)開(kāi)始----------
列:uid:value:9847
列:name:value:凈壇使者
列:tokenId:value:90
----------第3行數(shù)據(jù)結(jié)束----------
----------第4行數(shù)據(jù)開(kāi)始----------
列:uid:value:9848
列:name:value:無(wú)量功德佛祖
列:tokenId:value:100
----------第4行數(shù)據(jù)結(jié)束----------

ResultSetMetaData 方法是比較重要的,這里把他常用的api方法及解釋以表格形式列舉一下。
當(dāng)我們拿到返回的列名,就可以直接根據(jù)列名來(lái)返回?cái)?shù)據(jù)了。

方法 含義 示例
ResultSetMetaData#getColumnName 獲取數(shù)據(jù)庫(kù)字段名 name
ResultSetMetaData#getColumnLabel 查詢(xún)語(yǔ)句中字段別名,如果沒(méi)有保持跟字段名一致 user_id as userId,這里就是userId
ResultSetMetaData#getColumnTypeName 返回Sql字段類(lèi)型 INT、CHAR
ResultSetMetaData#getColumnClassName 返回Java字段類(lèi)型的完整限定名 java.lang.String、java.lang.Integer
ResultSetMetaData#getPrecision 獲取定義的字段長(zhǎng)度 int(11),返回11
ResultSetMetaData#getScale 獲取字段定義的保留小數(shù)位 -
ResultSetMetaData#getTableName 字段對(duì)應(yīng)的表 -
ResultSetMetaData#isNullable 字段是否可以為空 -
ResultSetMetaData#isAutoIncrement 是否數(shù)據(jù)庫(kù)自增字段 -
ResultSetMetaData#isAutoIncrement 是否數(shù)據(jù)庫(kù)自增字段 -

2.2.2 Mybatis獲取結(jié)果集

思考下結(jié)果集可能是什么?

  1. 場(chǎng)景一: 可能返回的是List
    @Select("select * from t_user")
    List<TUser> queryAllUsers();
  1. 場(chǎng)景二: 可能返回的是單個(gè)對(duì)象
    @Select("select * from t_user where uid = #{uid}")
    TUser queryUserByPlaceholderId(@Param("uid") Long uid);
  1. 場(chǎng)景三: 更新語(yǔ)句返回結(jié)果集是條數(shù)。
    @Update("update t_user set name = #{name}")
    int updateName(@Param("uid") Long uid, @Param("name") String name);
  1. 場(chǎng)景四: 更新語(yǔ)句返回boolean
    @Update("update t_user set name = #{name} where uid = #{uid}")
    boolean updateNameById(@Param("uid") Long uid, @Param("name") String name);

分別來(lái)分析。

場(chǎng)景一:

public class MapperMethod {
    private final MethodSignature method;
    public Object execute(SqlSession sqlSession, Object[] args) {
        Object result;
        switch (command.getType()) {
          case SELECT:
            if (method.returnsVoid() && method.hasResultHandler()) {
              executeWithResultHandler(sqlSession, args);
              result = null;
            } else if (method.returnsMany()) {
              result = executeForMany(sqlSession, args);
            } else if (method.returnsMap()) {
              result = executeForMap(sqlSession, args);
            } else if (method.returnsCursor()) {
              result = executeForCursor(sqlSession, args);
            } else {
              Object param = method.convertArgsToSqlCommandParam(args);
              result = sqlSession.selectOne(command.getName(), param);
              if (method.returnsOptional()
                  && (result == null || !method.getReturnType().equals(result.getClass()))) {
                result = Optional.ofNullable(result);
              }
            }
            break;
          case FLUSH:
            result = sqlSession.flushStatements();
            break;
          default:
            throw new BindingException("Unknown execution method for: " + command.getName());
        }
        if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
          throw new BindingException("Mapper method '" + command.getName()
              + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
        }
        return result;
      }
}

可以看到這里對(duì)于方法的返回值判斷是根據(jù)MethodSignature,MethodSignature不僅提供了對(duì)參數(shù)的解析,同時(shí)也是對(duì)方法的分析。
包括判斷方法的返回值,我們看它的內(nèi)部屬性。

 public static class MethodSignature {
    // 是否返回集合
    private final boolean returnsMany;
    // 是否返回是map結(jié)構(gòu)
    private final boolean returnsMap;
    // 是否沒(méi)有返回值
    private final boolean returnsVoid;
    // 是否返回的是游標(biāo)
    private final boolean returnsCursor;
    // 是否返回的是Optional對(duì)象
    private final boolean returnsOptional;
    // 返回值類(lèi)型
    private final Class<?> returnType;
    // 返回map結(jié)構(gòu)使用的key字段
    private final String mapKey;
    // 如果入?yún)⑹荝esultHandler 記錄器下標(biāo)
    private final Integer resultHandlerIndex;
    // 如果參數(shù)是RowBounds,記錄其下標(biāo)
    private final Integer rowBoundsIndex;
    // 參數(shù)處理
    private final ParamNameResolver paramNameResolver;
    
  }  

如果發(fā)現(xiàn)是返回List。則MethodSignature#returnsMany=true。直接調(diào)用SqlSession#selectList

private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
    List<E> result;
    Object param = method.convertArgsToSqlCommandParam(args);
    // 方法中是否包含邏輯分頁(yè)參數(shù)RowBounds
    if (method.hasRowBounds()) {
      // 如果有就獲取邏輯分頁(yè)參數(shù)
      RowBounds rowBounds = method.extractRowBounds(args);
      // 執(zhí)行sql
      result = sqlSession.selectList(command.getName(), param, rowBounds);
    } else {
      result = sqlSession.selectList(command.getName(), param);
    }
    // issue #510 Collections & arrays support
    if (!method.getReturnType().isAssignableFrom(result.getClass())) {
      if (method.getReturnType().isArray()) {
        return convertToArray(result);
      } else {
        return convertToDeclaredCollection(sqlSession.getConfiguration(), result);
      }
    }
    return result;
  }

最終在DefaultResultSetHandler#handleResultSets處理返回值。下面的代碼看了先不要害怕,其實(shí)
思路跟我們用jdbc來(lái)處理是一樣的。第一要拿到返回的數(shù)據(jù)信息。第二要將返回的數(shù)據(jù)信息包裝成方法的返回值。
只不過(guò)Mybatis將上面的兩個(gè)能力,都提供成了對(duì)應(yīng)的接口。其中數(shù)據(jù)的返回集就是ResultSetWrapper,從返回集中獲取數(shù)據(jù)是TypeHandler。
而將數(shù)據(jù)庫(kù)返回的行數(shù)據(jù),轉(zhuǎn)換成方法的返回值就要用到ResultMap。

  @Override
  public List<Object> handleResultSets(Statement stmt) throws SQLException {
    ErrorContext.instance().activity("handling results").object(mappedStatement.getId());

    final List<Object> multipleResults = new ArrayList<>();

    int resultSetCount = 0;
    // 讀取返回的數(shù)據(jù)信息(jdbcType,javaType,列名和別名)
    ResultSetWrapper rsw = getFirstResultSet(stmt);
    // Mapper簽名中找到返回集應(yīng)該信息
    List<ResultMap> resultMaps = mappedStatement.getResultMaps();
    int resultMapCount = resultMaps.size();
    // 做個(gè)校驗(yàn),如果sql執(zhí)行后沒(méi)有任何返回信息,但是Mapper簽名中卻指定了返回映射信息。則會(huì)報(bào)錯(cuò)告警 A query was run and no Result Maps were found for the Mapped Statement
    validateResultMapsCount(rsw, resultMapCount);
    while (rsw != null && resultMapCount > resultSetCount) {
      ResultMap resultMap = resultMaps.get(resultSetCount);
      // 處理返回集
      handleResultSet(rsw, resultMap, multipleResults, null);
      rsw = getNextResultSet(stmt);
      cleanUpAfterHandlingResultSet();
      resultSetCount++;
    }

    String[] resultSets = mappedStatement.getResultSets();
    if (resultSets != null) {
      while (rsw != null && resultSetCount < resultSets.length) {
        ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
        if (parentMapping != null) {
          String nestedResultMapId = parentMapping.getNestedResultMapId();
          ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
          handleResultSet(rsw, resultMap, null, parentMapping);
        }
        rsw = getNextResultSet(stmt);
        cleanUpAfterHandlingResultSet();
        resultSetCount++;
      }
    }

    return collapseSingleResultList(multipleResults);
  }

下面我們看這幾個(gè)關(guān)鍵類(lèi)。ResultSetWrapper。這個(gè)的源碼是不是有點(diǎn)想我們前面自己寫(xiě)的原生jdbc的方法了?
拿到返回的列名和對(duì)應(yīng)的java類(lèi)型。

 public ResultSetWrapper(ResultSet rs, Configuration configuration) throws SQLException {
    super();
    this.typeHandlerRegistry = configuration.getTypeHandlerRegistry();
    this.resultSet = rs;
    final ResultSetMetaData metaData = rs.getMetaData();
    final int columnCount = metaData.getColumnCount();
    for (int i = 1; i <= columnCount; i++) {
      columnNames.add(configuration.isUseColumnLabel() ? metaData.getColumnLabel(i) : metaData.getColumnName(i));
      jdbcTypes.add(JdbcType.forCode(metaData.getColumnType(i)));
      classNames.add(metaData.getColumnClassName(i));
    }
  }

TypeHandler 是從jdbc中獲取數(shù)據(jù)的接口,這個(gè)功能就跟前面我們用原生API實(shí)現(xiàn)時(shí)候的getValue方法類(lèi)似。
主要是根據(jù)數(shù)據(jù)的類(lèi)型,來(lái)確定是調(diào)用ResultSet#getString還是調(diào)用ResultSet#getInt等方法。

public interface TypeHandler<T> {

  void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;

  T getResult(ResultSet rs, String columnName) throws SQLException;

  T getResult(CallableStatement cs, int columnIndex) throws SQLException;
}

ResultMap 是返回?cái)?shù)據(jù)對(duì)應(yīng)的Java對(duì)象。會(huì)在生成MappedStatement時(shí)候構(gòu)建完成。如果是在xml中定義了就是 <resultMap/> 標(biāo)簽,如果沒(méi)有就是
根據(jù)返回類(lèi)自動(dòng)生成一個(gè)resultMap??梢钥吹竭@個(gè)類(lèi)屬性其實(shí)跟他的標(biāo)簽是一樣的。

public class ResultMap {
  private Configuration configuration;

  // 如果配置了<resultMap id="BaseResultMap" ,就是類(lèi)全路徑名+BaseResultMap。如果沒(méi)有就是類(lèi)名加方法名+Inline
  private String id;
  private Class<?> type;
  private List<ResultMapping> resultMappings;
  private List<ResultMapping> idResultMappings;
  private List<ResultMapping> constructorResultMappings;
  private List<ResultMapping> propertyResultMappings;
  private Set<String> mappedColumns;
  private Set<String> mappedProperties;
  private Discriminator discriminator;
  private boolean hasNestedResultMaps;
  private boolean hasNestedQueries;
  private Boolean autoMapping;
}

ResultMap的標(biāo)簽功能比較強(qiáng)大,我們深入研究下。舉一個(gè)例子。

/**
 * 一個(gè)學(xué)校,一個(gè)校長(zhǎng),多個(gè)學(xué)生
 * name,headMaster(id,name),users()
 * 2022/4/10 22:07
 */
@Data
public class School {

    private Long id;

    private String name;

    private SchoolHeadMaster schoolHeadMaster;

    private List<Student> students;

}
@Data
public class SchoolHeadMaster {

    private Long id;

    private String name;
}

@Data
public class Student {

    private Long id;

    private String name;
}

配置文件如下

<mapper namespace="orm.example.dal.mapper.SchoolMapper">
    <resultMap id="BaseResultMap" type="orm.example.dal.model.TUser">
        <id column="token_id" jdbcType="CHAR" property="tokenId"/>
        <result column="uid" jdbcType="INTEGER" property="uid"/>
        <result column="name" jdbcType="CHAR" property="name"/>
    </resultMap>

    <resultMap id="schoolResultMap" type="orm.example.dal.model.School">
        <result column="schoolId" jdbcType="CHAR" property="id"/>
        <result column="schoolName" jdbcType="CHAR" property="name"/>
        <!--        學(xué)校校長(zhǎng)跟學(xué)校關(guān)系1對(duì)1-->
        <association property="schoolHeadMaster" javaType="orm.example.dal.model.SchoolHeadMaster">
            <id column="hmId" property="id"/>
            <result column="schoolHeadName" jdbcType="CHAR" property="name"/>
        </association>
        <!--        學(xué)生關(guān)系是1對(duì)n-->
        <collection property="students" javaType="list" ofType="orm.example.dal.model.Student">
            <id column="studentId" property="id"/>
            <result column="studentName" jdbcType="CHAR" property="name"/>
        </collection>
    </resultMap>


    <select id="selectSchool" resultMap="schoolResultMap">
        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'
        from school
                 left join head_master hm on hm.id = school.head_master_id
                 left join student s on school.id = s.school_id
    </select>
</mapper>

執(zhí)行數(shù)據(jù)驗(yàn)證 com.test.example05.ResultMapTest#parseResultMap

  • line(11-22) 獲取MappedStatement觀察復(fù)雜對(duì)象ResultMap是什么樣。
  • line(25-26) 觀察mybatis如何填充數(shù)據(jù)。
    @Test
    public void parseResultMap() {
        // 讀取配置信息(為什么路徑前不用加/,因?yàn)槭窍鄬?duì)路徑。maven編譯后的資源文件和class文件都是在一個(gè)包下,所以不用加/就是當(dāng)前包目錄)
        InputStream mapperInputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("example05/mybatisConfig-ResultMap.xml");
        // 生成SqlSession工廠,SqlSession從名字上看就是,跟數(shù)據(jù)庫(kù)交互的會(huì)話(huà)信息,負(fù)責(zé)將sql提交到數(shù)據(jù)庫(kù)進(jìn)行執(zhí)行
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(mapperInputStream, "development");
        // 獲取Mybatis配置信息
        Configuration configuration = sqlSessionFactory.getConfiguration();

        // 只要看這個(gè)復(fù)雜對(duì)象如何映射。
        MappedStatement selectSchool = configuration.getMappedStatement("orm.example.dal.mapper.SchoolMapper.selectSchool");
        ResultMap resultMap = selectSchool.getResultMaps().get(0);
        // 確定是一個(gè)復(fù)雜對(duì)象,規(guī)則是XMLMapperBuilder#processNestedResultMappings,只要發(fā)現(xiàn)查詢(xún)語(yǔ)句對(duì)象的結(jié)果中有以下標(biāo)簽"association", "collection", "case"。就是復(fù)雜sql
        System.out.println("是否復(fù)雜對(duì)象:" + resultMap.hasNestedResultMaps());
        List<ResultMapping> propertyResultMappings = resultMap.getPropertyResultMappings();
        for (ResultMapping propertyResultMapping : propertyResultMappings) {
            // 1. 屬性:id,db列名:schoolId,JavaType:class java.lang.Long
            // 2. 屬性:name,db列名:schoolName,JavaType:class java.lang.String
            // 3. 屬性:schoolHeadMaster,db列名:null,JavaType:class orm.example.dal.model.SchoolHeadMaster,映射N(xiāo)estedResultMapId
            // 4. 屬性:students,db列名:null,JavaType:interface java.util.List,映射N(xiāo)estedResultMapId
            printResultMapping(propertyResultMapping, configuration);
        }

        // [School(id=1, name=西天小學(xué), schoolHeadMaster=SchoolHeadMaster(id=1, name=如來(lái)), students=[Student(id=1, name=孫悟空), Student(id=2, name=豬八戒), Student(id=3, name=唐三藏)])]
        List<School> schools = configuration.getMapper(SchoolMapper.class, sqlSessionFactory.openSession(false)).selectSchool();
        System.out.println(schools);
    }

    private static void printResultMapping(ResultMapping propertyResultMapping, Configuration configuration) {
        String property = propertyResultMapping.getProperty();
        System.out.println("屬性:" + property + ",db列名:" + propertyResultMapping.getColumn() + ",JavaType:" + propertyResultMapping.getJavaType() + ",映射N(xiāo)estedResultMapId:" + propertyResultMapping.getNestedResultMapId());
        String nestedResultMapId = propertyResultMapping.getNestedResultMapId();
        // 如果不等于空,說(shuō)明是復(fù)雜對(duì)象。從配置文件中獲取復(fù)雜屬性的映射集合
        if (Objects.nonNull(nestedResultMapId)) {
            ResultMap nestedResultMap = configuration.getResultMap(nestedResultMapId);
            System.out.println(nestedResultMap.getType());
            System.out.println("是否復(fù)雜對(duì)象:" + nestedResultMap.hasNestedResultMaps());
            List<ResultMapping> propertyResultMappings = nestedResultMap.getPropertyResultMappings();
            for (ResultMapping resultMapping : propertyResultMappings) {
                printResultMapping(resultMapping, configuration);
            }
        }
    }

下面我們就看如何填充數(shù)據(jù)了。同樣我們直接手?jǐn)]代碼。

schoolId schoolName hmId schoolHeadName studentName studentId
1 西天小學(xué) 1 如來(lái) 孫悟空 1
1 西天小學(xué) 1 如來(lái) 豬八戒 2
1 西天小學(xué) 1 如來(lái) 唐三藏 3
2 湖畔大學(xué) 2 馬云 馬化騰 4
2 湖畔大學(xué) 2 馬云 謝霆鋒 5
2 湖畔大學(xué) 2 馬云 張學(xué)友 6

Mybatis中處理返回值,分一下基礎(chǔ)。簡(jiǎn)單對(duì)象和復(fù)雜對(duì)象這里我們直接用復(fù)雜對(duì)象距離。
可以看到School中有2個(gè)基本屬性和1個(gè)對(duì)象屬性還有一個(gè)集合屬性。

看這個(gè)圖。

這部分示例代碼在 com.test.example05.ResultMapTest#handlerResultSet

  • line(26) 首先我們要獲取數(shù)據(jù)庫(kù)返回列信息
  • line(30) 一行一行讀取數(shù)據(jù),每次執(zhí)行ResultSet#next就是下一行
  • line(41) 因?yàn)槲覀僑chool中有一個(gè)是集合屬性,需要將多行數(shù)據(jù)轉(zhuǎn)換成一行。此時(shí)我們執(zhí)行完getRowValue
    會(huì)生成一個(gè)數(shù)據(jù)。但是這個(gè)數(shù)據(jù)不能直接就用, 還需要將第二行的數(shù)據(jù)也賦值到第一行的返回值中,這是我們就將
    第一行的數(shù)據(jù)返回值,帶進(jìn)去。
  • line(41) 我們?nèi)绾沃肋@6行數(shù)據(jù)如何合并。規(guī)則: 簡(jiǎn)單對(duì)象進(jìn)行拼接,School中簡(jiǎn)單對(duì)象是id,和name。
  • line(44) getRowValue中的每個(gè)方法都要注意看
  • line(93-99) 主要處理是否需要合并行,合并行的時(shí)候直接填充數(shù)據(jù)接口。而不是合并則緩存中查不到數(shù)據(jù),就重新生成一個(gè)結(jié)果。
  • line(104) 判斷ResultMap是否是一個(gè)復(fù)雜對(duì)象,這里School是一個(gè)復(fù)雜對(duì)象,因?yàn)椴粌H有一個(gè)HeadMaster還有一個(gè)List的學(xué)生集合。
  • line(109) 第一次進(jìn)去這里會(huì)有4個(gè)對(duì)象,id,name,schoolHeadMaster,students
  • line(115-118) 對(duì)于School中的id和name都會(huì)在這幾行被執(zhí)行了??梢钥吹礁鶕?jù)javaType找到了TypeHandler,然后TypeHandler負(fù)責(zé)取值。
  • line(141) 對(duì)于schoolHeadMaster這個(gè)屬性,是復(fù)雜對(duì)象,School中的Java類(lèi)型是SchoolHeadMaster和他對(duì)應(yīng)的ResultMap中的類(lèi)型是一樣的,
    則遞歸去獲取數(shù)據(jù),因?yàn)镾choolHeadMaster中也是都簡(jiǎn)單類(lèi)型的id和name,所以最終也會(huì)在line(115-118)被執(zhí)行了。
  • line(125-138) School中的students,java類(lèi)型是List,ResultMap中類(lèi)型是Student,所以要先從第一行的數(shù)據(jù)去獲取這個(gè)屬性
    看List是否被實(shí)例化了,如果沒(méi)有就實(shí)例化。然后執(zhí)行add操作給list中數(shù)據(jù)追加值。

主要這里我們使用了MetaObject這個(gè)工具,是一個(gè)包裝方法。不詳細(xì)介紹了,如果還不清楚請(qǐng)?zhí)D(zhuǎn)

第03篇:Mybatis核心類(lèi)詳細(xì)介紹

    @Test
    public void handlerResultSet() throws Exception {

        // 讀取配置信息(為什么路徑前不用加/,因?yàn)槭窍鄬?duì)路徑。maven編譯后的資源文件和class文件都是在一個(gè)包下,所以不用加/就是當(dāng)前包目錄)
        InputStream mapperInputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("example05/mybatisConfig-ResultMap.xml");
        // 生成SqlSession工廠,SqlSession從名字上看就是,跟數(shù)據(jù)庫(kù)交互的會(huì)話(huà)信息,負(fù)責(zé)將sql提交到數(shù)據(jù)庫(kù)進(jìn)行執(zhí)行
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(mapperInputStream, "development");
        // 獲取Mybatis配置信息
        Configuration configuration = sqlSessionFactory.getConfiguration();

        // 只要看這個(gè)復(fù)雜對(duì)象如何映射。
        MappedStatement selectSchool = configuration.getMappedStatement("orm.example.dal.mapper.SchoolMapper.selectSchool");
        ResultMap resultMap = selectSchool.getResultMaps().get(0);

        PreparedStatement preparedStatement = execute("select school.id   as 'schoolId',\n" +
                "       school.name as 'schoolName',\n" +
                "       hm.id       as 'hmId',\n" +
                "       hm.name     as 'schoolHeadName',\n" +
                "       s.name      as 'studentName',\n" +
                "       s.id        as 'studentId'\n" +
                "from school\n" +
                "         left join head_master hm on hm.id = school.head_master_id\n" +
                "         left join student s on school.id = s.school_id");
        // 2. 執(zhí)行sql語(yǔ)句獲取結(jié)果集
        preparedStatement.execute();
        ResultSetWrapper firstResultSet = getFirstResultSet(preparedStatement, configuration);
        ResultSet resultSet = firstResultSet.getResultSet();
        Map<String, Object> one2ManyAsMap = new HashMap<>();
        // 3. 處理結(jié)果轉(zhuǎn)換,一行一行讀取數(shù)據(jù)
        while (resultSet.next()) {
            // 3.1 用于判斷多行數(shù)據(jù)是否要合并 規(guī)則: 簡(jiǎn)單對(duì)象屬性,如果一樣則可以合并。
            // 如: 下面數(shù)據(jù)返回值是 List<School> schools;School(Long id,String name,SchoolHeadMaster schoolHeadMaster,List<Student> students)
            //INSERT INTO MY_TABLE(schoolId, schoolName, hmId, schoolHeadName, studentName, studentId) VALUES (1, '西天小學(xué)', 1, '如來(lái)', '孫悟空', 1);
            //INSERT INTO MY_TABLE(schoolId, schoolName, hmId, schoolHeadName, studentName, studentId) VALUES (1, '西天小學(xué)', 1, '如來(lái)', '豬八戒', 2);
            //INSERT INTO MY_TABLE(schoolId, schoolName, hmId, schoolHeadName, studentName, studentId) VALUES (1, '西天小學(xué)', 1, '如來(lái)', '唐三藏', 3);
            //INSERT INTO MY_TABLE(schoolId, schoolName, hmId, schoolHeadName, studentName, studentId) VALUES (2, '湖畔大學(xué)', 2, '馬云', '馬化騰', 4);
            //INSERT INTO MY_TABLE(schoolId, schoolName, hmId, schoolHeadName, studentName, studentId) VALUES (2, '湖畔大學(xué)', 2, '馬云', '謝霆鋒', 5);
            //INSERT INTO MY_TABLE(schoolId, schoolName, hmId, schoolHeadName, studentName, studentId) VALUES (2, '湖畔大學(xué)', 2, '馬云', '張學(xué)友', 6);
            // 我們?nèi)绾沃肋@6行數(shù)據(jù)如何合并。規(guī)則: 簡(jiǎn)單對(duì)象進(jìn)行拼接,School中簡(jiǎn)單對(duì)象是id,和name。
            // 所以這里構(gòu)建的緩存key就是 id + name。相同就不新建返回值,而是對(duì)返回值二次賦值
            String cacheKey = getCacheKey(resultMap, resultSet, configuration);
            Object parentObject = one2ManyAsMap.get(cacheKey);
            // 3.2 開(kāi)始填充數(shù)據(jù)
            parentObject = getRowValue(resultMap, firstResultSet, configuration, parentObject);
            one2ManyAsMap.put(cacheKey, parentObject);
        }
        for (Object value : one2ManyAsMap.values()) {
            System.out.println(value);
        }
    }

    private PreparedStatement execute(String sql) throws Exception {
        String dbUrl = "jdbc:mysql://127.0.0.1:3306/test";
        String user = "root";
        String pass = "123456";
        // 1. 獲取數(shù)據(jù)庫(kù)連接
        Connection connection = DriverManager.getConnection(dbUrl, user, pass);
        return connection.prepareStatement(sql);
    }

    private ResultSetWrapper getFirstResultSet(Statement stmt, Configuration configuration) throws SQLException {
        ResultSet rs = stmt.getResultSet();
        while (rs == null) {
            if (stmt.getMoreResults()) {
                rs = stmt.getResultSet();
            } else {
                if (stmt.getUpdateCount() == -1) {
                    break;
                }
            }
        }
        return rs != null ? new ResultSetWrapper(rs, configuration) : null;
    }

    private static String getCacheKey(ResultMap resultMap, ResultSet resultSet, Configuration configuration) throws Exception {
        StringBuffer sb = new StringBuffer();
        sb.append(resultMap.getId());
        List<ResultMapping> propertyResultMappings = resultMap.getPropertyResultMappings();
        for (ResultMapping propertyResultMapping : propertyResultMappings) {
            if (propertyResultMapping.isSimple()) {
                Class<?> javaType = propertyResultMapping.getJavaType();
                TypeHandler<?> typeHandler = configuration.getTypeHandlerRegistry().getTypeHandler(javaType);
                sb.append(propertyResultMapping.getProperty());
                Object propertyValue = typeHandler.getResult(resultSet, propertyResultMapping.getColumn());
                sb.append(propertyValue);
            }
        }
        return sb.toString();
    }

    private static Object getRowValue(ResultMap resultMap, ResultSetWrapper firstResultSet, Configuration configuration, Object rowValue) throws Exception {
        // 獲取返回值的實(shí)體類(lèi)
        Object returnValue = null;
        // 如果不等于空說(shuō)明是處理合并,那么不構(gòu)建新對(duì)象,只在合并的對(duì)象上重新賦值。
        if (Objects.nonNull(rowValue)) {
            returnValue = rowValue;
        } else {
            // 等于空說(shuō)明是第一次進(jìn)入,直接構(gòu)建返回值示例。
            returnValue = configuration.getObjectFactory().create(resultMap.getType());
        }
        // 下面對(duì)實(shí)例方法進(jìn)行賦值,利用工具類(lèi)MetaObject包裝提供統(tǒng)一的賦屬性方法
        MetaObject metaObject = configuration.newMetaObject(returnValue);
        // 判斷是否是嵌套對(duì)象
        boolean nestedFlag = resultMap.hasNestedResultMaps();
        ResultSet resultSet = firstResultSet.getResultSet();
        // 判斷是否簡(jiǎn)單對(duì)象
        if (nestedFlag) {
            // 非簡(jiǎn)單對(duì)象,說(shuō)明需要判斷屬性各自需要的映射對(duì)象
            List<ResultMapping> propertyResultMappings = resultMap.getPropertyResultMappings();
            for (ResultMapping propertyResultMapping : propertyResultMappings) {
                Class<?> javaType = propertyResultMapping.getJavaType();
                String nestedResultMapId = propertyResultMapping.getNestedResultMapId();
                Object propertyValue;
                // 是空說(shuō)明,當(dāng)前屬性是基本屬性
                if (Objects.isNull(nestedResultMapId)) {
                    // 獲取當(dāng)前屬性的Java類(lèi)型,從配置中獲取該類(lèi)型,讀取ResultSet要使用的方法。eg:StringTypeHandler 使用ResultSet#getString
                    TypeHandler<?> typeHandler = configuration.getTypeHandlerRegistry().getTypeHandler(javaType);
                    propertyValue = typeHandler.getResult(resultSet, propertyResultMapping.getColumn());
                } else {
                    // 不等于空說(shuō)明是嵌套對(duì)象,從配置中讀取嵌套對(duì)象的映射信息
                    ResultMap nestedResultMap = configuration.getResultMap(nestedResultMapId);
                    // 嵌套對(duì)象的java類(lèi)型。eg: School(students),這里的Java類(lèi)型就是Student
                    Class<?> nestedJavaType = nestedResultMap.getType();
                    // 若果是list方式,外面的javaType=list,里面是真實(shí)java對(duì)象
                    if (!javaType.equals(nestedJavaType) && Collection.class.isAssignableFrom(javaType)) {
                        propertyValue = getRowValue(nestedResultMap, firstResultSet, configuration, null);
                        MetaObject parentMetaObject = configuration.newMetaObject(returnValue);
                        // 獲取父對(duì)象School 獲取students的List
                        Object collect = parentMetaObject.getValue(propertyResultMapping.getProperty());
                        if (Objects.isNull(collect)) {
                            // 如果是null,則將list實(shí)例化
                            collect = configuration.getObjectFactory().create(javaType);
                            parentMetaObject.setValue(propertyResultMapping.getProperty(), collect);
                        }
                        // 給list中添加信息
                        MetaObject metaCollectObject = configuration.newMetaObject(collect);
                        metaCollectObject.add(propertyValue);
                        propertyValue = collect;
                    } else {
                        // 簡(jiǎn)單對(duì)象
                        propertyValue = getRowValue(nestedResultMap, firstResultSet, configuration, null);
                    }
                }
                metaObject.setValue(propertyResultMapping.getProperty(), propertyValue);
            }
        } else {
            List<ResultMapping> propertyResultMappings = resultMap.getPropertyResultMappings();
            for (ResultMapping propertyResultMapping : propertyResultMappings) {
                Class<?> javaType = propertyResultMapping.getJavaType();
                TypeHandler<?> typeHandler = configuration.getTypeHandlerRegistry().getTypeHandler(javaType);
                Object propertyValue = typeHandler.getResult(resultSet, propertyResultMapping.getColumn());
                metaObject.setValue(propertyResultMapping.getProperty(), propertyValue);
            }
        }
        return returnValue;
    }

好了到這里對(duì)于場(chǎng)景1中,返回list中的數(shù)據(jù)就處理好了。

  private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
    List<E> result;
    Object param = method.convertArgsToSqlCommandParam(args);
    if (method.hasRowBounds()) {
      RowBounds rowBounds = method.extractRowBounds(args);
      result = sqlSession.selectList(command.getName(), param, rowBounds);
    } else {
      result = sqlSession.selectList(command.getName(), param);
    }
    // issue #510 Collections & arrays support
    if (!method.getReturnType().isAssignableFrom(result.getClass())) {
      if (method.getReturnType().isArray()) {
        return convertToArray(result);
      } else {
        return convertToDeclaredCollection(sqlSession.getConfiguration(), result);
      }
    }
    return result;
  }

場(chǎng)景二:

如果是單個(gè)對(duì)象,在基于場(chǎng)景一的返回值上加一個(gè)判斷,如果結(jié)果只要1個(gè)就只取第一個(gè)。
如果是多個(gè),則報(bào)錯(cuò)。

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

場(chǎng)景三:

更新語(yǔ)句直接 Statement#getUpdateCount 獲取更新數(shù)量

  public int update(Statement statement) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    ps.execute();
    int rows = ps.getUpdateCount();
    Object parameterObject = boundSql.getParameterObject();
    KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();
    keyGenerator.processAfter(executor, mappedStatement, ps, parameterObject);
    return rows;
  }

場(chǎng)景四:

排除查詢(xún),其他語(yǔ)句返回都是int類(lèi)型的更新成數(shù)量。那么假如方法是boolean類(lèi)型,或者Long和Void呢

public class MapperMethod {
  private Object rowCountResult(int rowCount) {
    final Object result;
    if (method.returnsVoid()) {
      result = null;
    } else if (Integer.class.equals(method.getReturnType()) || Integer.TYPE.equals(method.getReturnType())) {
      result = rowCount;
    } else if (Long.class.equals(method.getReturnType()) || Long.TYPE.equals(method.getReturnType())) {
      result = (long) rowCount;
    } else if (Boolean.class.equals(method.getReturnType()) || Boolean.TYPE.equals(method.getReturnType())) {
      result = rowCount > 0;
    } else {
      throw new BindingException("Mapper method '" + command.getName() + "' has an unsupported return type: " + method.getReturnType());
    }
    return result;
  }
}  

感謝您的閱讀,本文由 西魏陶淵明 版權(quán)所有。如若轉(zhuǎn)載,請(qǐng)注明出處:西魏陶淵明(https://blog.springlearn.cn/)

本文由mdnice多平臺(tái)發(fā)布

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容