Mybatis sql解析過程


一、Mybatis解析sql的時機

Mybatis對於用戶在XXMapper.xml文件中配置的sql解析主要分為2個時機

靜態sql:程序啟動的時候解析

動態sql:用戶進行查詢等sql相關操作的時候解析

二、靜態sql、動態sql

1、什么是靜態sql,動態sql?

如果select|insert|update|delete標簽體內包含XML標簽或者select|insert|update|delete標簽體內的sql文本中包含${}參數占位符則為動態sql,否則為靜態sql。

如下面的2個sql中,第一個為動態sql,第二個為靜態sql

<select id="selectUser" parameterType="com.fit.bean.User" resultType="com.fit.bean.User" useCache="true">
    select id, name from tab_user where id = ${id}
    <if test="name!=null and name!=''">
    and name=#{name}
    </if>
    and 1 = 1
</select>
 
<select id="selectUserById" parameterType="int" resultType="com.fit.bean.User" useCache="true">
    select id, name from tab_user where id = #{id}
</select>
2、靜態sql和動態sql的選擇

由於靜態sql是在應用啟動的時候就解析,而動態sql是在執行該sql相關操作的時候才根據傳入的參數進行解析的,所以靜態sql效率會比動態sql好。

Static SqlSource is faster than DynamicSqlSource because mappings are calculated during startup.

PS:此處只針對常見的Mybatis的sql腳本寫法,通過<script></script>傳入sql執行的方式暫不討論。

三、sql解析過程

先看一下Mybatis的sql解析過程涉及到下面的幾個主要對象(關鍵類:MappedStatement、SqlSource、BoundSql)

 

其中DynamicSqlSource的解析過程涉及到動態sql節點(關鍵類:SqlNode)的解析,涉及到的類(以if標簽為例,只畫了解析過程中的幾個主要的類,SqlNode的其他子類如ChooseSqlNode、ForEachSqlNode、TrimSqlNode、WhereSqlNode等沒有畫出來)如下

 

先用一個圖表示解析結果如下:


再結合源碼看下解析過程,Myabatis解析每一個select|insert|update|delete標簽體成一個MappedStatement對象,里面保存了一個SqlSource對象的引用。

通過XMLStatementBuilder類的parseStatementNode方法解析xml

Mybatis解析select|insert|update|delete標簽體內配置的sql是通過XMLScriptBuilder類的parseScriptNode方法實現,

public SqlSource parseScriptNode() {
  //解析select|insert|update|delete標簽體,生成一系列的SqlNode
  List<SqlNode> contents = parseDynamicTags(context);
  //混合的SqlNode,其實就是保存了一個List<SqlNode>類型的屬性
  MixedSqlNode rootSqlNode = new MixedSqlNode(contents);
  SqlSource sqlSource = null;
  if (isDynamic) {
    //動態SqlSource
    sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
  } else {
    //靜態SqlSource
    sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
  }
  return sqlSource;
}
首先看下parseDynamicTags方法

List<SqlNode> parseDynamicTags(XNode node) {
  List<SqlNode> contents = new ArrayList<SqlNode>();
  NodeList children = node.getNode().getChildNodes();
  for (int i = 0; i < children.getLength(); i++) {
    XNode child = node.newXNode(children.item(i));
    if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
      String data = child.getStringBody("");
      //TextSqlNode解析判斷是否是動態sql
      TextSqlNode textSqlNode = new TextSqlNode(data);
      if (textSqlNode.isDynamic()) {
        contents.add(textSqlNode);
        isDynamic = true;
      } else {
        contents.add(new StaticTextSqlNode(data));
      }
    } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
      String nodeName = child.getNode().getNodeName();
      //不同的標簽用對應的NodeHandler處理
      NodeHandler handler = nodeHandlers(nodeName);
      if (handler == null) {
        throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
      }
     //如果還有動態的標簽,遞歸調用parseDynamicTags
      handler.handleNode(child, contents);
      isDynamic = true;
    }
  }
  return contents;
}

它的作用就是把select|insert|update|delete標簽體解析成一個個的SqlNode節點,並判斷出該標簽是靜態sql還是動態sql,如果是動態的生成DynamicSqlSource,如果是靜態sql,就生成RawSqlSource。而RawSqlSource就是包裝了一個StaticSqlSource,可以看下RawSqlSource構造方法的實現:
public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class<?> parameterType) {
    this(configuration, getSql(configuration, rootSqlNode), parameterType);
  }
 
 
  public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class<?> clazz = parameterType == null ? Object.class : parameterType;
    //解析其中的#{},替換成預編譯sql中的? 並保存參數映射
    sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<String, Object>());
  }
 
 
  private static String getSql(Configuration configuration, SqlNode rootSqlNode) {
    DynamicContext context = new DynamicContext(configuration, null);
    //StaticTextSqlNode的apply方法就是把append靜態sql文本
    rootSqlNode.apply(context);
    return context.getSql();
  }
1、靜態sql解析

靜態sql的解析就是替換sql文本中的#{}參數成?,即生成最終可以預編譯的sql,並把參數相關信息保存成ParameterMapping,包括參數名,數據類型,以及根據數據類型獲取對應的TypeHandler。

TypeHanler的作用就是在執行預編譯sql的時候設置參數值,決定參數設值是用prepareStatement.setInt()還是prepareStatement.setString()等。

2、動態sql解析

動態sql的解析是在執行db操作的調用MappedStatement方法的getBoundSql方式時進行解析的

public BoundSql getBoundSql(Object parameterObject) {
//SqlSource中生成BoundSql,如果是DynamicSqlSource則借助ognl根據入參替換${}成參數值
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings == null || parameterMappings.isEmpty()) {
  boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject);
}
 
 
// check for nested result maps in parameter mappings (issue #30)
for (ParameterMapping pm : boundSql.getParameterMappings()) {
  String rmId = pm.getResultMapId();
  if (rmId != null) {
    ResultMap rm = configuration.getResultMap(rmId);
    if (rm != null) {
      hasNestedResultMaps |= rm.hasNestedResultMaps();
    }
  }
}
return boundSql
我們看下<if></if>標簽的解析

<if test="name!=null and name!=''">
and name=#{name}
</if>
它會被解析成IfSqlNode
public class IfSqlNode implements SqlNode {
  private ExpressionEvaluator evaluator;
  private String test;
  private SqlNode contents;
 
  public IfSqlNode(SqlNode contents, String test) {
    this.test = test;
    this.contents = contents;
    this.evaluator = new ExpressionEvaluator();
  }
 
  @Override
  public boolean apply(DynamicContext context) {
    if (evaluator.evaluateBoolean(test, context.getBindings())) {
      contents.apply(context);
      return true;
    }
    return false;
  }
 
}
它的apply方法就是根據入參計算name!=null and name!=''表達式的值,如果是true,則調用if標簽體內的sqlNode的apply方法,and name=#{name}是StaticTextSqlNode,則替換#{name}成?后直接append,if標簽中計算表達式的值借助了ognl來實現。
ognl是對象圖導航語言,主要作用就是根據參數名直接取對象/級聯對象的屬性值,它也可以計算ognl表達式的值,如上面的name!=null and name!=''表達式。

最終DynamicSqlSource會被解析成只包含#{}的StaticSqlSource,靜態SqlSource再獲取可以直接預編譯的sql。

四、sql執行

執行查詢的時候真正調用的是SimpleExecutor的doQuery方法

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();
    //根據sql標簽配置的StatementType生成對應的StatementHandler,不配置的話默認是PreparedStatementHandler,即執行預編譯sql。
    StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
    //預編譯sql,並且給參數賦值,即根據解析的ParameterMapping一個一個進行參數設值
    stmt = prepareStatement(handler, ms.getStatementLog());
    //執行查詢、並解析結果集返回
    return handler.<E>query(stmt, resultHandler);
  } finally {
    closeStatement(stmt);
  }
}
prepareStatement方法就是預編譯sql,對不同的參數根據類型調用不同的TypeHandler進行preparestatement設值。

private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    Connection connection = getConnection(statementLog);
    stmt = handler.prepare(connection);
    handler.parameterize(stmt);
    return stmt;
  }
sql解析的大概流程就是這樣


五、#{}和${}的區別

所有的#{}標簽都會替換成?,而${}在sql解析的過程中會根據參數使用ognl直接替換成對應的參數值,如果參數中name是"jack",則sql中會直接替換成name=jack,sql執行會報錯。
如果傳入的參數是基本數據類型,則參數占位符不能用${},因為ognl取參數值的時候會對傳入的參數調用占位符中對應的屬性,導致基本數據類型不可能有該屬性而報錯。如果sql只想傳一個參數又是基本數據類型用#{}。
如果User對象的id為int類型,id值為0,ognl對user對象進行表達式id!=null and id!=''計算的時候會返回false,if便簽里面的sql就不會被append。所以基本數據類型int不要用 !=''做判斷
如果標簽中指定StatementType="STATEMENT",sql標簽體內包含#{},會被解析成?而不進行參數設值,sql執行報錯,默認StatementType="PREPARED"
#{}可以防止sql注入,也就是預編譯sql的好處

————————————————
版權聲明:本文為CSDN博主「bootstrap8」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/u012387062/article/details/55005414


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM