mybatis中#{}與${}的區別詳解

mybatis中#{}與${}的區別詳解

版本

此處分析基於mybatis-3.4.6完成。

介紹-猜想

網上的很多資料都表示,#{}表達式寫入參數時將表達式替換爲?,而${}表達式寫入參數時是直接寫入。本來以爲#{}利用的是jdbc中PreparedStatement的方式,而${}是直接使用Statement,其實不然。開發同學都知道PreparedStatement其預編譯的特性可以在操作大量SQL時有顯著的性能提升,並且可以防止SQL注入安全問題。Statement相比更加簡單,少了預編譯和SQL注入安全防範,因此在少量的SQL執行上,其性能要更高,只是這點性能一般來說忽略不計了,因此開發中往往都是隻會使用#{}。實際官網上也表明了,#{}使用PreparedStatement操作,但是沒明確說${}使用的Statement,只是表示${}表達式中的參數會被直接替換,下圖爲截選自mybatis官網。

在這裏插入圖片描述

在這裏插入圖片描述

驗證-源碼分析

回到主題,猜想了#{}與${}的區別後,現在開始從源碼方面驗證一下。

更新流程分析階段

要了解#{}與${}的區別需要知道mybatis的初始化與SQL的執行階段邏輯,下面會簡單介紹一下。

mybatis的初始化入口如下:

SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

下面是mybatis的更新執行流程圖:

在這裏插入圖片描述

可以從流程圖中發現,mybatis的更新操作在Executor#doUpdate()處執行。

public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
        Statement stmt = null;

        int var6;
        try {
            Configuration configuration = ms.getConfiguration();
            StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, (ResultHandler)null, (BoundSql)null);
            stmt = this.prepareStatement(handler, ms.getStatementLog());
            var6 = handler.update(stmt);
        } finally {
            this.closeStatement(stmt);
        }

        return var6;
    }

跟蹤源碼可以發現,在Executor執行操作時,SQL已經被初始化了(#{}表達式標識的參數已經被替換爲了?),而往上跟蹤可以發現初始化操作在SQLSessionFactory初始化階段,那麼回到初始化階段。

mybatis初始化階段
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);

var5 = this.build(parser.parse());

在初始化時,mybatis會parse我們的配置文件和mapper文件。XmlConfigBuilder#mapperElement()做映射構建、XMLMapperBuilder#parse()解析mapper、#configurationElement()配置mapper元素、#buildStatementFromContext()配置select|insert|update|delete語句、XmlStatementBuilder#parseStatementNode()語句構建,直到開始解析SQL語句。

//解析語句(select|insert|update|delete)
//<select
//  id="selectPerson"
//  parameterType="int"
//  parameterMap="deprecated"
//  resultType="hashmap"
//  resultMap="personResultMap"
//  flushCache="false"
//  useCache="true"
//  timeout="10000"
//  fetchSize="256"
//  statementType="PREPARED"
//  resultSetType="FORWARD_ONLY">
//  SELECT * FROM PERSON WHERE ID = #{id}
//</select>
public void parseStatementNode() {
    //...忽略一系列邏輯
    //官網可以查到,這裏的langDriver默認爲:XMLLanguageDriver。
    LanguageDriver langDriver = getLanguageDriver(lang);

    //注意,此處爲顯示指定PreParedStatement、Statement或者是CallableStatement的地方,默認情況下爲PreparedStatement。
    StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    
    //Mapper中的SQL映射初始化,#{}表達式被替換爲?,${}表達式不變化
    // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
  }

那麼接下來進入到XmlLanguageDriver中,XMLScriptBuilder#parseScriptNode(),通過調用parseDynamicTags() 來根據當前xml的tag拿到childTag(也就是select中包含的SQL語句),通過isDynamic方法來判斷。跟蹤到isDynamic方法中可以看到,new了一個GenericTokenParser默認以${}解析方式,而判斷在parse方法中,以String.indexOf判斷拿到的SQL語句中是否存在${,最後確認是否爲DynamicSqlSource(#{})或者RawSqlSource(${}),而如果是RawSQLSource,則在初始化中通過SqlSourceBuilder#parse()中,將SQL參數替換爲了?。

//isDynamic的判斷
public boolean isDynamic() {
    DynamicCheckerTokenParser checker = new DynamicCheckerTokenParser();
    GenericTokenParser parser = createParser(checker);
    parser.parse(text);
    return checker.isDynamic();
 }
//非isDynamic情況下,#{}解析。
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
    ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
    GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
    String sql = parser.parse(originalSql);
    return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
  }
更新操作執行階段

在SQLSource初始化完畢後,#{}的方法會替換爲"?",而${}的方式SQL語句不變,在後面具體執行時纔會動態設置參數。這個結果可以在SQLSessionFactory.configuration.mappedStatements中看到。

再次來到Executor#doUpdate()#prepareStatement(),在這裏#{}設置參數的核心方法入口爲DefaultParameterHandler -> setParameters,其實就是原生jdbc中PreparedStatement的setParameter。

/**
 * BaseTypeHandler的抽象方法,其中在此處可以看到,mybatis對不同的類型封裝了不同的typeHandler來做。
*/
public void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
    //...省略部分邏輯
      try {
        setNonNullParameter(ps, i, parameter, jdbcType);
      } catch (Exception e) {
        throw new TypeException("Error setting non null for parameter #" + i + " with JdbcType " + jdbcType + " . " +
                "Try setting a different JdbcType for this parameter or a different configuration property. " +
                "Cause: " + e, e);
      }
    }
  }

而關於${}設置參數,在parameterize()會發現其已經設置完成了,那麼回到構建doUpdate()方法中。在這裏newStatementHandler()完成的statementHandler的構建與SQL參數的注入。

public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    //可以看到其new了一個RoutingStatementHandler
    StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
    statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
    return statementHandler;
  }

RoutingStatementHandler的構造:

public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
	//這裏的statementType爲Prepared,這個在SQLSessionFactory中完成初始化
    switch (ms.getStatementType()) {
      case STATEMENT:
        delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      case PREPARED:
        delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      case CALLABLE:
        delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      default:
        throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
    }
  }

往下跟蹤會發現其調用super的構造器,因此來到BaseStatementHandler:

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();

    //前面在doUpdate中,boundSQL傳入的爲null,因此進入getBoundSql中
    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);
  }

mappedStatement.getBoundSql代碼如下:

public BoundSql getBoundSql(Object parameterObject) {
    //SQL的初始化邏輯入口,這裏的SQLSource爲DynamicSqlSource,這是在mybatis初始化時構建的,回想一下isDynamic就明白了。
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    //...忽略其他邏輯

    return boundSql;
  }

DynamicSqlSource#getBoundSql代碼如下:

public BoundSql getBoundSql(Object parameterObject) {
    DynamicContext context = new DynamicContext(configuration, parameterObject);
    //這裏就是初始化的方法了
    rootSqlNode.apply(context);
    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);
    for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
      boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
    }
    return boundSql;
  }

結論

自此,#{}與${}的分析完畢,可以發現#{}與${}的具體操作都是通過PreparedStatement來執行的,只是#{}與${}的參數注入上,一個是動態注入,一個是靜態注入。具體Statement的類型由開發者手動配置,默認情況下爲PreparedStatement。但是要注意,如果使用#{}表達式是不能配置statementType爲:Statement的,這裏個prepareStatement()方法中就能體現出來,最終結果會去執行一個帶有?的SQL語句導致語法錯誤。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章