[MyBatis] 1対多の関係にあるテーブルに対する照会クエリをネストされたBeanにマッピングしたい

# 問題

JIRAやRedmineのようなバグトラッキングシステムを例とします。このシステムでは、トピックごとにプロジェクトを作成します。チケットはプロジェクトごとに作成され、利用者はチケットにコメントを付与することができるとします。ER図は以下の通りです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/105859/2f62aef2-095d-c552-f67a-ed1d9c90f556.png)

データについては、次のようになっているとします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/105859/94fe46bc-9827-7e4c-b4dc-0320697de4ec.png)

ここで、プロジェクトとそれにひもづくチケット・コメントをすべて取得することを考えます。クエリを書くことは簡単ですが、`Project.java`というネストされたJava Beanに結果をマッピングした場合、どのようにすればよいでしょうか? 言い換えれば、1対多の関係にあるテーブルに対する照会クエリをネストされたBeanにマッピングしたい場合、どのようにすればよいでしょうか?

```Project.java
@Data
@ToString(exclude = {"tickets"})
public class Project {
    private int projectId;
    private String name;
    private List<Ticket> tickets;
}
```

```Ticket.java
@Data
@ToString(exclude = {"comments"})
public class Ticket {
    private int ticketId;
    private String content;
    private List<Comment> comments;
}
```

```Comment.java
@Data
public class Comment {
    private int commentId;
    private String content;
}
```

# 回答

[`resultMap`と`collection`という機能を利用します。](https://mybatis.org/mybatis-3/ja/sqlmap-xml.html) 上記の例であれば、以下のようなxmlを作成します。

```ProjectMapper.xml
<?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.mapper.ProjectMapper">
  <select id="findAllProject" resultMap="findAllProjectResultMap">
    SELECT
      project.project_id,
      project.name as project_name,
      ticket.ticket_id,
      ticket.content as ticket_content,
      comment.comment_id,
      comment.content as comment_content
    FROM project
    JOIN ticket ON project.project_id = ticket.project_id
    JOIN comment ON ticket.ticket_id = comment.ticket_id
  </select>

  <resultMap id="findAllProjectResultMap" type="com.example.demo.dto.Project">
    <id property="projectId" column="project_id" />
    <result property="name" column="project_name"/>
    <collection property="tickets" ofType="com.example.demo.dto.Ticket">
      <id property="ticketId" column="ticket_id" />
      <result property="content" column="ticket_content"/>
      <collection property="comments" ofType="com.example.demo.dto.Comment">
        <id property="commentId" column="comment_id" />
        <result property="content" column="comment_content"/>
      </collection>
    </collection>
  </resultMap>
</mapper>
```
以下のコードではSQLを実行結果がどのような形でJava BeanにマッピングされたのかをPretty Printすることで、確認することができます

```CommandlineappliApplication.java
@Component
public class CommandlineappliApplication implements CommandLineRunner{

    @Autowired
    private ProjectMapper projectMapper;

    @Override
    public void run(String... args) throws Exception {
        for (Project project : projectMapper.findAllProject()) {
            System.out.printf("%s%n", project);

            for (Ticket ticket : project.getTickets()) {
                System.out.printf("  └ %s%n", ticket);

                for (Comment comment : ticket.getComments()) {
                    System.out.printf("      └ %s%n", comment);
                }
            }
        }
    }
}
```

実際に動かした結果は次の通り。ネストされたBeanに対して、SQLの実行結果が想定通りマッピングされていることがわかります。

```
Project(projectId=1, name=プロジェクト1)
  └ Ticket(ticketId=1, content=チケット1-1)
      └ Comment(commentId=1, content=コメント1-1-1)
      └ Comment(commentId=2, content=コメント1-1-2)
  └ Ticket(ticketId=2, content=チケット1-2)
      └ Comment(commentId=3, content=コメント1-2-1)
      └ Comment(commentId=4, content=コメント1-2-2)
Project(projectId=2, name=プロジェクト2)
  └ Ticket(ticketId=3, content=チケット2-1)
      └ Comment(commentId=5, content=コメント2-1-1)
      └ Comment(commentId=6, content=コメント2-1-2)
  └ Ticket(ticketId=4, content=チケット2-2)
      └ Comment(commentId=7, content=コメント2-2-1)
      └ Comment(commentId=8, content=コメント2-2-2)
```

# 環境情報

```xml:pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.3.2.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.example</groupId>
	<artifactId>mybatis-sample</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>mybatis-sample</name>
	<description>Demo project for Spring Boot</description>

	<properties>
		<java.version>11</java.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-batch</artifactId>
		</dependency>
		<dependency>
			<groupId>org.mybatis.spring.boot</groupId>
			<artifactId>mybatis-spring-boot-starter</artifactId>
			<version>2.1.3</version>
		</dependency>

		<dependency>
			<groupId>com.h2database</groupId>
			<artifactId>h2</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
			<exclusions>
				<exclusion>
					<groupId>org.junit.vintage</groupId>
					<artifactId>junit-vintage-engine</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
		<dependency>
			<groupId>org.springframework.batch</groupId>
			<artifactId>spring-batch-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>
```