1.ForEachSqlNode

mybatis的foreach标签可以将列表、数组中的元素拼接起来,中间可以指定分隔符separator

  <select id="getByUserId" resultMap="BaseMap">
select <include refid="BaseFields"></include>
from user
<where>
user_id in
<foreach collection="userIdList" item="userId" open="(" separator="," close=")">
#{userId}
</foreach>
</where>
</select>

上面这段select sql代码使用了foreach标签,传入了一个userIdList的列表,首先会转化为一个ForeachSqlNode对象,经过处理后foreach标签里面的代码会解析成 (假设userIdList=[101,102,103])

(#{__frch_userId_0}, #{__frch_userId_1},#{__frch_userId_2}) , 后续预处理值替换后就会变成 (101,102,103)

下面是具体的ForEachSqlNode的解析过程源码:

public class ForEachSqlNode implements SqlNode {
public static final String ITEM_PREFIX = "__frch_"; // 表达式值获取器
private final ExpressionEvaluator evaluator;
// collection userIdList
private final String collectionExpression;
private final SqlNode contents;
// open值 (
private final String open;
// close值 )
private final String close;
// separator分隔符值 ,
private final String separator;
// item值 userId
private final String item;
// index值 null
private final String index;
// mybatis配置信息
private final Configuration configuration; 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();
// 迭代器
final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings);
if (!iterable.iterator().hasNext()) {
return true;
}
boolean first = true;
// 添加open值
applyOpen(context);
int i = 0;
for (Object o : iterable) {
DynamicContext oldContext = context;
// 第一次循环first为true,使用new PrefixedContext(context, "")构建context,因为第一个元素之前不用添加分隔符
// 第一次循环完毕后first为false,使用new PrefixedContext(context, separator)构建,之后先添加分隔符再添加sql值
if (first || separator == null) {
context = new PrefixedContext(context, "");
} else {
context = new PrefixedContext(context, separator);
}
// 每次循环不同值
int uniqueNumber = context.getUniqueNumber();
// Issue #709
if (o instanceof Map.Entry) {
// map的index为key,item为value
@SuppressWarnings("unchecked")
Map.Entry<Object, Object> mapEntry = (Map.Entry<Object, Object>) o;
applyIndex(context, mapEntry.getKey(), uniqueNumber);
applyItem(context, mapEntry.getValue(), uniqueNumber);
} else {
// list的index为序号(从0开始递增),item为value元素值
// 添加到上下文的bindings这个map中
// index不为空,key = __frch_index_uniqueNumber的格式,value = i
applyIndex(context, i, uniqueNumber);
// item不为空, key = __frch_item_uniqueNumber的格式, value = o
applyItem(context, o, uniqueNumber);
} // context -> PrefixedContext
// 处理sql内容
contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber));
if (first) {
// 是否应用了分隔符,first=false
first = !((PrefixedContext) context).isPrefixApplied();
}
context = oldContext;
i++;
}
// 添加close
applyClose(context);
// 移除item和index
context.getBindings().remove(item);
context.getBindings().remove(index);
return true;
} private void applyIndex(DynamicContext context, Object o, int i) {
if (index != null) {
context.bind(index, o);
context.bind(itemizeItem(index, i), o);
}
} private void applyItem(DynamicContext context, Object o, int i) {
if (item != null) {
context.bind(item, o);
context.bind(itemizeItem(item, i), o);
}
} private void applyOpen(DynamicContext context) {
if (open != null) {
context.appendSql(open);
}
} private void applyClose(DynamicContext context) {
if (close != null) {
context.appendSql(close);
}
} private static String itemizeItem(String item, int i) {
return ITEM_PREFIX + item + "_" + i;
} // 动态过滤
private static class FilteredDynamicContext extends DynamicContext {
private final DynamicContext delegate;
private final int index;
private final String itemIndex;
private final String item; public FilteredDynamicContext(Configuration configuration,DynamicContext delegate, String itemIndex, String item, int i) {
super(configuration, null);
this.delegate = delegate;
// uniqueNumber序号
this.index = i;
this.itemIndex = itemIndex;
this.item = item;
} @Override
public Map<String, Object> getBindings() {
return delegate.getBindings();
} @Override
public void bind(String name, Object value) {
delegate.bind(name, value);
} @Override
public String getSql() {
return delegate.getSql();
} @Override
public void appendSql(String sql) {
// 获取 #{}内的内容,之后用replaceFirst将item替换为 __frch__item_0
// 类似 #{orderId} -> #{__frch_orderId_0}, #{__frch_orderId_1}, #{__frch_orderId_2} 长度取决于集合列表
GenericTokenParser parser = new GenericTokenParser("#{", "}", content -> {
// 开头空格 + item + 后面是.,:或空格的字符串
String newContent = content.replaceFirst("^\\s*" + item + "(?![^.,:\\s])", itemizeItem(item, index));
// itemIndex不为空且原字符串和新字符串相同
if (itemIndex != null && newContent.equals(content)) {
newContent = content.replaceFirst("^\\s*" + itemIndex + "(?![^.,:\\s])", itemizeItem(itemIndex, index));
}
return "#{" + newContent + "}";
}); delegate.appendSql(parser.parse(sql));
} @Override
public int getUniqueNumber() {
return delegate.getUniqueNumber();
} } // 前缀填充功能
private class PrefixedContext extends DynamicContext {
private final DynamicContext delegate;
private final String prefix;
private boolean prefixApplied; public PrefixedContext(DynamicContext delegate, String prefix) {
super(configuration, null);
this.delegate = delegate;
this.prefix = prefix;
this.prefixApplied = false;
} public boolean isPrefixApplied() {
return prefixApplied;
} @Override
public Map<String, Object> getBindings() {
return delegate.getBindings();
} @Override
public void bind(String name, Object value) {
delegate.bind(name, value);
} @Override
public void appendSql(String sql) {
if (!prefixApplied && sql != null && sql.trim().length() > 0) {
// 添加分隔符前缀,可以是逗号,等值
delegate.appendSql(prefix);
prefixApplied = true;
}
// 再添加sql内容
delegate.appendSql(sql);
} @Override
public String getSql() {
return delegate.getSql();
} @Override
public int getUniqueNumber() {
return delegate.getUniqueNumber();
}
} }

2.TrimSqlNode

mybatis的trim标签可以添加/删除指定的前缀、后缀值

  <select id="getByUserId" resultMap="BaseMap">
select <include refid="BaseFields"></include>
from user
<trim prefix="where" prefixOverrides="and | or">
and user_id = #{userId}
</trim>
</select>

这段代码使用了trim标签,会先匹配去除and | or开头的<trim>标签内的sql内容,接着加上前缀where,会解析成

where user_id = #{userId}

下面是具体的ForEachSqlNode的解析过程源码:

public class TrimSqlNode implements SqlNode {

  private final SqlNode contents;
private final String prefix;
private final String suffix;
private final List<String> prefixesToOverride;
private final List<String> suffixesToOverride;
private final Configuration configuration; public TrimSqlNode(Configuration configuration, SqlNode contents, String prefix, String prefixesToOverride, String suffix, String suffixesToOverride) {
this(configuration, contents, prefix, parseOverrides(prefixesToOverride), suffix, parseOverrides(suffixesToOverride));
} protected TrimSqlNode(Configuration configuration, SqlNode contents, String prefix, List<String> prefixesToOverride, String suffix, List<String> suffixesToOverride) {
// 待处理的sql节点
this.contents = contents;
// 添加前缀
this.prefix = prefix;
// 要去除的前缀
this.prefixesToOverride = prefixesToOverride;
// 添加后缀
this.suffix = suffix;
// 要去除的后缀
this.suffixesToOverride = suffixesToOverride;
// mybatis配置
this.configuration = configuration;
} @Override
public boolean apply(DynamicContext context) {
FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context);
// 处理节点 文本添加到sqlBuffer中
boolean result = contents.apply(filteredDynamicContext);
filteredDynamicContext.applyAll();
return result;
} // 解析 prefixOverrides和suffixOverrides多个可用|分割
private static List<String> parseOverrides(String overrides) {
if (overrides != null) {
final StringTokenizer parser = new StringTokenizer(overrides, "|", false);
final List<String> list = new ArrayList<>(parser.countTokens());
while (parser.hasMoreTokens()) {
list.add(parser.nextToken().toUpperCase(Locale.ENGLISH));
}
return list;
}
return Collections.emptyList();
} private class FilteredDynamicContext extends DynamicContext {
private DynamicContext delegate;
private boolean prefixApplied;
private boolean suffixApplied;
private StringBuilder sqlBuffer; public FilteredDynamicContext(DynamicContext delegate) {
super(configuration, null);
// 委托原始的context
this.delegate = delegate;
this.prefixApplied = false;
this.suffixApplied = false;
this.sqlBuffer = new StringBuilder();
} public void applyAll() {
// 去除前后空格
sqlBuffer = new StringBuilder(sqlBuffer.toString().trim());
// 转大写格式 为了后续的匹配
String trimmedUppercaseSql = sqlBuffer.toString().toUpperCase(Locale.ENGLISH);
if (trimmedUppercaseSql.length() > 0) {
// 处理前缀
applyPrefix(sqlBuffer, trimmedUppercaseSql);
// 处理后缀
applySuffix(sqlBuffer, trimmedUppercaseSql);
}
// 想上下文追加sql内容
delegate.appendSql(sqlBuffer.toString());
} @Override
public Map<String, Object> getBindings() {
return delegate.getBindings();
} @Override
public void bind(String name, Object value) {
delegate.bind(name, value);
} @Override
public int getUniqueNumber() {
return delegate.getUniqueNumber();
} @Override
public void appendSql(String sql) {
sqlBuffer.append(sql);
} @Override
public String getSql() {
return delegate.getSql();
} private void applyPrefix(StringBuilder sql, String trimmedUppercaseSql) {
if (!prefixApplied) {
prefixApplied = true;
if (prefixesToOverride != null) {
for (String toRemove : prefixesToOverride) {
// 判断开头是否匹配,只可匹配一次后续会直接退出循环
if (trimmedUppercaseSql.startsWith(toRemove)) {
// 从头开始删除匹配的字符toRemove长度
sql.delete(0, toRemove.trim().length());
break;
}
}
}
if (prefix != null) {
// sql.insert(0, prefix + " ");
sql.insert(0, " ");
sql.insert(0, prefix);
}
}
} private void applySuffix(StringBuilder sql, String trimmedUppercaseSql) {
if (!suffixApplied) {
suffixApplied = true;
if (suffixesToOverride != null) {
for (String toRemove : suffixesToOverride) {
// 匹配末尾
if (trimmedUppercaseSql.endsWith(toRemove) || trimmedUppercaseSql.endsWith(toRemove.trim())) {
int start = sql.length() - toRemove.trim().length();
int end = sql.length();
sql.delete(start, end);
break;
}
}
}
if (suffix != null) {
sql.append(" ");
sql.append(suffix);
}
}
} } }

另外其实<where>和<set>标签底层的原理也是和<trim>标签相同的,有继承关系,代码如下

<set>
去除首尾的逗号, 添加前缀SET
public class SetSqlNode extends TrimSqlNode { private static final List<String> COMMA = Collections.singletonList(","); public SetSqlNode(Configuration configuration,SqlNode contents) {
super(configuration, contents, "SET", COMMA, null, COMMA);
} } <where>
去除开头的AND/OR值,添加前缀WHERE
public class WhereSqlNode extends TrimSqlNode { private static List<String> prefixList = Arrays.asList("AND ","OR ","AND\n", "OR\n", "AND\r", "OR\r", "AND\t", "OR\t"); public WhereSqlNode(Configuration configuration, SqlNode contents) {
super(configuration, contents, "WHERE", prefixList, null, null);
} }

Mybatis SqlNode源码解析的更多相关文章

  1. MyBatis详细源码解析(上篇)

    前言 我会一步一步带你剖析MyBatis这个经典的半ORM框架的源码! 我是使用Spring Boot + MyBatis的方式进行测试,但并未进行整合,还是使用最原始的方式. 项目结构 导入依赖: ...

  2. MyBatis 3源码解析(一)

    一.SqlSessionFactory 对象初始化 //加载全局配置文件 String resource = "mybatis-config.xml"; InputStream i ...

  3. Mybatis SqlSessionTemplate 源码解析

    As you may already know, to use MyBatis with Spring you need at least an SqlSessionFactory and at le ...

  4. MyBatis 3源码解析(四)

    四.MyBatis 查询实现 Employee empById = mapper.getEmpById(1); 首先会调用MapperProxy的invoke方法 @Override public O ...

  5. MyBatis 3源码解析(三)

    三.getMapper获取接口的代理对象 1.先调用DefaultSqlSession的getMapper方法.代码如下: @Override public <T> T getMapper ...

  6. MyBatis 3源码解析(二)

    二.获取SqlSession对象 1.首先调用DefaultSqlSessionFactory 的 openSession 方法,代码如下: @Override public SqlSession o ...

  7. mybatis源码解析1--前言

    在开始分析mybatis源码之前,需要定一个目标,也就是我们不是为了读源码而去读,一定是带着问题去读,在读的时候去寻找到答案,然后再读码的同时整理总结,学习一些高级的编码方式和技巧. 首先我们知道my ...

  8. Mybatis源码解析,一步一步从浅入深(二):按步骤解析源码

    在文章:Mybatis源码解析,一步一步从浅入深(一):创建准备工程,中我们为了解析mybatis源码创建了一个mybatis的简单工程(源码已上传github,链接在文章末尾),并实现了一个查询功能 ...

  9. Mybatis源码解析(二) —— 加载 Configuration

    Mybatis源码解析(二) -- 加载 Configuration    正如上文所看到的 Configuration 对象保存了所有Mybatis的配置信息,也就是说mybatis-config. ...

随机推荐

  1. ABP框架入门

    技术要求 在开始使用 ABP 框架之前,您需要在计算机上安装一些工具. IDE/编辑器 本书假设您使用的是Visual Studio 2022(支持 .NET 6.0 的 v10.0)或更高版本.如果 ...

  2. Java 线程池四种拒绝策略

    jdk1.5版本新增了 JUC 并发包,其中一个包含线程池. 四种拒绝策略: 拒绝策略类型 说明 1 ThreadPoolExecutor.AbortPolicy 默认拒绝策略,拒绝任务并抛出任务 2 ...

  3. XCTF练习题---MISC---stegano

    XCTF练习题---MISC---stegano flag:flag{1nv151bl3m3554g3} 解题步骤: 1.观察题目,下载附件 2.打开发现是一张PDF图片,尝试转换word无果后,想到 ...

  4. 【Azure Developer】使用 adal4j(Azure Active Directory authentication library for Java)如何来获取Token呢

    问题描述 使用中国区的Azure,在获取Token时候,参考了 adal4j的代码,在官方文档中,发现了如下的片段代码: ExecutorService service = Executors.new ...

  5. [python][flask] Flask 入门(以一个博客后台为例)

    目录 1.安装 1.1 创建虚拟环境 1.2 进入虚拟环境 1.3 安装 flask 2.上手 2.1 最小 Demo 2.2 基本知识 3.解构官网指导 Demo 3.1 克隆与代码架构分析 3.2 ...

  6. MAC 地址为什么不需要全球唯一

    MAC 地址(Media access control address)是分配给网络接口控制器(Network interface controller, NIC)的唯一标识符,它会在网络段中充当网络 ...

  7. 1 Mybatis动态SQL

    Mybatis动态SQL 1. 注解开发 ​ 我们也可以使用注解的形式来进行开发,用注解来替换掉xml. 使用注解来映射简单语句会使代码显得更加简洁,但对于稍微复杂一点的语句,Java 注解不仅力不从 ...

  8. 用 Go 快速开发一个 RESTful API 服务

    何时使用单体 RESTful 服务 对于很多初创公司来说,业务的早期我们更应该关注于业务价值的交付,而单体服务具有架构简单,部署简单,开发成本低等优点,可以帮助我们快速实现产品需求.我们在使用单体服务 ...

  9. python PDF转图片,World转PDF

    软件不用续费了... PDF转World暂时没需求,有需求了再搞 Python3.9 ---------------pip3 install  PyMuPdf ---------------pip3 ...

  10. DirectX11--CPU与GPU计时器

    前言 GAMES104的王希说过: 游戏引擎的世界里,它的核心是靠Tick()函数把这个世界驱动起来. 本来单是一个CPU的计时器是不至于为其写一篇博客的,但把GPU计时器功能加上后就不一样了.在这一 ...