Mybatis3.4.x技術內幕(十八):Mybatis之動態Sql設計本來(下)

上一篇博文中,簡要介紹了Mybatis動態sql的基本用法和基本設計結構,本篇博文重點闡述一些動態sql的技術細節,#{name}和${name}的區別,將在本篇博文中揭曉。也許讀者早已瞭解它們之間的區別,可是,做爲技術內幕,咱們不只要了解它們的區別,還要介紹它們的工做原理,是否是很開森呢?java

1. #{name}和${name}的區別。

#{name}:表示這是一個參數(ParameterMapping)佔位符,值來自於運行時傳遞給sql的參數,也就是XXXMapper.xml裏的parameterType。其值經過PreparedStatement的setObject()等方法賦值。node

動態sql中的<bind>標籤綁定的值,也是使用#{name}來使用的。mysql

#{name}用在sql文本中。sql

${name}:表示這是一個屬性配置佔位符,值來自於屬性配置文件,好比jdbc.properties,其值經過相似replace方法進行靜態替換。好比${driver},將被靜態替換爲com.mysql.jdbc.Driver。apache

${name}則能夠用在xml的Attribute屬性,還能夠用在sql文本當中。網絡

<select id="countAll" resultType="${driver}">
		select count(1) from (
			select 
			stud_id as studId
			, name, email
			, dob
			, phone
		from students #{offset}, ${driver}
		) tmp 
	</select>

2. ${name}的工做原理

org.apache.ibatis.builder.xml.XMLStatementBuilder.parseStatementNode()部分源碼。app

public void parseStatementNode() {
//...
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    includeParser.applyIncludes(context.getNode());
// ...
}

org.apache.ibatis.builder.xml.XMLIncludeTransformer.applyIncludes(Node, Properties)部分源碼。ide

private void applyIncludes(Node source, final Properties variablesContext) {
    if (source.getNodeName().equals("include")) {
      // new full context for included SQL - contains inherited context and new variables from current include node
      Properties fullContext;

      String refid = getStringAttribute(source, "refid");
      // replace variables in include refid value
      refid = PropertyParser.parse(refid, variablesContext);
      Node toInclude = findSqlFragment(refid);
      Properties newVariablesContext = getVariablesContext(source, variablesContext);
      if (!newVariablesContext.isEmpty()) {
        // merge contexts
        fullContext = new Properties();
        fullContext.putAll(variablesContext);
        fullContext.putAll(newVariablesContext);
      } else {
        // no new context - use inherited fully
        fullContext = variablesContext;
      }
      applyIncludes(toInclude, fullContext);
      if (toInclude.getOwnerDocument() != source.getOwnerDocument()) {
        toInclude = source.getOwnerDocument().importNode(toInclude, true);
      }
      source.getParentNode().replaceChild(toInclude, source);
      while (toInclude.hasChildNodes()) {
        toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);
      }
      toInclude.getParentNode().removeChild(toInclude);
    } else if (source.getNodeType() == Node.ELEMENT_NODE) {
      NodeList children = source.getChildNodes();
      for (int i=0; i<children.getLength(); i++) {
        applyIncludes(children.item(i), variablesContext);
      }
    } else if (source.getNodeType() == Node.ATTRIBUTE_NODE && !variablesContext.isEmpty()) {
      // replace variables in all attribute values
      // 經過PropertyParser替換全部${xxx}佔位符(attribute屬性)
      source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
    } else if (source.getNodeType() == Node.TEXT_NODE && !variablesContext.isEmpty()) {
      // replace variables ins all text nodes
      // 經過PropertyParser替換全部${xxx}佔位符(文本節點)
      source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
    }
  }

也就是說,Mybatis在解析<include>標籤時,就已經靜態替換${name}佔位符了。工具

public class PropertyParser {

  private PropertyParser() {
    // Prevent Instantiation
  }

  public static String parse(String string, Properties variables) {
    VariableTokenHandler handler = new VariableTokenHandler(variables);
    GenericTokenParser parser = new GenericTokenParser("${", "}", handler);
    return parser.parse(string);
  }

  private static class VariableTokenHandler implements TokenHandler {
    private Properties variables;

    public VariableTokenHandler(Properties variables) {
      this.variables = variables;
    }

    @Override
    public String handleToken(String content) {
      if (variables != null && variables.containsKey(content)) {
        return variables.getProperty(content);
      }
      return "${" + content + "}";
    }
  }
}

3. #{name}的工做原理

#{name}是ParameterMapping參數佔位符,Mybatis將會把#{name}替換爲?號,並經過OGNL來計算#{xxx}內部的OGNL表達式的值,做爲PreparedStatement的setObject()的參數值。ui

舉例:#{item.name}將被替換爲sql的?號佔位符,item.name則是OGNL表達式,OGNL將計算item.name的值,做爲sql的?號佔位符的值。

若是隻有靜態sql,#{name}將在解析xml文件時,完成替換爲?佔位符。若是有動態sql的內容,#{name}將在執行sql時,動態替換爲?佔位符。

org.apache.ibatis.scripting.xmltags.XMLScriptBuilder.parseScriptNode()。

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;
  }
public class RawSqlSource implements SqlSource {

  private final SqlSource sqlSource;

  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;
    // 在這裏完成#{xxx}替換爲?號
    sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<String, Object>());
  }

  private static String getSql(Configuration configuration, SqlNode rootSqlNode) {
    DynamicContext context = new DynamicContext(configuration, null);
    // 建立RawSqlSource時,就完成sql的拼接工做,由於它沒有動態sql的內容,Mybatis初始化時,就能肯定最終的sql。
    rootSqlNode.apply(context);
    return context.getSql();
  }

  @Override
  public BoundSql getBoundSql(Object parameterObject) {
    return sqlSource.getBoundSql(parameterObject);
  }

}

org.apache.ibatis.builder.SqlSourceBuilder.parse(String, Class<?>, Map<String, Object>)。

public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
    //  使用ParameterMappingTokenHandler策略來處理#{xxx}
    ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
    GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
    String sql = parser.parse(originalSql);
    return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
  }

GenericTokenParser.java是通用解析佔位符的工具類,它能夠解析${name}和#{name},那麼,解析到${name}和#{name}後,要如何處理這樣的佔位符,則由不一樣的策略TokenHandler來完成。

4. TokenHandler

GenericTokenParser.java負責解析sql中的佔位符${name}和#{name},TokenHandler則是如何處理這些佔位符。

ParameterMappingTokenHandler:處理#{xxx}佔位符。

private static class ParameterMappingTokenHandler extends BaseBuilder implements TokenHandler {

    private List<ParameterMapping> parameterMappings = new ArrayList<ParameterMapping>();
    private Class<?> parameterType;
    private MetaObject metaParameters;

    public ParameterMappingTokenHandler(Configuration configuration, Class<?> parameterType, Map<String, Object> additionalParameters) {
      super(configuration);
      this.parameterType = parameterType;
      this.metaParameters = configuration.newMetaObject(additionalParameters);
    }

    public List<ParameterMapping> getParameterMappings() {
      return parameterMappings;
    }

    @Override
    public String handleToken(String content) {
      // 建立一個ParameterMapping對象,並返回?號佔位符
      parameterMappings.add(buildParameterMapping(content));
      return "?";
    }
//..
}

VariableTokenHandler:處理${xxx}佔位符。

private static class VariableTokenHandler implements TokenHandler {
    private Properties variables;

    public VariableTokenHandler(Properties variables) {
      this.variables = variables;
    }

    @Override
    public String handleToken(String content) {
      if (variables != null && variables.containsKey(content)) {
        return variables.getProperty(content);
      }
      return "${" + content + "}";
    }
  }

DynamicCheckerTokenParser:空實現,動態sql標籤,都由它來標識。

BindingTokenParser:用於在註解Annotation中處理${xxx},待研究。

至此,${name}將直接替換爲靜態Properties的靜態屬性值,而#{name}將被替換爲?號,並同時建立了ParameterMapping對象,綁定到參數列表中。

5. DynamicSqlSource生成sql的原理

對於RawSqlSource,因爲是靜態的sql,Mybatis初始化時就生成了最終能夠直接使用的sql語句,即在建立RawSqlSource時,就直接生成。而DynamicSqlSource,則是執行sql時,才動態生成。

public class DynamicSqlSource implements SqlSource {

  private Configuration configuration;
  private 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);
    // 逐一調用各類SqlNode,拼接sql
    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;
  }

}

BoundSql不只保存了最終的可執行的sql,還保存了sql中?號佔位符的參數列表。

public class BoundSql {

  private String sql;
  private List<ParameterMapping> parameterMappings;
// ...
}

最後,在執行sql時,經過org.apache.ibatis.scripting.defaults.DefaultParameterHandler.setParameters(PreparedStatement)方法,遍歷List<ParameterMapping> parameterMappings = boundSql.getParameterMappings()來逐一對sql中的?號佔位符進行賦值操做。

整個sql處理變量佔位符的流程就完成了。

6. OGNL表達式運算完成動態sql拼接

咱們就舉一個略微複雜一點的ForEachSqlNode的拼接sql原理。

public class ForEachSqlNode implements SqlNode {
// OGNL表達式計算器
private ExpressionEvaluator evaluator;
//...
@Override
  public boolean apply(DynamicContext context) {
    Map<String, Object> bindings = context.getBindings();
    // 計算集合表達式
    final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings);
    if (!iterable.iterator().hasNext()) {
      return true;
    }
    boolean first = true;
    applyOpen(context);
    int i = 0;
    // 遍歷拼接sql
    for (Object o : iterable) {
      DynamicContext oldContext = context;
      if (first) {
        context = new PrefixedContext(context, "");
      } else if (separator != null) {
        context = new PrefixedContext(context, separator);
      } else {
          context = new PrefixedContext(context, "");
      }
      int uniqueNumber = context.getUniqueNumber();
      // Issue #709 
      if (o instanceof Map.Entry) {
        @SuppressWarnings("unchecked") 
        Map.Entry<Object, Object> mapEntry = (Map.Entry<Object, Object>) o;
        applyIndex(context, mapEntry.getKey(), uniqueNumber);
        applyItem(context, mapEntry.getValue(), uniqueNumber);
      } else {
        applyIndex(context, i, uniqueNumber);
        applyItem(context, o, uniqueNumber);
      }
      contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber));
      if (first) {
        first = !((PrefixedContext) context).isPrefixApplied();
      }
      context = oldContext;
      i++;
    }
    applyClose(context);
    return true;
  }
//...
}

Mybatis的所有動態sql內容,至此就所有介紹完了,在實際工做中,絕大多數的sql,都是動態sql。

最後,慶祝中國女排里約奧運奪冠。

版權提示:文章出自開源中國社區,若對文章感興趣,可關注個人開源中國社區博客(http://my.oschina.net/zudajun)。(通過網絡爬蟲或轉載的文章,常常丟失流程圖、時序圖,格式錯亂等,仍是看原版的比較好)

相關文章
相關標籤/搜索