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(); } }}
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>去除首尾的逗号, 添加前缀SETpublic 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值,添加前缀WHEREpublic 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); }}