问题背景
最近写代码的时候遇到了想用forEach遍历Map的场景,然后按照惯性思维进行编写代码,如下
<foreach item="entry" collection="map.entrySet()" separator=" OR ">
(name = #{entry.key} AND age = #{entry.value})
</foreach>
报错There is no getter for property 'key' in 'java.lang.String'
这个应该是jdk8版本,在jdk17版本则会报错一个关于什么BASE包无法访问的问题,与这个报错类似,时间稍微有点久,就不详细贴了。
解决方案
在网上搜索了一些列的使用方式,包括上面的错误写法,并且上面的错误写法非常的多,所以我在这里用源码对正确的方式进行证明,之前直接用了,现在有时间看一下源码,希望大家后续用到Map遍历的时候,能够少踩坑,使用时也能知道为啥是这样用的,正确的使用方式如下:
<foreach item="value" index="key" collection="map.entrySet()">
<!-- 正确:直接通过 key 和 value 访问 -->
(name = #{key} AND age = #{value})
</foreach>
如果将map.entrySet()
作为collection
,则mybatis的处理为,将key绑定为index,将value绑定为item,而不是正常的index为数字,item为迭代器中的元素,下面是源码证明,源码版本从3.5.0开始就已经是这样了,再之前的版本就没有进行验证了,大家可以自行搜索自己代码中的类ForEachSqlNode.class
进行源码查看。
@Override
public boolean apply(DynamicContext context) {
Map<String, Object> bindings = context.getBindings();
final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings,
Optional.ofNullable(nullable).orElseGet(configuration::isNullableOnForEach));
if (iterable == null || !iterable.iterator().hasNext()) {
return true;
}
boolean first = true;
applyOpen(context);
int i = 0;
for (Object o : iterable) {
DynamicContext oldContext = context;
if (first || separator == null) {
context = new PrefixedContext(context, "");
} else {
context = new PrefixedContext(context, separator);
}
int uniqueNumber = context.getUniqueNumber();
// 这里,进行判断如果迭代器中的元素是继承自Map.Entry则走单独的逻辑
if (o instanceof Map.Entry) {
@SuppressWarnings("unchecked")
Map.Entry<Object, Object> mapEntry = (Map.Entry<Object, Object>) o;
// 将mapEntry.getKey()和index绑定
applyIndex(context, mapEntry.getKey(), uniqueNumber);
// 将mapEntry.getValue()和item绑定
applyItem(context, mapEntry.getValue(), uniqueNumber);
} else {
applyIndex(context, i, uniqueNumber);
applyItem(context, o, uniqueNumber);
}
contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber));
if (first) {
first = !((PrefixedContext) context).isPrefixApplied();
}
context = oldContext;
i++;
}
applyClose(context);
context.getBindings().remove(item);
context.getBindings().remove(index);
return true;
}