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

### 今回のお題
今回のお題は、Sring Bootプロジェクトにおけるテーブル結合です。

テーブル結合といっても色々な種類がありますよね。

今回の実装内容は、m_userテーブルとm_departmentテーブルが多数体１で紐づいている状態で、ユーザーの詳細ページにそのユーザーの部署の情報も表示したいというものです。
### <details><summary>テーブル設計など</summary><div>

【m_userテーブル】

| 1 | 2 |
|:------:|:-:|
| user_id |String|
| password |String|
| user_name|String|
| birthday      | Date    |
| age           | Integer |
| gender        | Integer |
| department_id | Integer |
| role          | String  |

【m_departmentテーブル】

| 1 | 2 |
|:-:|:-:|
| department_id | Integer |
| department_name | String |

</div></details>

### 現状および実装内容

- 現在のユーザー詳細画面は以下のようになっており、部署に関しては表示されていない。
- それに合わせて、フォームにバインドさせるクラスも部署に関するインスタンスフィールドは無しで定義されている。
- テーブルと対応させているMUserクラスにおいては、department_idというInteger型のフィールドが定義されている。
- 上記の状態を修正し、ユーザー詳細画面で部署の情報が表示されるようにする。
- そのために、MUserがDepartmentインスタンス自体をフィールドとして持てるようにする。

#####<details><summary>現在の状況詳細</summary><div>

ユーザー詳細画面

```java:user/detail.html
<tbody>
					<tr>
						<th class="w-25">ユーザーID</th>
						<td th:text="*{userId}"></td>
					</tr>
					<tr>
						<th>パスワード</th>
						<td>
							<input type="text" class="form-control" th:field="*{password}">
						</td>
					</tr>
					<tr>
						<th>ユーザー名</th>
						<td>
							<input type="text" class="form-control" th:field="*{userName}">
						</td>
					</tr>
					<tr>
						<th>誕生日</th>
						<td th:text="*{#dates.format(birthday, 'YYYY/MM/dd')}"></td>
					</tr>
					<tr>
						<th>年齢</th>
						<td th:text="*{age}"></td>
					</tr>
					<tr>
						<th>性別</th>
						<td th:text="*{gender == 1 ?'男性':'女性'}"></td>
					</tr>
				</tbody>
```

【UserMapper.java】
DB操作のインターフェースは以下の通り。

```java:UserMapper.java
package com.example.demo.repository;

import java.util.List;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import com.example.demo.domain.user.model.MUser;

@Mapper
public interface UserMapper {
	public int insertOne(MUser user);
        // これがユーザー詳細
	public List<MUser> findMany(MUser user);
	public MUser findOne(String userId);
	public void updateOne(@Param("userId")String userId, @Param("password") String password,@Param("userName") String userName);
	public int deleteOne(@Param("userId")String userId);
}
```

【xmlファイル】
DB操作関数に対応するクエリは以下の通り

```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"/>
	</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 * from m_user where 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>
```

【MUserクラス、UserDetailFormクラス】
エンティティクラスおよびフォームクラスは以下の通り。

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

import java.util.Date;

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 String role;
}

```

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

import java.util.Date;

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

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;
}
```

【インターフェース・コントローラなど】
インターフェース・実装クラス・コントローラに関しては以下の通りです。

```java:UserService.java
package com.example.demo.application.service;

import java.util.List;

import com.example.demo.domain.user.model.MUser;

public interface UserService {
	public void signup(MUser user);
        // ここがユーザー詳細機能
	public List<MUser> getUsers(MUser user);
	public MUser getUserOne(String userId);
	public void updateUserOne(String userId, String password, String userName);
	public void deleteUserOne(String userId);
}
```

```java:UserServiceImpl.java
package com.example.demo.domain.user.service.impl;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.example.demo.application.service.UserService;
import com.example.demo.domain.user.model.MUser;
import com.example.demo.repository.UserMapper;

@Service
public class UserServiceImpl implements UserService{
	@Autowired
	private UserMapper mapper;
	
	@Override
	public void signup(MUser user) {
		// user.setDepartmentId(1);
		user.setRole("ROLE_GENERAL");
		mapper.insertOne(user);
	}
        // ここがユーザー詳細
	@Override
	public List<MUser> getUsers(MUser user){
		return mapper.findMany(user);
	}
	@Override
	public MUser getUserOne(String userId) {
		return mapper.findOne(userId);
	}
	@Override
	public void updateUserOne(String userId, String password, String userName) {
		mapper.updateOne(userId, password, userName);
	}
	@Override
	public void deleteUserOne(String userId) {
		int count = mapper.deleteOne(userId);
	}
}

```

```java:UserDetailController.java
package com.example.demo.controller;



import org.modelmapper.ModelMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import com.example.demo.application.service.UserService;
import com.example.demo.domain.user.model.MUser;
import com.example.demo.form.UserDetailForm;

import lombok.extern.slf4j.Slf4j;


@Controller
@RequestMapping("/user")
@Slf4j
public class UserDetailController {
	@Autowired
	private UserService userService;
	
	@Autowired
	private ModelMapper modelMapper;
	
        // ここがユーザー詳細
	@GetMapping("/detail/{userId:.+}")
	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);
		
		model.addAttribute("userDetailForm", form);
		
		return "user/detail";
	}
	@PostMapping(value="/detail", params="update")
	public String updateUser(UserDetailForm form, Model model) {
		userService.updateUserOne(form.getUserId(), form.getPassword(), form.getUserName());
		
		return "redirect:/user/list";
	}
	@PostMapping(value="/detail", params="delete")
	public String deleteUser(UserDetailForm form, Model model) {
		userService.deleteUserOne(form.getUserId());
		
		return "redirect:/user/list";
	}
}
```
</div></details>

では、上記の内容を踏まえて実装を行っていきます。
### 実装手順
- Department.javaの作成
- xmlファイルの編集
- エンティティクラスの編集
- ビューの編集

### Department.javaの作成
まずは、m_departmentテーブルに対応するDepartment.javaを作成します。

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

import lombok.Data;

@Data
public class Department {
	private Integer departmentId;
	private String departmentName;
}
```
次に、ユーザー詳細ページに遷移する際に紐づく部署の情報を取得できるようにします。
### UserMapper.xmlファイルの編集
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"/>
                <!-- 追加終了 -->
	</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>
        <!-- 追加終了 -->
	<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>
        <!-- left join 以下を追加 -->
	<select id="findOne" resultMap="user">
		select * from m_user left join m_department on m_user.department_id = m_department.department_id where 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>
```

主な変更点は4つです。

まず、以下でm_departmentテーブルとDepartmentクラスを対応させています。

```xml
	<!-- 追加 -->
	<resultMap type="com.example.demo.domain.user.model.Department" id="department">
		<id column="department_id" property="departmentId"/>
		<result column="department_name" property="departmentName"/>
	</resultMap>
    <!-- 追加終了 -->
```
次に、上記で取得したm_departmentテーブルの情報とm_userテーブルを紐づける処理を行います。
ここで登場するのが下記のassociationタグで、マッピングの中に別のマッピングを入れることができるようになります。
上記のresultMapタグのidが以下のassociationタグのresultMap属性と一致している必要がある点に注意してください。

```xml
		<!-- 追加 -->
		<association property="department" resultMap="department"/>
        <!-- 追加終了 -->
```
次に、クエリの中身を修正していきます。

```xml
    <!-- left join 以下を追加 -->
	<select id="findOne" resultMap="user">
		select * from m_user left join m_department on m_user.department_id = m_department.department_id where user_id = #{userId}
	</select>
```
left join句を用いてテーブルを結合させています。
join の前にleftをつけたのは、部署に所属していないユーザーの情報でも取得できるようにするためです。

最後に、以下のように既存のコードを一箇所コメントアウトします。

```xml
        <!-- コメントアウト -->
		<!-- <result column="department_id" property="departmentId"/> -->
```
associationタグを用いてdepartment_id周りの処理については新たに定義したので、既存のものが残っていると干渉してエラーになります。

何はともあれ、これでm_userとm_departmentの情報をセットで取得できるようになりました。

次に、m_departmentの情報を受け取れるようにエンティティクラスを修正していきます。
### エンティティクラスの編集
現在、MUser.javaおよびUserDetailForm.javaはDepartment.javaに関するフィールドを持っていない、もしくは持っていても型がインスタンスではなくIntegerという状態です。

なので、自身のフィールドにDepartment.javaのインスタンスをセットできるように編集していきます。

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

import java.util.Date;

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;
}
```

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

import java.util.Date;

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

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;
}
```

これで、MUser経由でフォームにDepartmentクラスのインスタンスも渡せるようになりました。

最後に、ビューを編集して終わりです。
### ビューの編集
ビューに以下を追加して、部署名を表示できるようにします。

```html:user/detail.html
<form id="user-detail-form" method="post" th:action="@{/user/detail}" class="form-signup" th:object="${userDetailForm}">
<!-- 中略 -->
					<!-- 追加 -->
					<tr>
						<th>部署名</th>
						<td>
							<span th:if="*{department != null}" th:text="*{department.departmentName}"></span>
						</td>
					</tr>
					<!-- 追加ここまで -->
```

### 終わりに
これで、関連テーブルの情報を同時に取得できるようになりました。

個人的に一番違和感があったのは、**m_userテーブルのカラムはdepartment_idだが対応するMUserモデルのフィールドはDepartmentクラスになっている**というところです。

私が学習経験のあるフレームワークはRailsにしろLaravelにしろModel（Spring bootでいうところのエンティティクラス）とテーブルを同時に作ることが可能で、テーブルのカラムとモデルのフィールドが自動的に対応します（例えばRailsであれば、usersテーブルを作成しそこにnameカラムを用意すると、Userモデルとnameというインスタンス変数が自動的に作成される）。

Spring Bootにはこのような仕組みがないので、今回のような方法になったのかなと感じました。