MyBatis ResultMap and Nested Associations
When mapping query results in MyBatis, you can choose betwean resultType and resultMap. Only one can be used in a single select. With resultType, MyBatis relies on column-to-property name matching (and aliases) to populate a target class. With resultMap, you describe exact how each column should be mapped, and you can define object graphs (one-to-one and one-to-many) via association and collection. MyBatis internally represents every mapping as a ResultMap; resultType is a convenient shorthand for straightforward cases.
Domain model
Post.java
package org.example.mybatis.domain;
import java.util.List;
public class Post {
private long postId;
private String headline;
private String body;
private String author;
private List<Comment> comments;
public long getPostId() { return postId; }
public void setPostId(long postId) { this.postId = postId; }
public String getHeadline() { return headline; }
public void setHeadline(String headline) { this.headline = headline; }
public String getBody() { return body; }
public void setBody(String body) { this.body = body; }
public String getAuthor() { return author; }
public void setAuthor(String author) { this.author = author; }
public List<Comment> getComments() { return comments; }
public void setComments(List<Comment> comments) { this.comments = comments; }
}
Comment.java
package org.example.mybatis.domain;
import java.util.Date;
public class Comment {
private long commentId;
private String message;
private Date createdAt;
private Post post; // owning Post
public long getCommentId() { return commentId; }
public void setCommentId(long commentId) { this.commentId = commentId; }
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public Date getCreatedAt() { return createdAt; }
public void setCreatedAt(Date createdAt) { this.createdAt = createdAt; }
public Post getPost() { return post; }
public void setPost(Post post) { this.post = post; }
}
Assume relational tables:
- posts(id, title, content, author)
- comments(id, text, created_at, post_id)
Basic mapping with resultType
<!-- aliases -->
<typeAliases>
<typeAlias alias="Post" type="org.example.mybatis.domain.Post"/>
<typeAlias alias="Comment" type="org.example.mybatis.domain.Comment"/>
</typeAliases>
<!-- simple mapping using resultType -->
<select id="getPost" parameterType="long" resultType="Post">
select
p.id as postId,
p.title as headline,
p.content as body,
p.author as author
from posts p
where p.id = #{value}
</select>
With resultType, MyBatis uses the column labels (or aliases) to match bean properties. Here aliases map id→postId, title→headline, content→body.
Explicit ResultMap for the same query
<resultMap id="PostMap" type="Post">
<id column="id" property="postId"/>
<result column="title" property="headline"/>
<result column="content" property="body"/>
<result column="author" property="author"/>
</resultMap>
<select id="getPostByMap" parameterType="long" resultMap="PostMap">
select id, title, content, author from posts where id = #{value}
</select>
Defining the id element helps MyBatis identify unique rows, which matters when assembling collections from joined or nested results.
One-to-one: Comment → Post via association
A Comment references its owning Post. Use association with a nested select.
<resultMap id="CommentMap" type="Comment">
<id column="id" property="commentId"/>
<result column="text" property="message"/>
<result column="created_at" property="createdAt"/>
<association property="post"
column="post_id"
select="getPostByMap"
javaType="Post"/>
</resultMap>
<select id="getComment" parameterType="long" resultMap="CommentMap">
select c.id, c.text, c.created_at, c.post_id
from comments c
where c.id = #{value}
</select>
- property: target property on the result type (Comment.post)
- select: the nested statement that loads the associated object
- column: the current row’s value past to the nested select as the parameter object
- javaType: the type of the associated property
The nested select receives a simple parameter (the post_id value). Inside getPostByMap, reference it with #{value} or #{_parameter}.
One-to-many: Post → Comments via collection
Load a Post and its Comments with a nested select. Use ofType for collection element type.
<resultMap id="PostWithComments" type="Post">
<id column="id" property="postId"/>
<result column="title" property="headline"/>
<result column="content" property="body"/>
<result column="author" property="author"/>
<collection property="comments"
ofType="Comment"
column="id"
select="findCommentsForPost"/>
</resultMap>
<select id="findPostWithComments" parameterType="long" resultMap="PostWithComments">
select p.id, p.title, p.content, p.author from posts p where p.id = #{value}
</select>
<select id="findCommentsForPost" parameterType="long" resultMap="CommentMap">
select id, text, created_at, post_id
from comments
where post_id = #{value}
</select>
- collection maps a property that is a List/Set
- ofType is the element type of the collection
- column passes the outer row’s id into the nested select
Mapper interfaces and sample tests
PostMapper.java
package org.example.mybatis.mapper;
import org.example.mybatis.domain.Post;
public interface PostMapper {
Post getPost(long id);
Post getPostByMap(long id);
Post findPostWithComments(long id);
}
CommentMapper.java
package org.example.mybatis.mapper;
import java.util.List;
import org.example.mybatis.domain.Comment;
public interface CommentMapper {
Comment getComment(long id);
List<Comment> findCommentsForPost(long postId);
}
JUnit examples
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.example.mybatis.domain.Comment;
import org.example.mybatis.domain.Post;
import org.example.mybatis.mapper.CommentMapper;
import org.example.mybatis.mapper.PostMapper;
import org.junit.Test;
import java.util.List;
public class MyBatisResultMapTest {
private final SqlSessionFactory sqlSessionFactory = MyBatisBootstrap.build();
@Test
public void loadCommentsByPost() {
try (SqlSession session = sqlSessionFactory.openSession()) {
CommentMapper mapper = session.getMapper(CommentMapper.class);
List<Comment> list = mapper.findCommentsForPost(10L);
for (Comment c : list) {
System.out.printf("%d %s%n", c.getCommentId(), c.getMessage());
}
}
}
@Test
public void loadPostGraph() {
try (SqlSession session = sqlSessionFactory.openSession()) {
PostMapper mapper = session.getMapper(PostMapper.class);
Post post = mapper.findPostWithComments(10L);
if (post.getComments() != null) {
System.out.println("comments=" + post.getComments().size());
}
}
}
}
Notes on automatic mapping
- With resultType, MyBatis maps columns to properties by name. Aliases help align column names to bean properties.
- With resultMap, you can be explicit and compose graphs. MyBatis still performs automatic mapping for any properties not explicit listed if auto-mapping is enabled.
- When using nested selects, the value from column is passed as the paramter object. For simple types, reference it with #{value} or #{_parameter}.