文档章节

Mybatis3.4.x技术内幕(十八):Mybatis之动态Sql设计原本(下)

祖大俊
 祖大俊
发布于 2016/08/21 13:08
字数 1756
阅读 1349
收藏 10

上一篇博文中,简要介绍了Mybatis动态sql的基本用法和基本设计结构,本篇博文重点阐述一些动态sql的技术细节,#{name}和${name}的区别,将在本篇博文中揭晓。也许读者早已了解它们之间的区别,但是,作为技术内幕,我们不仅要了解它们的区别,还要介绍它们的工作原理,是不是很开森呢?

1. #{name}和${name}的区别。

#{name}:表示这是一个参数(ParameterMapping)占位符,值来自于运行时传递给sql的参数,也就是XXXMapper.xml里的parameterType。其值通过PreparedStatement的setObject()等方法赋值。

动态sql中的<bind>标签绑定的值,也是使用#{name}来使用的。

#{name}用在sql文本中。

${name}:表示这是一个属性配置占位符,值来自于属性配置文件,比如jdbc.properties,其值通过类似replace方法进行静态替换。比如${driver},将被静态替换为com.mysql.jdbc.Driver。

${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()部分源码。

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

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

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()的参数值。

举例:#{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)。(经过网络爬虫或转载的文章,经常丢失流程图、时序图,格式错乱等,还是看原版的比较好)

© 著作权归作者所有

祖大俊
粉丝 797
博文 32
码字总数 52477
作品 0
昌平
私信 提问
加载中

评论(1)

messi_10
messi_10
关键在最后-女排夺冠了!
Mybatis3.4.x技术内幕(十七):Mybatis之动态Sql设计原本(上)

上一篇博文中,介绍了可复用的sql片段,通过<include>标签进行引入,而<include>标签内一般存放的是静态sql,其实,sql片段也是可以放置动态sql标签内容。 1. Mybatis支持的动态sql及基本用法...

祖大俊
2016/08/20
3.1K
2
Mybatis3.4.x技术内幕(二十三):Mybatis面试问题集锦(大结局)

Mybatis技术内幕系列博客,从原理和源码角度,介绍了其内部实现细节,无论是写的好与不好,我确实是用心写了,由于并不是介绍如何使用Mybatis的文章,所以,一些参数使用细节略掉了,我们的目...

祖大俊
2016/09/17
15.2K
36
Mybatis3.4.x技术内幕(十九):Mybatis之plugin插件设计原理

大多数框架,都支持插件,用户可通过编写插件来自行扩展功能,Mybatis也不例外。 我们从插件配置、插件编写、插件运行原理、插件注册与执行拦截的时机、初始化插件、分页插件的原理等六个方面...

祖大俊
2016/08/28
3.9K
3
Mybatis3.4.x技术内幕(十六):Mybatis之sqlFragment(可复用的sql片段)

Mybatis目前最新版本为3.4.0,因此,我也将我的项目由3.3.1替换为3.4.0。在上一篇博文中,详细分析了Mybatis在使用foreach循环进行批量insert,返回主键id列表时,如果使用BatchExecutor,那...

祖大俊
2016/06/05
2.1K
0
Mybatis3.4.x技术内幕(二十):PageHelper分页插件源码及原理剖析

PageHelper是一款好用的开源免费的Mybatis第三方物理分页插件,其实我并不想加上好用两个字,但是为了表扬插件作者开源免费的崇高精神,我毫不犹豫的加上了好用一词作为赞美。 原本以为分页插...

祖大俊
2016/09/10
8.9K
6

没有更多内容

加载失败,请刷新页面

加载更多

IDEA Maven project: 'xxx/pom.xml' already exists in VFS

Failed to create a Maven project: ‘xxx/pom.xml‘ already exists in VFS idea创建项目后,发现项目有问题,删除后重新创建,提示错误如下。 解决办法 1.通过idea打开任意一个项目 2.File...

国产大熊猫
53分钟前
7
0
Flutter之 State 生命周期

State 的生命周期,指的是在用户参与的情况下,其关联的 Widget 所经历的,从创建到显示,再到更新最后到停止,直至销毁等各个阶段 不同的阶段涉及到特定的任务处理 State 的生命周期流程如下...

過愙
59分钟前
11
0
零基础一年拿下BAT三家offer

背景 1、本人本科一本双非垫底的那种,硕士211。本硕电子通信,完全0基础,转行一年。 2、研一上第一学期上课+外派到老师合作公司写MATLAB。去年4月开始学习Java。 起步 1、实话说,刚决定转...

gzc426
今天
6
0
并行和并发的区别

并行和并发的区别 并行(Parallel)的重点在于同一时间点多个任务同时进行; 并发(Concurrent)的重点在于同一时间段有多个任务执行,不要求某个时间点有多个任务同时进行。...

StupidZhe
今天
38
0
java内存泄漏问题

所谓内存泄露就是指一个不再被程序使用的对象或变量一直被占据在内存中。Java中有垃圾回收机制,它可以保证一对象不再被引用的时候,即对象编程了孤儿的时候,对象将自动被垃圾回收器从内存中...

Bb进阶
今天
10
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部