Florian Schaetz
Florian Schaetz

Reputation: 10662

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

assuming we have MyBatis 3.3.0 and MyBatis-Spring 1.2.3 and a simple select query...

<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>

(should simple return 1, if the id given is 1 and all key properties given in the collection are "a" and all values "b")

...which makes a simple TestMapper interface method...

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

...and we test it with this test method...

@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);

}

...we will get the following error....

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.

The reason for that seems to be that the call to item.value results in a call of the value property of the String itself. Unfortunately, I have no clue, why.

Replacing the entries.entrySet() with a Collection of custom Entry objects (with key and value property) works fine. Also strange: This only seems to happen inside a <collection>, giving a Map.Entry directly as a parameter, like...

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

...and...

@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);

}

...works.

Has anyone an idea what the problem with Map.EntrySet is here? Any chance to get it fixed somehow? Of course creating a workaround is easy enough, but imho it should not be needed.

Upvotes: 0

Views: 13496

Answers (2)

Florian Schaetz
Florian Schaetz

Reputation: 10662

Seems that the correct way to handle this (documentation update already submitted) is the following (since the developers made some changes a few versions ago):

<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>

The reason is, that <foreach> behaves a little bit different for Iterables/Arrays and Maps (and Iteratable<Map.Entry> Objects):

  • For an Iterable or Array, index is the number of the current iteration and item is the element retrieved from the Iterable in this iteration.
  • For a Map (or Iterable<Map.Entry>) index is the current entry's key and item is the current entry's value

This explains why item.value for a Map<String, String> leads actually to String.value (the value is already a String - which has a private char array member called value, so MyBatis is trying to access String.value - and fails, because a char array is not a mapped type).

Upvotes: 3

Sai Ye Yan Naing Aye
Sai Ye Yan Naing Aye

Reputation: 6738

You just pass parameter map instead of map.entrySet(), like this

parameters.put("properties", entries);

and call your mybatis like that

<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> 

Upvotes: 0

Related Questions