查看原文
其他

MyBatis动态SQL处理

SpringForAll 2022-07-06
关注我,回复关键字“spring”
免费领取Spring学习资料

来源:https://blog.csdn.net/landywu1985

动态Sql介绍

动态 SQL 是 MyBatis 的强大特性之一。如果你使用过 JDBC 或其它类似的框架,你应该能理解根据不同条件拼接 SQL 语句有多痛苦,例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。利用动态 SQL,可以彻底摆脱这种痛苦。

使用动态 SQL 并非一件易事,但借助可用于任何 SQL 映射语句中的强大的动态 SQL 语言,MyBatis 显著地提升了这一特性的易用性。

Mybatis动态解析里面有2个核心的类SqlNode、SqlSource、ExpressionEvaluator。Mybatis动态Sql使用分为2个部分:动态Sql解析、动态Sql拼接执行。

封装SqlNode

SqlNode是在解析Xml文件的时候对动态Sql进行解析,并存在MappedStatement的sqlSource属性中。对于嵌套动态Sql,mybatis用递归调用来进行解析。这块东西个人觉得还是比较绕,所以这块博主准备事例、源码、执行结果一起讲解。

Sql脚本分类

在Mybatis中Sql脚本分为2种类型:静态Sql和动态Sql。下面我们通过具体的源码来看下2者区分。

静态Sql和动态Sql

静态Sql说白了就没有太任何判断了解的Sql脚本。

// Select 是查询的一些属性
<select id="selectBypageTwo" resultType="com.wwl.mybatis.dao.User">
       //这条查询语句select * from user where id > #{user.id}就是Mybatis中的静态Sql
       //静态Sql就是不太任何条件的Sql语句
       select * from user where id > #{ user.id}
       //这里有if判断条件,Mybatis把带有判断条件的Sql叫动态Sql
       //动态Sql除了if之外还有foreach、wheretrim等。具体自己去mybatis官网看下
        <if test="user.name != null and user.name!=''">
            AND name = #{ user.name}
        </if>
   </select>
SqlNode类结果体系


看mybatis代码很多时候可以看到这种结构。每个SqlNode负责自己那块功能。职责单一。SqlNode的核心方法apply就是通过ExpressionEvaluator来解析OGNL表达式数据的。接下来我们看看Mybatis是如何递归解析动态sql脚本的。

// 解析Sql脚本节点
  public SqlSource parseScriptNode() 
    //解析静态和动态脚本,并存在MixedSqlNode里面
    //这行代码很关键,后面我们会去分析parseDynamicTags这里就是一层一层递归调用该方法把Sql脚本生成MixedSqlNode对象。
    MixedSqlNode rootSqlNode = parseDynamicTags(context);
    SqlSource sqlSource = null;
    //是否为动态Sql
    if (isDynamic) { 
      //动态Sql则生成DynamicSqlSource
      sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else { 
      //否则为静态SqlSource
      sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    return sqlSource;
  }
// An highlighted block
protected MixedSqlNode parseDynamicTags(XNode node) 
    //创建个SqlNode,这个列表存了当前Sql脚本节点下的所有的SqlNode信息
    List<SqlNode> contents = new ArrayList<SqlNode>();
    NodeList children = node.getNode().getChildNodes();
    for (int i = 0; i < children.getLength(); i++) { 
      XNode child = node.newXNode(children.item(i));
      //判断子元素或属性中的文本内容 || 子元素文档中的 CDATA 部(不会由解析器解析的文本)
      if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) { 
        String data = child.getStringBody("");
        //解析data
        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) {  // issue #628
        //获取元素的名字
        String nodeName = child.getNode().getNodeName();
        //根据元素名获取到元素节点的处理器,Mybatis提供了8中元素处理器,ChooseHandler、IfHandler、OtherwiseHandler
        //TrimHandler、BindHandler、WhereHandler、SetHandler、ForEachHandler。博主会给大家分析下IfHandler
        NodeHandler handler = nodeHandlerMap.get(nodeName);
        if (handler == null) { 
          throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
        }
        //调用对应的handler进行节点处理,递归调用就在这块
        handler.handleNode(child, contents);
        isDynamic = true;
      }
    }
    //创建MixedSqlNode
    return new MixedSqlNode(contents);
  }
// 下面我们看下IfHandler是如何处理,IfHandler是XMLScriptBuilder的内部类
private class IfHandler implements NodeHandler 
    public IfHandler() 
      // Prevent Synthetic Access
    }
    //我们着重分析这个方法
    @Override
    public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) 
      //调用parseDynamicTags进行节点解析。这里就是递归,又调用了上面的方法。
      MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
      //获取if对应的表达式
      String test = nodeToHandle.getStringAttribute("test");
      //创建IfSqlNode
      IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);
      targetContents.add(ifSqlNode);
    }
  }

下面我们根据Sql脚本和执行结果来分析。

// 静态Sql脚本和嵌套的动态Sql脚本
<select id="selectBypageTwo" resultType="com.wwl.mybatis.dao.User">
       select * from user where id > #{ user.id}
        <if test="user.name != null and user.name!=''">
            AND name = #{ user.name}
            <if test="user.name != null and user.name!=''">
                AND name = #{ user.name}
                <if test="user.name != null and user.name!=''">
                    AND name = #{ user.name}
                </if>
            </if>
        </if>
   </select>

下面我们分析下执行结果:

上面递归结果已经用不通颜色标记了,大家自己看下。特别需要看下IfSqlNode的属性。

动态Sql解析

动态Sql解析主要是执行数据库操作的时候把动态Sql转换成JDBC能识别的Sql脚本。Mybatis中主要是通过SqlSource来解析Sql脚本,替换成JDBC能识别的Sql脚本。我们先看下类图。

SqlSource:提供了Sql解析的行为。
RawSqlSource:静态Sql脚本的编译,只生成一次StaticSqlSource。
DynamicSqlSource:每次调用都会生成StaticSqlSource。每次调用传入参数可能不一样。需要每次生成StaticSqlSource。
ProviderSqlSource:第三方脚本语言的集成。
FreeMarkerSqlSource:对FreeMarker的支持。
StaticSqlSource:StaticSqlSource只是对上面4中类型做了层封装。博主没有这个类会更清爽些。
我们这次主要对StaticSqlSource、RawSqlSource、和DynamicSqlSource进行分析。

StaticSqlSource

其实StaticSqlSource就是对其他几种类型Sql处理器结果进行包装。我们看下源码。

//我们主要分析下getBoundSql
public class StaticSqlSource implements SqlSource 

  private final String sql;
  private final List<ParameterMapping> parameterMappings;
  private final Configuration configuration;

  public StaticSqlSource(Configuration configuration, String sql) 
    this(configuration, sql, null);
  }

  public StaticSqlSource(Configuration configuration, String sql, List<ParameterMapping> parameterMappings) 
    this.sql = sql;
    this.parameterMappings = parameterMappings;
    this.configuration = configuration;
  }

  //getBoundSql就是创建一个BoundSql对象。
  @Override
  public BoundSql getBoundSql(Object parameterObject) 
    return new BoundSql(configuration, sql, parameterMappings, parameterObject);
  }

}

看完是不是非常简单,其实有些代码确实没有我们想象中那么难。

RawSqlSource

// 我们着重分析RawSqlSource方法
public class RawSqlSource implements SqlSource 

  private final SqlSource sqlSource;

  public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class<?> parameterType) 
    this(configuration, getSql(configuration, rootSqlNode), parameterType);
  }
  //这里实现了对静态脚本的解析,所谓的静态脚本解析就是把 #{}解析成?静态Sql解析是在解析Mapper.xml的时候执行的
  public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) 
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class<?> clazz = parameterType == null ? Object.class : parameterType;
    //通过调用SqlSourceBuilder的parse方法来解析Sql
    sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<String, Object>());
  }

  private static String getSql(Configuration configuration, SqlNode rootSqlNode) 
    DynamicContext context = new DynamicContext(configuration, null);
    rootSqlNode.apply(context);
    return context.getSql();
  }

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

}

下面我们来看下SqlSourceBuilder的parse方法

public SqlSource parse(String originalSql, Class<?> parameterType, Map<StringObject> additionalParameters) { 
    ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
    //找到Sql脚本中#{}符号的脚本用?号进行替代。GenericTokenParser里面代码比较复杂,博主也没有研究。
    //有兴趣自己可以研究下。
    GenericTokenParser parser = new GenericTokenParser("#{""}", handler);
    String sql = parser.parse(originalSql);
    return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
  }

DynamicSqlSource

动态Sql解析主要由DynamicSqlSource来完成。这里面又是通过递归调进行sql解析。我们还是延用上面的Sql给大家讲解。

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) 
    //动态Sql解析上下文
    DynamicContext context = new DynamicContext(configuration, parameterObject);
    //rootSqlNode就是我们前面讲解的,把动态Sql解析成SqlNode对象。外层为MixedSqlNode节点,节点存储了
    //节点下的所有子节点。里面递归调用并根据传入参数的属性检查是否需要拼接sql
    rootSqlNode.apply(context);
    //这块代码和上面静态Sql接代码一致。
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
    //把我们动态Sql中的#{}替换成?
    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;
  }

}

动态Sql解析apply方法博主只根据场景介绍下MixedSqlNode和IfSqlNode的apply方法。其他有兴趣自己去研究下。逻辑大体一致,实现有些区别。

public class MixedSqlNode implements SqlNode 
  private final List<SqlNode> contents;

  public MixedSqlNode(List<SqlNode> contents) 
    this.contents = contents;
  }

 //获取循环SqlNode列表的所有SqlNode,调用apply方法根据传入参数和条件进行静态sql的拼接。
 //列表中的SqlNode可能是一个简单的SqlNode对象,也可能是一个MixedSqlNode或者有更多的嵌套。
 //博主的例子就是3个嵌套If查询。根据博主的Sql脚本,这里直接会调用IfSqlNode的apply方法。
 //我们接下来看下IfSqlNode是如何实现的。
  @Override
  public boolean apply(DynamicContext context) 
    for (SqlNode sqlNode : contents) { 
      sqlNode.apply(context);
    }
    return true;
  }
}

IfSqlNode的apply

public class IfSqlNode implements SqlNode 
  //ExpressionEvaluator会调用ognl来对表达式进行解析 
  private final ExpressionEvaluator evaluator;
  private final String test;
  private final SqlNode contents;

  public IfSqlNode(SqlNode contents, String test) 
    this.test = test;
    this.contents = contents;
    this.evaluator = new ExpressionEvaluator();
  }

  @Override
  public boolean apply(DynamicContext context) 
    //context.getBindings()里面就存储这请求参数,这里是一个HashMap,OGNl里面代码博主没有研究。
    //如果条件if成立,直接获取contents中的SqlNode的apply方法进行动态脚本处理。
    if (evaluator.evaluateBoolean(test, context.getBindings())) { 
      contents.apply(context);
      return true;
    }
    return false;
  }

}

这块代码很多递归调用,博主自认为讲的不太透彻,所以大家看完务必自己去调试下。

总结

Mybatis动态Sql从解析到执行分为2个过程下面对这个2个过程进行简单总结。
1.动态Sql生成SqlNode信息,这个过程发生在对select、update等Sql语句解析过程。如果是静态Sql直接会把#{}替换成?。
2.动态Sql解析在获取BoundSql时候触发。会调用SqlNode的apply进行Sql解析成静态Sql,然后把#{}替换成?,并绑定ParameterMapping映射。


END



API开放接⼝设计之appId,appSecret,accessToken
Spring Cloud Alibaba Nacos配置的加载规则详解
spring-configuration-metadata.json文件是做啥的?
Spring Cloud Alibaba基础教程:使用Nacos作为配置中心
Spring Boot 实现读写分离,还有谁不会??

关注后端面试那些事,回复【2022面经】

获取最新大厂Java面经


最后重要提示:高质量的技术交流群,限时免费开放,今年抱团最重要。想进群的,关注SpringForAll社区,回复关键词:加群,拉你进群。

点击“阅读原文”领取2022大厂面经
↓↓↓ 

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存