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>
去除首尾的逗號, 新增字首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);
}
}