Search code examples
javamybatisspring-mybatismybatis-mapper

mybatis resultMapping not working when order of field names are changed


I am storing playlist information in an SQLite database. This is my entity class;

@Data
@Builder
public class Playlist {
    private String id;
    private String playlistName;
    private PlaylistType type;
}

The PlaylistType is an enum of ("TYPE_A", "TYPE_B")

At first, in my mapper XML, I tried using resultType="Playlist" and it worked when database column names and Playlist properties were in the same order. But if I change the order of properties, for example as shown below,

public class Playlist {
    private String id;
    private PlaylistType type; // just moved PlaylistType property 
    private String playlistName;
}

It is giving me this error:

Caused by: org.apache.ibatis.executor.result.ResultMapException: Error attempting to get column 'name' from result set.  Cause: java.lang.IllegalArgumentException: No enum constant com.example.demo.PlaylistType.p1

Then some threads suggested to use resultMap, but the issue persists;

This is my mapper XML using resultMap

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.PlaylistRepository">

    <resultMap id="toPlaylist" type="com.example.demo.Playlist">
        <id column="id" property="id"/>
        <result column="name" property="playlistName"/>
        <result column="type" property="type"/>
    </resultMap>
    <select id="findById" resultMap="toPlaylist">
        select * from playlist where id = #{id}
    </select>
</mapper>

Is there a better way to map database columns? It shouldn't be too restrictive in silly matters like the order of properties in the entity class. Or am I missing something?


Solution

  • Adding @NoArgsConstructor is the easiest solution.
    This answer is for people who don't want to add no-args constructor to an immutable class.


    When you add @Builder to the class, Lombok generates the following constructor.

    Playlist(String id, String playlistName, PlaylistType type) {
      this.id = id;
      this.playlistName = playlistName;
      this.type = type;
    }
    

    Although it is package-private, MyBatis can (has to; because there is no other constructor) use this constructor using reflection [1].

    To map the result to this constructor, MyBatis provides four different methods.

    1. Order-based constructor auto-mapping
    2. Arg-name-based constructor auto-mapping
    3. Result map with <constructor> without name attribute
    4. Result map with <constructor> with name attribute

    tl;dr

    Method 2 should be sufficient for most simple cases.
    Use method 3 or 4 for advanced mapping or when you need the best performance.

    1. Order-based constructor auto-mapping

    This is the default behavior when you don't use <resultMap>.

    In your example, the constructor takes three arguments, so the first, second and third columns in the result set are mapped to id, playlistName and type respectively.
    When you change the field order in the class, the order of the constructor arguments changes and you have to change the column order as well.

    Personally, I do not recommend this order-based constructor auto-mapping because there is a known issue that can be a head-scratcher.
    I explained it in this answer if you are interested.

    2. Arg-name-based constructor auto-mapping

    This is the behavior when you 1) enable argNameBasedConstructorAutoMapping in the config and 2) don't use <resultMap>. With this method, MyBatis looks for a column that has the same name as the constructor argument [2].
    The column order does not matter.

    Note that, in your example, the column name name does not match the target argument name playlistName, so you may have to specify a column alias in the SELECT statement.

    argNameBasedConstructorAutoMapping was added in version 3.5.10.

    3. Result map with <constructor> without name attribute

    When using a result map, you need <constructor>, <idArg> and <arg> elements to perform constructor mapping.

    <resultMap>
      <constructor>
        <idArg column="id" javaType="string" />
        <arg column="name" javaType="string" />
        <arg column="type" javaType="pkg.PlaylistType" />
      </constructor>
    </resultMap>
    

    For the sake of completeness, here is the same result map declared in a Java mapper [3].

    @Arg(id = true, column = "id", javaType = String.class)
    @Arg(column = "name", javaType = String.class)
    @Arg(column = "type", javaType = PlaylistType.class)
    Playlist findById(String id);
    

    With this method, the column order does not matter, but the XML element order must match the constructor argument order.
    In case it is difficult for you to maintain the XML element order (e.g. the target class is frequently updated), see the next section.

    4. Result map with <constructor> with name attribute

    When name attribute is specified, the order of XML elements does not have to match the order of the actual constructor arguments [4].

    <resultMap>
      <constructor>
        <idArg column="id" name="id" javaType="string" />
        <arg column="name" name="playlistName" javaType="string" />
        <arg column="type" name="type" javaType="pkg.PlaylistType" />
      </constructor>
    </resultMap>
    

    In your case, the constructor argument types always match the field types, so javaType can be omitted.

    <resultMap>
      <constructor>
        <idArg column="id" name="id" />
        <arg column="name" name="playlistName" />
        <arg column="type" name="type" />
      </constructor>
    </resultMap>
    

    And here is the same result map using annotation.

    @Arg(id = true, column = "id", name = "id")
    @Arg(column = "name", name = "name")
    @Arg(column = "type", name = "type")
    Playlist findById(String id);
    

    With the above result map, MyBatis searches a constructor that has the following three arguments, but in arbitrary order.

    • name: id, type=java.lang.String
    • name: playlistName, type=java.lang.String
    • name: type, type=pkg.PlaylistType

    When there are multiple constructors that match the criteria, you need to add @AutomapConstructor to the right one.
    Once the constructor is found, the value of the specified column is mapped to each constructor argument.

    So, with this method, both XML element order and column order do not matter, but you may need to edit the name value if you change a field name.

    This method requires version 3.4.3 or later.


    [1] If you use the Java Platform Module System (JPMS), you may have to allow MyBatis to access this constructor.

    [2] To include argument names in the binary, you must either 1) specify -parameters compiler option or 2) add @Param annotation to each argument.

    [3] If you use a version older than 3.5.4, you may need @ConstructorArgs.

    [4] <idArg> must be written before <arg> because it is enforced by the DTD.