Spring Bootにおけるテーブル結合〜多数編

### 本日のお題
本日のお題はテーブル結合です。

[以前](https://qiita.com/keishi04hrikzira/items/77e1c94258ee8b60cfdc)に取り上げたテーブル結合（ユーザー情報とともに所属している部署の情報も取得する）は、「あるレコードを検索した時にそこに紐づくレコードを他のテーブルから一つだけ取得する」というものでした。

今回は、「ユーザー情報とともにそのユーザーの給与情報を取得する」場面を例にとり、他のテーブルから複数のレコードを同時に取得していきたいと思います。
### テーブル設計
【m_userテーブル】
[![Image from Gyazo](https://i.gyazo.com/ff514fc730bb4259ac27e59de02d1dec.png)](https://gyazo.com/ff514fc730bb4259ac27e59de02d1dec)
【t_salaryテーブル】
[![Image from Gyazo](https://i.gyazo.com/870d5583805f81dbc2c864c038f666da.png)](https://gyazo.com/870d5583805f81dbc2c864c038f666da)
### 目次
- エンティティクラスの作成
- 既存のエンティティクラスの編集
- xmlファイルの編集
- コントローラの編集
- ビューの編集

### エンティティクラスの作成
まずは、t_salaryテーブルに対応するエンティティクラスを作成します。

Salary.javaを作成し、以下のように編集します。

```java:Salary.java
package com.example.demo.domain.user.model;

import lombok.Data;

@Data
public class Salary {
	private String userId;
	private String yearMonth;
	private Integer salary;
}
```
次に、このSalary.javaのインスタンスをフィールドとして持てるように他のエンティティクラスを編集していきます。
### 既存のエンティティクラスの編集
MUser.java, UserDetailForm.javaを以下のように編集してください。

```java:MUser.java
package com.example.demo.domain.user.model;

import java.util.Date;
import java.util.List;

import lombok.Data;

@Data
public class MUser {
	private String userId;
	private String password;
	private String userName;
	private Date birthday;
	private Integer age;
	private Integer gender;
	// private Integer departmentId;
	private Department department;
	private String role;
	// 追加
	private List<Salary> salaryList;
}
```

```java:UserDetailForm.java
package com.example.demo.form;

import java.util.Date;
import java.util.List;

import com.example.demo.domain.user.model.Department;
import com.example.demo.domain.user.model.Salary;

import lombok.Data;

@Data
public class UserDetailForm {
	private String userId;
	private String password;
	private String userName;
	private Date birthday;
	private Integer age;
	private Integer gender;
	// 追加
	private Department department;
	// 追加
	private List<Salary> salaryList;
}
```

ポイントは以下の点ですね。

```java
// 追加
private List<Salary> salaryList;
```
1人のユーザーに複数の給与情報が紐づく可能性があるので、一旦それらを配列の形で持てるようにしています。

では、エンティティクラスの準備ができたので、t_salaryテーブルからも情報を取得できるようにしていきます。
### xmlファイルの編集
UserMapper.xmlを以下のように編集してください。

```xml:UserMapper.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.repository.UserMapper">
	<resultMap type="com.example.demo.domain.user.model.MUser" id="user">
		<id column="user_id" property="userId"/>
		<result column="password" property="password"/>
		<result column="user_name" property="userName"/>
		<result column="birthday" property="birthday"/>
		<result column="age" property="age"/>
		<result column="gender" property="gender"/>
		<!-- <result column="department_id" property="departmentId"/> -->
		<result column="role" property="role"/>
		<!-- 追加 -->
		<association property="department" resultMap="department"/>
		<!-- 追加 -->
		<collection property="salaryList" resultMap="salary" columnPrefix="salary_"/>
	</resultMap>
	<!-- 追加 -->
	<resultMap type="com.example.demo.domain.user.model.Department" id="department">
		<id column="department_id" property="departmentId"/>
		<result column="department_name" property="departmentName"/>
	</resultMap>
	<!-- 追加 -->
	<resultMap id="salary" type="com.example.demo.domain.user.model.Salary">
		<id column="user_id" property="userId"/>
		<id column="year_month" property="yearMonth"/>
		<result column="salary" property="salary"/>
	</resultMap>
	<insert id="insertOne">
		insert into m_user(user_id, password, user_name, birthday, age, gender, department_id, role)values(#{userId}, #{password}, #{userName}, #{birthday}, #{age}, #{gender}, #{departmentId}, #{role})
	</insert>
	<select id="findMany" resultType="MUser">
		select * from m_user
		<where>
			<if test="userId != null">
				user_id like '%' || #{userId} || '%'
			</if>
			<if test="userName != null">
				and user_name like '%' || #{userName} || '%'
			</if>
		</where>
	</select>
	<select id="findOne" resultMap="user">
		<!-- 表示するカラムを*から変更 -->
		select 
		m_user.user_id
		, m_user.password
		, m_user.user_name
		, m_user.birthday
		, m_user.age
		, m_user.gender
		, m_department.department_id
		, m_department.department_name
		, t_salary.user_id as salary_user_id
		, t_salary.year_month as salary_year_month
		, t_salary.salary as salary_salary
		 from m_user 
		 left join m_department on m_user.department_id = m_department.department_id 
		<!-- 追加 -->
		left join t_salary on m_user.user_id = t_salary.user_id		 
		<!-- 修正 -->
		 where m_user.user_id = #{userId}
	</select>
	<update id="updateOne">
		update m_user set password = #{password}, user_name = #{userName} where user_id = #{userId}
	</update>
	<delete id="deleteOne">
		delete from m_user where user_id = #{userId}
	</delete>
</mapper>
```
まず、以下でSalary.javaのフィールドとt_salaryのカラムをマッピングさせています。

```xml
<!-- 追加 -->
<resultMap id="salary" type="com.example.demo.domain.user.model.Salary">
    <id column="user_id" property="userId"/>
	<id column="year_month" property="yearMonth"/>
	<result column="salary" property="salary"/>
</resultMap>
```
次に、このマッピングを以下でm_userテーブルのマッピングにネストしています。

```xml
<!-- 追加 -->
<collection property="salaryList" resultMap="salary" columnPrefix="salary_"/>
```
[以前](https://qiita.com/keishi04hrikzira/items/77e1c94258ee8b60cfdc)は１対１なのでassociationタグを用いましたが、今回は１対多数なのでcollectionタグを用います。
例によって、collectionタグのresultMap属性とresultMapタグのid属性の値が一致していることを確認してください。
columnPrefix属性は、クエリ内でカラムにエイリアスを設定する場合に必要です。
エイリアスの命名にはルールがあり、"columnPrefixの値"+"元のカラム名"がエイリアスになります。

```xml
	<select id="findOne" resultMap="user">
		<!-- 表示するカラムを*から変更 -->
		select 
		m_user.user_id
		, m_user.password
		, m_user.user_name
		, m_user.birthday
		, m_user.age
		, m_user.gender
		, m_department.department_id
		, m_department.department_name
        // エイリアスを設定
		, t_salary.user_id as salary_user_id
		, t_salary.year_month as salary_year_month
		, t_salary.salary as salary_salary
		 from m_user 
		 left join m_department on m_user.department_id = m_department.department_id 
		<!-- 追加 -->
		left join t_salary on m_user.user_id = t_salary.user_id		 
		<!-- 修正 -->
		 where m_user.user_id = #{userId}
	</select>
```
では、次にコントローラを編集します。
### コントローラの編集
UserDetailController.javaを以下のように編集してください。

```java:UserDetailController.java
public String getUser(UserDetailForm form, Model model, @PathVariable("userId")String userId) {
		MUser user = userService.getUserOne(userId);
		user.setPassword(null);
		log.info(user.toString());
		form = modelMapper.map(user, UserDetailForm.class);
		// 追加・formにsalaryListをセット
		form.setSalaryList(user.getSalaryList());
		
		model.addAttribute("userDetailForm", form);
		
		return "user/detail";
	}
```

追加したのは以下の部分です。

```java
form = modelMapper.map(user, UserDetailForm.class);
// 追加・formにsalaryListをセット
form.setSalaryList(user.getSalaryList());
```
なぜこのようなことをしているのかというと、mapメソッドではListのコピーができないからです。
通常のインスタンスであれば組み込みであっても自分で定義したものであってもコピーできるので、[以前](https://qiita.com/keishi04hrikzira/items/77e1c94258ee8b60cfdc)のように1対1のテーブル結合であれば今回のような処理は必要ありません。

では、最後にビューを編集します。
### ビューの編集
user/detail.htmlに以下を追加してください。

```user/detail.html
<form id="user-detail-form" method="post" th:action="@{/user/detail}" class="form-signup" th:object="${userDetailForm}">
<!-- 省略 -->
<!-- 給与部分 -->
			<th:block th:if="*{salaryList != null and salaryList.size() > 0}">
				<div class="header border-bottom">
					<h1 class="h2">給料</h1>
				</div>
				<table class="table table-striped table-border table-hover">
					<thead>
						<tr>
							<th class="w-25">年月</th>
							<th>給料</th>
						</tr>
					</thead>
					<tbody>
						<tr th:each="item:*{salaryList}">
							<td th:text="${item.yearMonth}"></td>
							<td th:text="${#numbers.formatInteger(item.salary, 3, 'COMMA')}"></td>
						</tr>
					</tbody>
				</table>
			</th:block>
		</form>
```

基本的にはsalaryListの中身をeachで展開しています。

ちなみに、以下のth:blockについてですが、

```html
<th:block th:if="*{salaryList != null and salaryList.size() > 0}">
```
このth:blockタグには

- th:ifの条件式がtrueの場合にはタグの中身を表示。
- 条件式のtrue/falseに関わらず、自身はhtmlの要素を作らない。

という特徴があります。

if文に応じて表示非表示を切り替えるだけであればdivなどで事足りますが、それだとfalseの場合に空のdiv要素が表示されてレイアウトが崩れるので、今回はth:blockを用いています。
### 終わりに
というわけで、１対多数のテーブル結合が実装できました。
やはり、エンティティクラスが配列をフィールドとして持てるというのが違和感ですね。
ここについては、そのうちまとめます。