Mybatis的動態sql和StatementHandler
一年前為了在公司刷積分還寫過關於mybatis的動態sql的原理,一年後就發現自己有點忘了,再寫一次加深印象
一 初始化
還是先從MapperStatement說起
XMLMapperBuilder.buildStatementFromContext
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) { for (XNode context : list) { final XMLStatementBuilder statementParser = newXMLStatementBuilder(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,準備執行