写点什么

java 培训 Mybatis 动态 Sql 处理解析

作者:@零度
  • 2022 年 4 月 18 日
  • 本文字数:5415 字

    阅读完需:约 18 分钟

以下文章来源于架构师必备

动态 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 用递归调用来进行解析。这块东西个人觉得还是比较绕,所以这块博主准备事例、源码、执行结果一起讲解_java培训

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、where、trim 等。具体自己去 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<String, Object> 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 映射。

用户头像

@零度

关注

关注尚硅谷,轻松学IT 2021.11.23 加入

IT培训 www.atguigu.com

评论

发布
暂无评论
java培训Mybatis动态Sql处理解析_sql_@零度_InfoQ写作平台