1. 程式人生 > 實用技巧 >Mybatis的動態sql和StatementHandler

Mybatis的動態sql和StatementHandler

  一年前為了在公司刷積分還寫過關於mybatis的動態sql的原理,一年後就發現自己有點忘了,再寫一次加深印象

  一 初始化

  還是先從MapperStatement說起

  XMLMapperBuilder.buildStatementFromContext

private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    for (XNode context : list) {
      final XMLStatementBuilder statementParser = new
XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId); try { statementParser.parseStatementNode(); } catch (IncompleteElementException e) { configuration.addIncompleteStatement(statementParser); } } }

每個XNode都是一個select|update|delete|insert

的完整標籤

<select resultMap="BaseResultMap" parameterType="java.lang.String" id="selectByPrimaryKey">
<include refid="Base_Column_List"/>
</select>

  精簡了程式碼,第8行表示把mapper檔案中的一個select|update|delete|insert一種構造成一個MapperStatement並加入到configuration裡

 1 public void parseStatementNode() {
 2     
 3
... 4 // Parse the SQL (pre: <selectKey> and <include> were parsed and removed) 5 SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass); 6 ... 7 8 builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, 9 fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, 10 resultSetTypeEnum, flushCache, useCache, resultOrdered, 11 keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets); 12 }

  而本篇重點分析第5行,獲取SqlSource的過程

  現在有一個帶有動態標籤的sql語句

<insert parameterType="me.gacl.domain.User" id="insertSelective">
<trim suffixOverrides="," prefix="(" suffix=")">
<if test="userId != null">
        user_id,
      </if>
<if test="userName != null">
        user_name,
      </if>
<if test="userBirthday != null">
        user_birthday,
      </if>
<if test="userSalary != null">
        user_salary,
      </if>
</trim>
<trim suffixOverrides="," prefix="values (" suffix=")">
<if test="userId != null">
        #{userId,jdbcType=CHAR},
      </if>
<if test="userName != null">
        #{userName,jdbcType=VARCHAR},
      </if>
<if test="userBirthday != null">
        #{userBirthday,jdbcType=DATE},
      </if>
<if test="userSalary != null">
        #{userSalary,jdbcType=DOUBLE},
      </if>
</trim>
</insert>

  XMLScriptBuilder

public SqlSource parseScriptNode() {
    List<SqlNode> contents = parseDynamicTags(context);
    MixedSqlNode rootSqlNode = new MixedSqlNode(contents);
    SqlSource sqlSource = null;
    if (isDynamic) {
      sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {
      sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    return sqlSource;
  }

跟程式碼的結果就是 經過parseDynamicTags的處理後,List<SqlNode> contents的樣子

  

  

  再把這些SqlNode都匯聚成一個MixedSqlNode ,再通過MixedSqlNode構造成SqlSource

  總結,現在有了MappedStatement,在MappedStatement中有SqlSource,注意此時的SqlSource還是靜態的,為什麼呢?因為mybatis提供的動態sql功能,每次執行的sql是根據輸入的入參

  決定的,每次的sql是不同的。

  二 執行階段

  先說一下mybatis的執行過程吧,sqlSession -> executor -> statementHandler

  一個SqlSession有一個成員Executor,而每次呼叫executor的方法時,都是會新建一個StatementHandler。為啥會new出來一個StatementHandler呢?

  因為mybatis的動態sql是根據入參的不同,每次執行的sql也不一定相同,所以每當呼叫executor的方法就只能new一個StatementHandler

public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
    Statement stmt = null;
    try {
      Configuration configuration = ms.getConfiguration();
      StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
      stmt = prepareStatement(handler, ms.getStatementLog());
      return handler.update(stmt);
    } finally {
      closeStatement(stmt);
    }
  }

  那就來看看new一個statementHandler的邏輯,在new一個statementHandler的過程中就要獲取BoundSql

protected BaseStatementHandler(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;

  最終還是呼叫到DynamicSqlSource.public BoundSql getBoundSql(Object parameterObject)

 1 public BoundSql getBoundSql(Object parameterObject) {
 2     DynamicContext context = new DynamicContext(configuration, parameterObject);//parameterObject就是入參,也就是PO物件
 3     rootSqlNode.apply(context);//經過這一步之後,動態sql就會被確定下來了
 4     SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
 5     Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
 6     SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());//sqlSource就長下面圖片的樣子
 7     BoundSql boundSql = sqlSource.getBoundSql(parameterObject);//BoundSql就是在SqlSource的基礎上再加上實際的入參
 8     for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
 9       boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
10     }
11     return boundSql;
12   }

  

    SqlSource

  

  BoundSql

  總結

    configuration中會快取所有的MappedStatement,MappedStatement在解析mapper檔案的過程中就會保留一個SqlSource,而這個SqlSource裡有SqlNode的集合,對於一個Update標籤來說,它

  的每一個子節點比如if,都會形成一個SqlNode。這些資訊相當於是靜態的資訊,快取在MappedStatement中

    當要執行某一個方法的時候,每次Executor都會new一個StatementHandler,在new的過程中就會通過真實的入參將這些SqlNode依次解析,最終構成本次要執行的SqlSource,把SqlSource和入參合並變成BoundSql交給StatementHandler,準備執行