Map.Entry 和 Map.entrySet() 集合的 MyBatis 错误

MyBatis Error on Collection of Map.Entry and Map.entrySet()

假设我们有 MyBatis 3.3.0 和 MyBatis-Spring 1.2.3 和一个简单的 select 查询...

<select id="testSelect" parameterType="map" resultType="Integer">
    select 1 from dual where
    <foreach collection="properties" index="index" item="item" separator=" and ">
        1 = #{id} AND 'a' = #{item.key,jdbcType=VARCHAR} AND 'b' = #{item.value,jdbcType=VARCHAR}
    </foreach>
</select>

(应该简单 return 1,如果给定的 id 是 1 并且集合中给定的所有关键属性都是 "a" 并且所有值都是 "b")

...这使得一个简单的 TestMapper 接口方法...

Integer testSelect(Map<String, Object> arguments);

...我们用这个测试方法测试...

@Test
public void test_for_bug() {

    final Map<String, Object> parameters = new HashMap<>();
    parameters.put("id", 1);

    final Map<String, String> entries = new HashMap<>();
    entries.put("a", "b");
    parameters.put("properties", entries.entrySet());

    final Integer result = this.testMapper.testSelect(parameters);

    assertThat(result).isEqualTo(1);

}

...我们将得到以下错误....

Type handler was null on parameter mapping for property '__frch_item_0.value'. It was either not specified and/or could not be found for the javaType / jdbcType combination specified.

原因似乎是对 item.value 的调用导致对字符串本身的 value 属性 的调用。不幸的是,我不知道为什么。

Collection 个自定义 Entry 对象(keyvalue 属性)替换 entries.entrySet() 效果很好。也很奇怪:这似乎只发生在 <collection> 中,直接给 Map.Entry 作为参数,比如...

<select id="testSelect" parameterType="map" resultType="Integer">
    select 1 from dual where 'b' = #{entry.value,jdbcType=VARCHAR}
</select>

...和...

@Test
public void test_for_bug() {

    final Map<String, String> entries = new HashMap<>();
    entries.put("a", "b");

    final Map<String, Object> parameters = new HashMap<>();
    parameters.put("entry", entries.entrySet().iterator().next());

    final Integer result = this.testMapper.testSelect(parameters);

    assertThat(result).isEqualTo(1);

}

...有效。

有人知道 Map.EntrySet 的问题出在哪里吗?有机会以某种方式修复它吗?当然,创建变通办法很容易,但恕我直言,不需要。

你只需传递参数映射而不是 map.entrySet(),像这样

parameters.put("properties", entries);

然后这样调用你的 mybatis

<select id="testSelect" parameterType="map" resultType="Integer">
    select 1 from dual where
    <foreach collection="properties.entrySet()" index="index" item="item" separator=" and ">
        1 = #{id} AND 'a' = #{item.key,jdbcType=VARCHAR} AND 'b' = #{item.value,jdbcType=VARCHAR}
    </foreach>
</select> 

似乎处理此问题(已提交文档更新)的正确方法如下(因为开发人员在几个版本前进行了一些更改):

<select id="testSelect" parameterType="map" resultType="Integer">
    select 1 from dual where
    <foreach collection="properties" index="index" item="item" separator=" and ">
        1 = #{id} AND 'a' = #{index,jdbcType=VARCHAR} AND 'b' = #{item,jdbcType=VARCHAR}
    </foreach>
</select>

原因是,对于 Iterables/ArraysMaps(和 Iteratable<Map.Entry> 对象),<foreach> 的行为略有不同:

  • 对于IterableArrayindex是当前迭代的编号,item是本次迭代中从Iterable中检索到的元素。
  • 对于Map(或Iterable<Map.Entry>index是当前条目的键,item是当前条目的value

这解释了为什么 item.value 对于 Map<String, String> 实际上导致 String.valuevalue 已经是 String - 它有一个私有的 char array 成员调用了 value,所以 MyBatis 试图访问 String.value - 但失败了,因为 char array 不是映射类型)。