之前的一片文章中我们已经了解了MappedStatement中有一个SqlSource字段,而SqlSource又有一个getBoundSql方法来获得BoundSql对象。而BoundSql中的sql字段表示了绑定的SQL语句

而且我们也已经了解过了SqlSource中的静态SQL的解析过程(RawSqlSource),这次我们来了解下动态SQL的解析过程。

动态SQL对应的SqlSource实现主要是DynamicSqlSource:

  1. public class DynamicSqlSource implements SqlSource {
  2.  
  3. private final Configuration configuration;
  4. private final SqlNode rootSqlNode;
  5.  
  6. public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
  7. this.configuration = configuration;
  8. this.rootSqlNode = rootSqlNode;
  9. }
  10.  
  11. @Override
  12. public BoundSql getBoundSql(Object parameterObject) {
  13. DynamicContext context = new DynamicContext(configuration, parameterObject);
  14. rootSqlNode.apply(context);
  15. SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
  16. Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
  17. SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
  18. BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
  19. for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
  20. boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
  21. }
  22. return boundSql;
  23. }
  24.  
  25. }

我们知道无论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:

  1. <!DOCTYPE mapper
  2. PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  3. "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
  4.  
  5. <mapper namespace="org.apache.ibatis.domain.blog.mappers.AuthorMapper">
  6.  
  7. <select id="selectAuthorWithInlineParams" parameterType="int"
  8. resultType="org.apache.ibatis.domain.blog.Author">
  9. select * from author where id = ${id}
  10. </select>
  11.  
  12. </mapper>

我们可以看到SQL语句中简单的包含了${}。因此它会被判定为动态SQL。

测试代码:

  1. public void dynamicParse() throws Exception{
  2. //阶段一:启动时
  3. String resource = "org/apache/ibatis/builder/MapperConfig.xml";
  4. InputStream inputStream = Resources.getResourceAsStream(resource);
  5. SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
  6.  
  7. //阶段二:执行时
  8. SqlSession sqlSession = sqlSessionFactory.openSession();
  9. AuthorMapper mapper = sqlSession.getMapper(AuthorMapper.class);
  10. mapper.selectAuthorWithInlineParams(1);
  11. }

我已经在代码中注释出了两个阶段,正是之前我们介绍的两个阶段。

Mybatis相关的配置文件相对简单,对数据源做了配置并引入了相应的mapper文件,不再贴出。

AuthorMapper类也比较简单,同样不贴出。

接下来让我们跟着断点,来具体看一下动态节点的解析。

为了直观起见,除了会对代码做一些说明外,我还会在代码右侧注释出一些关键对象的信息。

阶段一中有部分过程之前的解析过程和静态SQL的解析过程是一致的,因此我们从XMLLanguageDriver开始,

  1. //XNode对象是XNode对象是xml中由XPathParser对象解析出来的节点对象,parameterType是要传入SQL的参数类型
  2. 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>
  3.  
  4. //创建XMLScriptBuilder对象,通过它创建SqlSource对象,建造者模式的应用。
  5. XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
  6. return builder.parseScriptNode();
  7. }

接着看XMLScriptBuilder.parseScrpitNode方法:

  1. public SqlSource parseScriptNode() {
  2. //解析XNode成一系列SqlNode对象,并封装成MixedSqlNode对象,并会判断此SQL是否为动态
  3. MixedSqlNode rootSqlNode = parseDynamicTags(context);
  4. SqlSource sqlSource = null;
  5. if (isDynamic) {//动态SQL则创建DynamicSqlSource
  6. sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
  7. } else {//静态SQL则创建RawSqlSource
  8. sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
  9. }
  10. return sqlSource;
  11. }

我们看看真正的解析过程parseDynamicTags:

  1. //他会解析XNode成一系列SqlNode对象,并封装成MixedSqlNode对象
  2. protected MixedSqlNode parseDynamicTags(XNode node) {
  3. List<SqlNode> contents = new ArrayList<SqlNode>();
  4. //获取当前XNode下的子节点,并遍历解析子节点
  5. NodeList children = node.getNode().getChildNodes();
  6. for (int i = 0; i < children.getLength(); i++) {
  7. //子节点,本例中只有一个子节点,为一个文本节点
  8. XNode child = node.newXNode(children.item(i));//child:<#text>select * from author where id = ${id}</#text>
  9. //如果是文本节点,则解析成TextSqlNode
  10. if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
  11. String data = child.getStringBody("");
  12. TextSqlNode textSqlNode = new TextSqlNode(data);
  13. //判断文本中是否含有${},如果有则是动态SQL
  14. if (textSqlNode.isDynamic()) {
  15. contents.add(textSqlNode);
  16. isDynamic = true;
  17. } else {
  18. contents.add(new StaticTextSqlNode(data));
  19. }
  20. } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { //其他节点类型将被解析成对应的SqlNode类型,
  21. String nodeName = child.getNode().getNodeName();
  22. NodeHandler handler = nodeHandlerMap.get(nodeName);
  23. if (handler == null) {
  24. throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
  25. }
  26. handler.handleNode(child, contents);
  27. //此类节点都是动态的
  28. isDynamic = true;
  29. }
  30. }
  31. //将解析得到的SqlNode,封装成MixedSqlNode对象
  32. return new MixedSqlNode(contents);
  33. }

上面的过程和解析静态SQL的过程有一个不同的地方是静态SQL会再根据TextSqlNode的文本创建出StaticTextSqlNode。而动态SQL对于文本节点,仍然使用TextSqlNode。

再回到之前的parseScrpitNode方法中,它根据解析结果创建了一个DynamicSqlSource对象,它保存了解析过程所得的Configuration对象和MixedSqlNode对象。

  1. public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
  2. this.configuration = configuration;
  3. this.rootSqlNode = rootSqlNode;
  4. }

这样我们就解析得到了DynamicSqlSource对象,即完成了动态SQL解析的阶段一的过程。

接下来我们来看阶段二的过程,前面的过程也不再赘述,直接看SqlSource调用getBoundSql方法时:

  1. @Override
  2. public BoundSql getBoundSql(Object parameterObject) {//参数对象:{"id"->1 ; "param1" -> 1}
  3. //传入configuration和运行时的参数,创建DynamicContext对象
  4. DynamicContext context = new DynamicContext(configuration, parameterObject);
  5. //应用每个SqlNode,拼接Sql片段,这里只替换动态部分
  6. rootSqlNode.apply(context);//此时context的sqlBuilder已经被解析成了:select * from author where id = 1
  7. //继续解析SQL,将#{}替换成?
  8. SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
  9. Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
  10. SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
  11. //创建BoundSql对象
  12. BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
  13. for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
  14. boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
  15. }
  16. return boundSql;
  17. }

先了解一下DynamicContext对象,可以将它理解为Sql片段的一个容器,用于之后拼接出Sql。同时它还有一个bindings属性,可以用来保存运行信息,比如绑定的参数,数据库ID等:

  1. public class DynamicContext {
  2.  
  3. public static final String PARAMETER_OBJECT_KEY = "_parameter";
  4. public static final String DATABASE_ID_KEY = "_databaseId";
  5.  
  6. static {
  7. OgnlRuntime.setPropertyAccessor(ContextMap.class, new ContextAccessor());
  8. }
  9.  
  10. //用来保存运行时上下文参数
  11. private final ContextMap bindings;
  12. //用来拼接SQL片段
  13. private final StringBuilder sqlBuilder = new StringBuilder();
  14. private int uniqueNumber = 0;
  15.  
  16. public DynamicContext(Configuration configuration, Object parameterObject) {
  17. if (parameterObject != null && !(parameterObject instanceof Map)) {
  18. MetaObject metaObject = configuration.newMetaObject(parameterObject);
  19. bindings = new ContextMap(metaObject);
  20. } else {
  21. bindings = new ContextMap(null);
  22. }
  23. bindings.put(PARAMETER_OBJECT_KEY, parameterObject);
  24. bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId());
  25. }
  26.  
  27. public Map<String, Object> getBindings() {
  28. return bindings;
  29. }
  30.  
  31. public void bind(String name, Object value) {
  32. bindings.put(name, value);
  33. }
  34.  
  35. public void appendSql(String sql) {
  36. sqlBuilder.append(sql);
  37. sqlBuilder.append(" ");
  38. }
  39.  
  40. public String getSql() {
  41. return sqlBuilder.toString().trim();
  42. }
  43.  
  44. public int getUniqueNumber() {
  45. return uniqueNumber++;
  46. }
  47.  
  48. static class ContextMap extends HashMap<String, Object> {
  49. private static final long serialVersionUID = 2977601501966151582L;
  50.  
  51. private MetaObject parameterMetaObject;
  52. public ContextMap(MetaObject parameterMetaObject) {
  53. this.parameterMetaObject = parameterMetaObject;
  54. }
  55.  
  56. @Override
  57. public Object get(Object key) {
  58. String strKey = (String) key;
  59. if (super.containsKey(strKey)) {
  60. return super.get(strKey);
  61. }
  62.  
  63. if (parameterMetaObject != null) {
  64. // issue #61 do not modify the context when reading
  65. return parameterMetaObject.getValue(strKey);
  66. }
  67.  
  68. return null;
  69. }
  70. }
  71.  
  72. static class ContextAccessor implements PropertyAccessor {
  73.  
  74. @Override
  75. public Object getProperty(Map context, Object target, Object name)
  76. throws OgnlException {
  77. Map map = (Map) target;
  78.  
  79. Object result = map.get(name);
  80. if (map.containsKey(name) || result != null) {
  81. return result;
  82. }
  83.  
  84. Object parameterObject = map.get(PARAMETER_OBJECT_KEY);
  85. if (parameterObject instanceof Map) {
  86. return ((Map)parameterObject).get(name);
  87. }
  88.  
  89. return null;
  90. }
  91.  
  92. @Override
  93. public void setProperty(Map context, Object target, Object name, Object value)
  94. throws OgnlException {
  95. Map<Object, Object> map = (Map<Object, Object>) target;
  96. map.put(name, value);
  97. }
  98.  
  99. @Override
  100. public String getSourceAccessor(OgnlContext arg0, Object arg1, Object arg2) {
  101. return null;
  102. }
  103.  
  104. @Override
  105. public String getSourceSetter(OgnlContext arg0, Object arg1, Object arg2) {
  106. return null;
  107. }
  108. }
  109. }

至此,我们就获得了BoundSql的对象,之后的过程就和静态SQL的使用过程是一致的。

mybatis源码学习(四):动态SQL的解析的更多相关文章

  1. mybatis源码学习(四)--springboot整合mybatis原理

    我们接下来说:springboot是如何和mybatis进行整合的 1.首先,springboot中使用mybatis需要用到mybatis-spring-boot-start,可以理解为mybati ...

  2. Mybatis源码学习之parsing包(解析器)(二)

    简述 大家都知道mybatis中,无论是配置文件mybatis-config.xml,还是SQL语句,都是写在XML文件中的,那么mybatis是如何解析这些XML文件呢?这就是本文将要学习的就是,m ...

  3. mybatis源码学习: 动态代理的应用(慢慢改)

    动态代理概述 在学spring的时候知道使用动态代理实现aop,入门的列子:需要计算所有方法的调用时间.可以每个方法开始和结束都获取当前时间咋办呢.类似这样: long current=system. ...

  4. mybatis源码学习:基于动态代理实现查询全过程

    前文传送门: mybatis源码学习:从SqlSessionFactory到代理对象的生成 mybatis源码学习:一级缓存和二级缓存分析 下面这条语句,将会调用代理对象的方法,并执行查询过程,我们一 ...

  5. mybatis源码学习(一) 原生mybatis源码学习

    最近这一周,主要在学习mybatis相关的源码,所以记录一下吧,算是一点学习心得 个人觉得,mybatis的源码,大致可以分为两部分,一是原生的mybatis,二是和spring整合之后的mybati ...

  6. mybatis源码学习:一级缓存和二级缓存分析

    目录 零.一级缓存和二级缓存的流程 一级缓存总结 二级缓存总结 一.缓存接口Cache及其实现类 二.cache标签解析源码 三.CacheKey缓存项的key 四.二级缓存TransactionCa ...

  7. mybatis源码学习:插件定义+执行流程责任链

    目录 一.自定义插件流程 二.测试插件 三.源码分析 1.inteceptor在Configuration中的注册 2.基于责任链的设计模式 3.基于动态代理的plugin 4.拦截方法的interc ...

  8. Mybatis源码学习第六天(核心流程分析)之Executor分析

    今Executor这个类,Mybatis虽然表面是SqlSession做的增删改查,其实底层统一调用的是Executor这个接口 在这里贴一下Mybatis查询体系结构图 Executor组件分析 E ...

  9. Spring mybatis源码学习指引目录

    前言: 分析了很多方面的mybatis的源码以及与spring结合的源码,但是难免出现错综的现象,为了使源码陶冶更为有序化.清晰化,特作此随笔归纳下分析过的内容.博主也为mybatis官方提供过pul ...

随机推荐

  1. MFC之使用blat发送邮件

    blat的下载地址:http://www.blat.net 我用它进行了smtp服务的邮件发送.这里我使用的qq邮箱,qq邮箱使用的密码是授权码,可以再qq邮箱设置里面开启smtp服务.下载下来是文件 ...

  2. 多线程之旅(Task 任务)

    一.Task(任务)和ThreadPool(线程池)不同       源码 1.线程(Thread)是创建并发工具的底层类,但是在前几篇文章中我们介绍了Thread的特点,和实例.可以很明显发现局限性 ...

  3. 前端面试题解密:经典算法之冒泡算法(ES6版)及优化

    前言 随着前端的飞速发展,前端业务开发给前端工程师提出了更高的要求,因而算法题也越来越高频次的出现在前端面试中.有很多的小伙伴找胡哥苦诉,在前端实际开发中(除了涉及游戏开发方面),算法使用有很多吗?大 ...

  4. PHP获取所有扩展及扩展下的所有函数签名生成php.snippet

    <?php $ext_info = array(); $modules = get_loaded_extensions(); foreach ($modules as $module) { $f ...

  5. 打开scratch后蓝屏怎么办

    1.试试开机,百出完电脑品牌后,按F8,安全模式,光标选定:最后一次正确配置,回车,回车,按下去,[度关键一步]2.再不行,问进安全模式,回车,到桌面后,用杀毒软件腾讯电脑管家,全盘杀毒,“隔离区”的 ...

  6. 用curl调用https接口

    今天在windows下用curl类获取微信token一直返回false,查阅资料后,发现是https证书的锅,在curl类中加上这两条,问题瞬间解决. curl_setopt($ch, CURLOPT ...

  7. Tkinter布局管理器

    Layout management in Tkinter 原英文教程地址:zetcode.com In this part of the Tkinter tutorial, we introduce ...

  8. MTK Android Git提取出两个版本之间的差异文件并打包

    git提取出两个版本之间的差异文件并打包 首先你得知道版本之间的commit id git log –pretty=oneline $ git log --pretty=oneline 1 差异文件并 ...

  9. wireshark抓包实战(五),首选项设置和基本的抓包设置

    一.首选项 首选项一般是修改软件底层的一些默认参数 选中编辑,点击首选项按钮 二.抓包选项设置 点击捕获,选中选项 1.捕获网卡设置 2.保存文件方式设置 很多情况下wireshark会保存很大的数据 ...

  10. C++ memset函数用法

    #include<stdio.h>#include<string.h>int main(){ char buffer[] = "I love you!"; ...