mybatis源碼學習(四):動態SQL的解析


之前的一片文章中我們已經了解了MappedStatement中有一個SqlSource字段,而SqlSource又有一個getBoundSql方法來獲得BoundSql對象。而BoundSql中的sql字段表示了綁定的SQL語句

而且我們也已經了解過了SqlSource中的靜態SQL的解析過程(RawSqlSource),這次我們來了解下動態SQL的解析過程。

動態SQL對應的SqlSource實現主要是DynamicSqlSource:

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

}

我們知道無論RawSqlSource還是DynamicSqlSource,都會將getBoundSql的方法委托給內部的StaticSqlSource對象。

但是對比RawSqlSource和DynamicSqlSource的字段值,我們可以很直觀的發現RawSqlSource直接有一個SqlSource屬性,構造函數中通過configuration和SqlNode直接解析SqlSource對象,

而DynamicSqlSource相反,他沒有SqlSource屬性,反而是保留了configuration和SqlNode作為屬性,只有在getBoundSql時,才會去創建SqlSource對象。

這正是因為動態Sql的sqlsource是無法直接確定的,需要在運行時根據條件才能確定。

所以,對於動態SQL的解析其實是分為兩階段的:

1.解析XML資源:之前的解析過程都類似(可參考前一篇文章),XMLScriptBuilder會將XML中的節點解析成各個類型的SqlNode,然后封裝成MixedSqlNode,它和Configuration對象一起作為參數,創建DynamicSqlSource對象。

2.執行SQL:SQL的執行過程我也在之前的文章中介紹過了,我們知道在Executor在執行SQL時,會通過MappedStatement對象獲取BoundSql對象,而文章一開始我們已經說了MappedStatement對象是把這一操作委托給SqlSource。因此,這時候DynamicSqlSource才會真的執行getBoundSql方法,解析得到BoundSql對象。

 

介紹了大概的過程,我們通過一些簡單的示例來更清晰的認識這一過程。

 測試用的Mapper.xml:

  <!DOCTYPE mapper
    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="org.apache.ibatis.domain.blog.mappers.AuthorMapper">

    <select id="selectAuthorWithInlineParams" parameterType="int"
        resultType="org.apache.ibatis.domain.blog.Author">
        select * from author where id = ${id}
    </select>

</mapper>

我們可以看到SQL語句中簡單的包含了${}。因此它會被判定為動態SQL。

測試代碼:

 public void dynamicParse() throws Exception{
     //階段一:啟動時
    String resource = "org/apache/ibatis/builder/MapperConfig.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

    //階段二:執行時
    SqlSession sqlSession = sqlSessionFactory.openSession();
    AuthorMapper mapper = sqlSession.getMapper(AuthorMapper.class);
    mapper.selectAuthorWithInlineParams(1);
  }

我已經在代碼中注釋出了兩個階段,正是之前我們介紹的兩個階段。

Mybatis相關的配置文件相對簡單,對數據源做了配置並引入了相應的mapper文件,不再貼出。

AuthorMapper類也比較簡單,同樣不貼出。

接下來讓我們跟着斷點,來具體看一下動態節點的解析。

為了直觀起見,除了會對代碼做一些說明外,我還會在代碼右側注釋出一些關鍵對象的信息。

 

階段一中有部分過程之前的解析過程和靜態SQL的解析過程是一致的,因此我們從XMLLanguageDriver開始,

  //XNode對象是XNode對象是xml中由XPathParser對象解析出來的節點對象,parameterType是要傳入SQL的參數類型
  public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {//script:<select resultType="org.apache.ibatis.domain.blog.Author" parameterType="int" id="selectAuthorWithInlineParams">select * from author where id = ${id}</select>

      //創建XMLScriptBuilder對象,通過它創建SqlSource對象,建造者模式的應用。
    XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
    return builder.parseScriptNode();
  }

接着看XMLScriptBuilder.parseScrpitNode方法:

  public SqlSource parseScriptNode() {
      //解析XNode成一系列SqlNode對象,並封裝成MixedSqlNode對象,並會判斷此SQL是否為動態
    MixedSqlNode rootSqlNode = parseDynamicTags(context);
    SqlSource sqlSource = null;
    if (isDynamic) {//動態SQL則創建DynamicSqlSource
      sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {//靜態SQL則創建RawSqlSource
      sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    return sqlSource;
  }

我們看看真正的解析過程parseDynamicTags:

  //他會解析XNode成一系列SqlNode對象,並封裝成MixedSqlNode對象
  protected MixedSqlNode parseDynamicTags(XNode node) {
    List<SqlNode> contents = new ArrayList<SqlNode>();
    //獲取當前XNode下的子節點,並遍歷解析子節點
    NodeList children = node.getNode().getChildNodes();
    for (int i = 0; i < children.getLength(); i++) {
        //子節點,本例中只有一個子節點,為一個文本節點
      XNode child = node.newXNode(children.item(i));//child:<#text>select * from author where id = ${id}</#text>
      //如果是文本節點,則解析成TextSqlNode
      if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
        String data = child.getStringBody("");
        TextSqlNode textSqlNode = new TextSqlNode(data);
        //判斷文本中是否含有${},如果有則是動態SQL
        if (textSqlNode.isDynamic()) {
          contents.add(textSqlNode);
          isDynamic = true;
        } else {
          contents.add(new StaticTextSqlNode(data));
        }
      } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { //其他節點類型將被解析成對應的SqlNode類型,
        String nodeName = child.getNode().getNodeName();
        NodeHandler handler = nodeHandlerMap.get(nodeName);
        if (handler == null) {
          throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
        }
        handler.handleNode(child, contents);
        //此類節點都是動態的
        isDynamic = true;
      }
    }
    //將解析得到的SqlNode,封裝成MixedSqlNode對象
    return new MixedSqlNode(contents);
  }

上面的過程和解析靜態SQL的過程有一個不同的地方是靜態SQL會再根據TextSqlNode的文本創建出StaticTextSqlNode。而動態SQL對於文本節點,仍然使用TextSqlNode。

再回到之前的parseScrpitNode方法中,它根據解析結果創建了一個DynamicSqlSource對象,它保存了解析過程所得的Configuration對象和MixedSqlNode對象。

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

這樣我們就解析得到了DynamicSqlSource對象,即完成了動態SQL解析的階段一的過程。

 

接下來我們來看階段二的過程,前面的過程也不再贅述,直接看SqlSource調用getBoundSql方法時:

  @Override
  public BoundSql getBoundSql(Object parameterObject) {//參數對象:{"id"->1 ; "param1" -> 1}
      //傳入configuration和運行時的參數,創建DynamicContext對象
    DynamicContext context = new DynamicContext(configuration, parameterObject);
    //應用每個SqlNode,拼接Sql片段,這里只替換動態部分
    rootSqlNode.apply(context);//此時context的sqlBuilder已經被解析成了:select * from author where id = 1
    //繼續解析SQL,將#{}替換成?
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
    SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
    //創建BoundSql對象
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
      boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
    }
    return boundSql;
  }

先了解一下DynamicContext對象,可以將它理解為Sql片段的一個容器,用於之后拼接出Sql。同時它還有一個bindings屬性,可以用來保存運行信息,比如綁定的參數,數據庫ID等:

public class DynamicContext {

  public static final String PARAMETER_OBJECT_KEY = "_parameter";
  public static final String DATABASE_ID_KEY = "_databaseId";

  static {
    OgnlRuntime.setPropertyAccessor(ContextMap.class, new ContextAccessor());
  }

  //用來保存運行時上下文參數
  private final ContextMap bindings;
  //用來拼接SQL片段
  private final StringBuilder sqlBuilder = new StringBuilder(); 
  private int uniqueNumber = 0;

  public DynamicContext(Configuration configuration, Object parameterObject) {
    if (parameterObject != null && !(parameterObject instanceof Map)) {
      MetaObject metaObject = configuration.newMetaObject(parameterObject);
      bindings = new ContextMap(metaObject);
    } else {
      bindings = new ContextMap(null);
    }
    bindings.put(PARAMETER_OBJECT_KEY, parameterObject);
    bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId());
  }

  public Map<String, Object> getBindings() {
    return bindings;
  }

  public void bind(String name, Object value) {
    bindings.put(name, value);
  }

  public void appendSql(String sql) {
    sqlBuilder.append(sql);
    sqlBuilder.append(" ");
  }

  public String getSql() {
    return sqlBuilder.toString().trim();
  }

  public int getUniqueNumber() {
    return uniqueNumber++;
  }

  static class ContextMap extends HashMap<String, Object> {
    private static final long serialVersionUID = 2977601501966151582L;

    private MetaObject parameterMetaObject;
    public ContextMap(MetaObject parameterMetaObject) {
      this.parameterMetaObject = parameterMetaObject;
    }

    @Override
    public Object get(Object key) {
      String strKey = (String) key;
      if (super.containsKey(strKey)) {
        return super.get(strKey);
      }

      if (parameterMetaObject != null) {
        // issue #61 do not modify the context when reading
        return parameterMetaObject.getValue(strKey);
      }

      return null;
    }
  }

  static class ContextAccessor implements PropertyAccessor {

    @Override
    public Object getProperty(Map context, Object target, Object name)
        throws OgnlException {
      Map map = (Map) target;

      Object result = map.get(name);
      if (map.containsKey(name) || result != null) {
        return result;
      }

      Object parameterObject = map.get(PARAMETER_OBJECT_KEY);
      if (parameterObject instanceof Map) {
        return ((Map)parameterObject).get(name);
      }

      return null;
    }

    @Override
    public void setProperty(Map context, Object target, Object name, Object value)
        throws OgnlException {
      Map<Object, Object> map = (Map<Object, Object>) target;
      map.put(name, value);
    }

    @Override
    public String getSourceAccessor(OgnlContext arg0, Object arg1, Object arg2) {
      return null;
    }

    @Override
    public String getSourceSetter(OgnlContext arg0, Object arg1, Object arg2) {
      return null;
    }
  }
}
View Code

至此,我們就獲得了BoundSql的對象,之后的過程就和靜態SQL的使用過程是一致的。

 


免責聲明!

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



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