为了账号安全,请及时绑定邮箱和手机立即绑定

MyBatis 基石之 SqlNode

标签:
Java

简介

平时我们基于 MyBaits 框架进行编写的 Mapper.xml 中每一个 insert/update/delete/select 标签里面的每一行 SQL(包括 include 标签被替换成 SQL ) 文本被抽象为 SqlNode。

SqlNode 分类

  1. StaticTextSqlNode:纯 SQL 语句和 #{} 占位符,不包含任何动态 SQL 语句(包含 ${} 占位符 )
  2. TextSqlNode: SQL 语句中含有 ${} 占位符;
  3. IfSqlNode:if/when 子标签里面的 SQL 语句;
  4. ChooseSqlNode:choose 子标签里面的 SQL 语句;
  5. ForEachSqlNode:foreach 子标签里面的 SQL 语句;
  6. VarDecSqlNode:bind 子标签里面的 SQL 语句;
  7. TrimSqlNode:trim 子标签里面的 SQL 语句;
  8. WhereSqlNode:where 子标签里面的 SQL 语句;
  9. SetSqlNode:set 子标签里面的 SQL 语句;
  10. MixedSqlNode: 如果 insert/update/delete/select 标签的 SQL 文本不止一行,则把所有的 SqlNode 组装在一起的 SqlNode。

类图

在这里插入图片描述
SqlNode 接口只定义了一个 boolean apply(DynamicContext context) 方法,通过 DynamicContext 对象把各个 SqlNode 组装成一条完整的 SQL 语句。

DynamicContext

在这里插入图片描述
DynamicContext 就像上图串串的竹签,而 SqlNode 就是竹签上一块块肉肉,一个竹签上的所有肉肉就是 MixedSqlNode,通过竹签把肉肉串在一起,就组成了美味的烧烤——SQL!!烧烤怎么少了佐料,就如 SQL 语句怎么少了参数呢?参数保存在 DynamicContext 中 bindings 字段中。通过 getSql() 方法获取 StringJoiner 拼接 SQL 语句。

源码解读

StaticTextSqlNode

由于不包含任何动态 SQL 所以不依赖实参来拼接 SQL 语句

示例

public class StaticTextSqlNodeDemo {
    public static void main(String[] args) {
        Configuration configuration = new Configuration();
        SqlNode staticTextSqlNode = new StaticTextSqlNode("SELECT * FROM user ");
        DynamicContext dynamicContext = new DynamicContext(configuration, null);
        staticTextSqlNode.apply(dynamicContext);
        String sql = dynamicContext.getSql();
        System.out.println(sql);
    }
}

源码

public class StaticTextSqlNode implements SqlNode {
  private final String text;

  public StaticTextSqlNode(String text) {
    this.text = text;
  }

  @Override
  public boolean apply(DynamicContext context) {
    context.appendSql(text);
    return true;
  }

}

StaticTextSqlNode 源码非常简单就是把 SQL 语句通过 DynamicContext 的 appendSql() 方法拼接在之前的 SQL 语句后面。

TextSqlNode

由于 SQL 语句中含有 ${} 占位符,要解析占位符所以需要参数。

示例

public class TextSqlNodeDemo {
    public static void main(String[] args) {
        Configuration configuration = new Configuration();
        Map<String, Object> paraMap = new HashMap<>();
        // 把注释放放开并把下面put 方法注解之后会发现解析 ${} 占位符的值为空字符串 
        // Map<String, Object> paraMap = null;
        paraMap.put("user", "user");
		// paraMap.put("user", "'user'");
        SqlNode textSqlNode = new TextSqlNode("SELECT * FROM ${user}");
        DynamicContext dynamicContext = new DynamicContext(configuration, paraMap);
        textSqlNode.apply(dynamicContext);
        String sql = dynamicContext.getSql();
        System.out.println(sql);
    }
}

源码

	@Override
	public boolean apply(DynamicContext context) {
		// 通过 createParse 获取 GenericTokenParser 对象(主要是解决 ${} 占位符)。
		// 如果发现 ${} 占位符则通过 BindingTokenParser 的 handleToken(String) 方法返回值替换 ${} 占位符
	  GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
	  context.appendSql(parser.parse(text));
	  return true;
	}

	@Override
	public String handleToken(String content) {
	  // 通过 DynamicContext 获取实参
	  Object parameter = context.getBindings().get("_parameter");
	  if (parameter == null) {
	    context.getBindings().put("value", null);
	  } else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) {
	  	// SimpleTypeRegistry 中 SIMPLE_TYPE_SET 包含的类则存在 DynamicContext 参数中
	    context.getBindings().put("value", parameter);
	  }
	  // 通过 OGNL 从实参中获取 ${} 占位符的值
	  Object value = OgnlCache.getValue(content, context.getBindings());
	  String srtValue = value == null ? "" : String.valueOf(value); // issue #274 return "" instead of "null"
	  checkInjection(srtValue);
	  return srtValue;
	}


IfSqlNode

if/when 子标签里面的 SQL 语句抽象,只要 if 标签里面的 test 表达式为 true 时才拼接 if 标签里面的 SQL 语句。

示例

public class IfSqlNodeDemo {
	public static void main(String[] args) {
		Configuration configuration = new Configuration();
		// 实参对象
		Map<String, Object> paraMap = new HashMap<>();
		paraMap.put("user", "user");
		SqlNode staticTextSqlNode = new StaticTextSqlNode("SELECT * FROM user");
		// 构建 IfSqlNode 对象,传入 if 标签里面的 SQL 抽象和 test 表达式
		SqlNode ifSqlNode = new IfSqlNode(staticTextSqlNode, "user != null");
		DynamicContext dynamicContext = new DynamicContext(configuration, paraMap);
		// 通过 DynamicContext 拼接 SQL
		ifSqlNode.apply(dynamicContext);
		// 获取 SQL 语句
		String sql = dynamicContext.getSql();
		// 控制台输出
		System.out.println(sql);
	}
}

源码

	@Override
	public boolean apply(DynamicContext context) {
		// 通过 OGNL 判断 test 表达式是否成立,表达式里面涉及的属性值通过
		//  DynamicContext 传入的实参获取。如果成立折拼接 SQL 语句
		if (evaluator.evaluateBoolean(test, context.getBindings())) {
		  contents.apply(context);
		  return true;
		}
		return false;
	}

ChooseSqlNode

choose 子标签里面的 SQL 语句抽象,当 when 标签里面的 test 表达式成立时才会拼接里面的 SQL 语句,否则取 otherwise 标签里面的 SQL 语句。类似于 Java 里面的 if… else if…else 语句,只执行一个分支逻辑。

示例

public class ChooseSqlNodeDemo {
	public static void main(String[] args) {
		Configuration configuration = new Configuration();
		// 实参对象
		Map<String, Object> paraMap = new HashMap<>();
		paraMap.put("name", "文海");
		SqlNode staticTextSqlNode = new StaticTextSqlNode("SELECT * FROM user WHERE 1 = 1");
		// 构建 IfSqlNode 对象,传入 if 标签里面的 SQL 抽象和 test 表达式
		SqlNode ifSqlNode = new IfSqlNode(new StaticTextSqlNode(" AND name = #{name}"), "name != null");
		SqlNode defaultSqlNode = new StaticTextSqlNode(" AND name = 'wenhai'");
		DynamicContext dynamicContext = new DynamicContext(configuration, paraMap);
		// 通过 DynamicContext 拼接 SQL
		staticTextSqlNode.apply(dynamicContext);
		// 通过 DynamicContext 拼接 SQL
		ChooseSqlNode chooseSqlNode = new ChooseSqlNode(Collections.singletonList(ifSqlNode), defaultSqlNode);
		chooseSqlNode.apply(dynamicContext);
		// 获取 SQL 语句
		String sql = dynamicContext.getSql();
		// 控制台输出
		System.out.println(sql);
	}
}

源码

	// 通过构造函数传入 when 标签 SQL 抽象和 otherwise 标签的 SQL 抽象
	public ChooseSqlNode(List<SqlNode> ifSqlNodes, SqlNode defaultSqlNode) {
	  this.ifSqlNodes = ifSqlNodes;
	  this.defaultSqlNode = defaultSqlNode;
	}
	
	@Override
	public boolean apply(DynamicContext context) {
		// 如果一个分支条件满足就不再执行后面的逻辑
		for (SqlNode sqlNode : ifSqlNodes) {
		  if (sqlNode.apply(context)) {
		    return true;
		  }
		}
		// 前面的 when 标签里面的表达式都不满足,并且有兜底的 otherwise 标签则拼接里面的 SQL
		if (defaultSqlNode != null) {
		  defaultSqlNode.apply(context);
		  return true;
		}
		return false;
	}

ForEachSqlNode

foreach 子标签里面的 SQL 抽象,可以通过标签里面的 item 和 index 设置的变量获取对应的值。index 是数组以及集合的索引值而 Map 类型则是 key 里面的值,item 则是数组以及集合里面的元素而 Map 类型则是 value 里面的值。

示例

public class ForeachSqlNodeDemo {
    public static void main(String[] args) {
        Configuration configuration = new Configuration();
        // 实参对象
        Map<String, Object> paraMap = new HashMap<>();
//        Map<String, String> param = new HashMap<>();
//        param.put("wenhai", "文海");
//        param.put("wenhai2", "文海2");
//        paraMap.put("map", param);
        List<String> list = new ArrayList<>();
        list.add("wenhai");
        list.add("wenhai2");
        paraMap.put("list", list);
        DynamicContext dynamicContext = new DynamicContext(configuration, paraMap);
        SqlNode staticTextSqlNode = new StaticTextSqlNode("SELECT * FROM user WHERE name in");
        // 通过 DynamicContext 拼接 SQL
        staticTextSqlNode.apply(dynamicContext);
//        String collection = "map";
        String collection = "list";
        String item = "item";
        String index = "index";
        String open = "(";
        String close = ")";
        String separator = ",";
        ForEachSqlNode forEachSqlNode = new ForEachSqlNode(configuration, new StaticTextSqlNode("#{index}"), collection, index, item, open, close, separator);

        forEachSqlNode.apply(dynamicContext);
        // 获取 SQL 语句
        String sql = dynamicContext.getSql();
        // 控制台输出 :SELECT * FROM user WHERE name in (  #{__frch_index_0} , #{__frch_index_1} )
        // 同时 DynamicContext 里面的 _parameter 多出以  __frch_#index_n 和 __frch_#item_n 属性值
        // 便于后续通过
        System.out.println(sql);
    }
}


源码

	/**
	 * ForEachSqlNode 构造函数
	 * 
	 * @param configuration			  全局 Configuration 对象
	 * @param contents                foreach 标签里面的 SQL 抽象
	 * @param collectionExpression    foreach 标签里面的 collection 属性值
	 * @param index					  foreach 标签里面的 index 属性值
	 * @param item					  foreach 标签里面的 item 属性值
	 * @param open					  foreach 标签里面的 open 属性值
	 * @param close				      foreach 标签里面的 close 属性值
	 * @param separator               foreach 标签里面的 separator 属性值
	 */
	public ForEachSqlNode(Configuration configuration, SqlNode contents, String collectionExpression, String index, String item, String open, String close, String separator) {
	   this.evaluator = new ExpressionEvaluator();
	   this.collectionExpression = collectionExpression;
	   this.contents = contents;
	   this.open = open;
	   this.close = close;
	   this.separator = separator;
	   this.index = index;
	   this.item = item;
	   this.configuration = configuration;
	 }


	@Override
	public boolean apply(DynamicContext context) {
	  // 获取参数列表
	  Map<String, Object> bindings = context.getBindings();
	  // 通过 OGNL 获取 collectionExpression 表达式的值,该值不能为 null,
	  // 只能是 Iterable 实例和数组已经 Map 实例,其他都会报错
	  final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings);
	  if (!iterable.iterator().hasNext()) {
	    return true;
	  }
	  // 是否是第一次,第一次不用拼接 separator 值
	  boolean first = true;
	  // 如果设置了 open 属性值,则先拼接 open 属性值
	  applyOpen(context);
	  int i = 0;
	  for (Object o : iterable) {
	    DynamicContext oldContext = context;
	    // 如果是第一次或者是分隔符没有设置则通过 PrefixedContext 包装 DynamicContext 对象
	    // 在 appendSql 方法进行拼接 SQL 时候加上设置的前缀(此处就是 “”)
	    if (first || separator == null) {
	      context = new PrefixedContext(context, "");
	    } else {
	      context = new PrefixedContext(context, separator);
	    }
	    // 获取唯一序列号递增用于集合的索引
	    int uniqueNumber = context.getUniqueNumber();
	    // 为 DynamicContext 中的类型为 ContextMap 属性保存 foreach 遍历对应的值
	    // 以 __frch_#{index}_uniqueNumber 和 __frch_#{item}_uniqueNumber 为 key
	    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);
	    }
	    // 通过 FilteredDynamicContext 包装 PrefixedContext 替换 foreach 标签里面
	    // 以 #{} 占位符并且使用正则表达式匹配 item 以及 index 属性值为 __frch_#{index}_uniqueNumber 和 __frch_#{item}_uniqueNumber
	    contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber));
	    if (first) {
	      first = !((PrefixedContext) context).isPrefixApplied();
	    }
	    context = oldContext;
	    i++;
	  }
	  // 如果 foreach 标签里面的 close 属性设置了则拼接在 SQL 语句后面
	  applyClose(context);
	  context.getBindings().remove(item);
	  context.getBindings().remove(index);
	  return true;
	}


剩余的 SqlNode 就不分析了都是类似,通过包装 DynamicContext 以达到效果。

总结

此节分析了 Mapper.xml 中的 SQL 语句抽象为 SqlNode,通过实参传递给 DynamicContext 来动态拼接 SQL 语句,为后面学习 SqlSource 打下坚实的基础。

点击查看更多内容
1人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消