zoukankan      html  css  js  c++  java
  • 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的使用过程是一致的。

  • 相关阅读:
    Java分层经验
    appium自动化测试之H5页面测试
    appium自动化测试日志收集-logging
    mysql查询实践题
    python每日一练之单元测试
    python每日一练之I/O实现读写csv文件
    python每日一练之读写txt文件
    selenium定位元素后,click不生效的问题。
    selenium自动化测试之浏览器滚动条
    selenium自动化测试之单选、下拉列表、alert弹窗处理、页面刷新
  • 原文地址:https://www.cnblogs.com/insaneXs/p/9230264.html
Copyright © 2011-2022 走看看