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?
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.
<constructor>
without name
attribute<constructor>
with name
attributeMethod 2 should be sufficient for most simple cases.
Use method 3 or 4 for advanced mapping or when you need the best performance.
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.
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.
<constructor>
without name
attributeWhen 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.
<constructor>
with name
attributeWhen 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.
id
, type=java.lang.String
playlistName
, type=java.lang.String
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.