SpringBoot × MyBatis 中間テーブルへ登録する方法
#はじめに
今回は、自作している学習用チャットアプリで中間テーブルを実装したところ、かなり詰まったので、中間テーブルへの登録方法を備忘録として残そうと思います。
同じように悩んでいる方がいたら参考にしてみてください。
#アプリ概要
ログインユーザーが他の登録ユーザーを選択してチャットを開始するという、DMのようなものが主機能のアプリです。
- リレーション
usersテーブルとroomsテーブルが多対多の関係性のため、N+1問題が懸念されるので、中間テーブルとしてroom_usersテーブルを作成しました。
#環境
- Spring2.5.5
- gradle
- MyBatis2.2.0
- MySQL
#中間テーブルへのinsertを実装
今回の実装では、
①チャットルーム作成時に、roomsテーブルにチャットルーム情報を登録する
②作成者のユーザーIDと作成者が選択したユーザーのユーザーID、作成したチャットルームのIDを中間テーブルに登録する
上記2つの処理が必要です。
ということで実装内容を解説します。
##entity
@Data
public class MUser {
private int id;
private String name;
private String email;
private String password;
private String passwordConfirmation;
private String role;
@DateTimeFormat(pattern = "yyyy_MM_dd HH:mm:ss")
private LocalDateTime createdAt;
@DateTimeFormat(pattern = "yyyy_MM_dd HH:mm:ss")
private LocalDateTime updatedAt;
}
@Data
public class MRoom {
private int id;
private String roomName;
@DateTimeFormat(pattern = "yyyy_MM_dd HH:mm:ss")
private LocalDateTime createdAt;
}
@Data
public class TRoomUser {
private int id;
private int roomId;
private int currentUserId;
private int userId;
@DateTimeFormat(pattern = "yyyy_MM_dd HH:mm:ss")
private LocalDateTime createdAt;
}
entityクラスのポイントは中間テーブルのTRoomUserクラス
です。
roomsテーブルのIDにroomId
、チャットルーム作成者のIDをcurrentUserId
に、選択されたユーザーのIDをuserId
として定義しています。
##RoomMapper
@Mapper
public interface RoomMapper {
/**チャットルーム登録*/
public int insertOneRoom(MRoom room);
}
<?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とxmlのマッピング -->
<mapper namespace="com.example.demo.repository.RoomMapper">
<!-- マッピング定義(rooms) -->
<resultMap type="com.example.demo.entity.MRoom" id="room">
<id column="id" property="id"></id>
<result column="room_name" property="roomName"></result>
<result column="created_at" property="createdAt"></result>
<collection property="roomUserList" resultMap="roomUser"></collection>
</resultMap>
<!-- チャットルーム登録 -->
<insert id="insertOneRoom">
insert into rooms (
id,
room_name,
created_at
) values (
#{id,jdbcType=INTEGER},
#{roomName,jdbcType=VARCHAR},
#{createdAt,jdbcType=TIMESTAMP}
)
<selectKey resultType="int" keyProperty="id" order="AFTER">
select @@IDENTITY
</selectKey>
</insert>
</mapper>
まず①の処理ですが、roomsテーブルへの登録なので単純にinsert文を作成するだけで大丈夫です。
また、selectKeyタグ
で主キーであるidを取得していますが、後ほど使用するために取得しています。
##RoomUserMapper
@Mapper
public interface RoomUserMapper {
public int insertRoomUser(TRoomUser roomUser);
}
<?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とxmlのマッピング -->
<mapper namespace="com.example.demo.repository.RoomUserMapper">
<!-- マッピング定義(room_user) -->
<resultMap type="com.example.demo.entity.TRoomUser" id="roomUser">
<id column="id" property="id"></id>
<result column="room_id" property="roomId"></result>
<result column="current_user_id" property="currentUserId"></result>
<result column="created_at" property="createdAt"></result>
<result column="user_id" property="userId"></result>
</resultMap>
<!-- room_user登録 -->
<insert id="insertRoomUser">
insert into room_users (
id,
room_id,
current_user_id,
created_at,
user_id
) values (
#{id,jdbcType=INTEGER},
#{roomId,jdbcType=INTEGER},
#{currentUserId,jdbcType=INTEGER},
#{createdAt,jdbcType=TIMESTAMP},
#{userId,jdbcType=INTEGER}
)
</insert>
</mapper>
中間テーブルもMapperに関してはroomsテーブルと同様、登録の処理を記述するだけでOKです。
##RoomService
public interface RoomService {
/**チャットルーム登録*/
public void insertRoom(MRoom room, RoomForm form);
}
@Service
public class RoomServiceImpl implements RoomService {
@Autowired
private RoomMapper mapper;
/**
*チャットルーム登録
*/
@Transactional
@Override
public void insertRoom(MRoom room, RoomForm form) {
//チャットルーム名取得
room.setRoomName(form.getRoomName());
//現在時刻の取得
LocalDateTime now = LocalDateTime.now();
room.setCreatedAt(now);
//チャットルーム登録
mapper.insertOneRoom(room);
}
}
Serviceクラスは一つにまとめることもできますが、Mapperと同じメソッド名になるとわかりにくくなるため、自分の場合は、インターフェースと実装クラスで分けています。
ロジックですが、チャットルーム名はフォームに入力された値を取得して、それをセットするだけです。
作成日時も現在時刻を取得し、それをセットするだけで簡単に実装できます。
最後にroomsテーブルに登録するためRoomMapperインターフェース
の登録メソッドを呼び出して登録処理を行います。
##Form
@Data
public class RoomForm {
@NotBlank
private String roomName;
private int userId;
}
チャットルーム登録画面のフォームクラスです。
自分の実装はプルダウンから、選択したユーザーとチャットができる仕様のため、プルダウンに入力されるユーザーの情報を取得するためuserId
を定義しています。
##RoomUserService
public interface RoomUserService {
/**room_user登録*/
public void registRoomUser(RoomForm form, TRoomUser roomUser, @AuthenticationPrincipal UserDetailServiceImpll loginUser);
}
@Service
public class RoomUserServiceImpl implements RoomUserService {
@Autowired
private RoomUserMapper mapper;
@Autowired
private RoomService service;
@Transactional
@Override
public void registRoomUser(RoomForm form, TRoomUser roomUser, @AuthenticationPrincipal UserDetailServiceImpll loginUser) {
//formをMRoomクラスに変換
MRoom room = new MRoom();
//チャットルーム登録
service.insertRoom(room, form);
//ログインユーザーのユーザーID取得
int currentUserId = loginUser.getUser().getId();
//roomsテーブルのIDを設定(FK)
roomUser.setRoomId(room.getId());
//ログインユーザーのIDを設定
roomUser.setCurrentUserId(currentUserId);
//プルダウン選択されたユーザーIDを設定
roomUser.setUserId(form.getUserId());
//現在時刻の取得
LocalDateTime now = LocalDateTime.now();
roomUser.setCreatedAt(now);
//roomUserTBL登録
mapper.insertRoomUser(roomUser);
}
}
少し処理が多めですが、中間テーブルへの登録ロジックです。
ポイントは、このロジックの中でRoomServiceインターフェース
のinsertRoom()メソッド
を呼び出している点です。
これにより、roomsテーブルの登録と同時にroom_usersテーブルの登録も行うことができ、roomsテーブルのidを取得して、room_usersテーブルのroomIdカラム
に値を設定できます。
また、roomsテーブルに登録するメソッド(insertRoom())の前にroomsテーブルのエンティティのインスタンス(MRoom room = new MRoom();
)を作成しておくことも重要です。
これがないと、チャットルーム登録処理時に引数としてMRoomのエンティティを渡せないため登録処理自体が行えないため、最初にインスタンスを作成することが必要です。
その後、UserDetailServiceImpllクラス
で取得したログインユーザーのIDを取得、フォームから送られるユーザーIDを取得して、各自セッターで中間テーブルへ登録する値を設定します。
最後に、中間テーブルのMapperに定義している登録メソッドを呼び出せば、roomsテーブルの登録と同時に、中間テーブルへも値を登録できるというロジックが完成します。
なお、ログインユーザーの取得については、以下の記事で詳しく解説しているので、参考にしてみてください。
##RoomController
@Controller
@RequestMapping("/")
@Slf4j
public class RoomController {
@Autowired
private UserService userService;
@Autowired
private RoomService roomService;
@Autowired
private RoomUserService roomUserService;
@GetMapping("/rooms/new")
public String getRoomsNew(Model model, @ModelAttribute("form") RoomForm form, @AuthenticationPrincipal UserDetailServiceImpll loginUser) {
//ログインユーザーのユーザーID取得
int currentUserId = loginUser.getUser().getId();
//ユーザー取得(複数件)
List<MUser> users = userService.getUsers(currentUserId);
model.addAttribute("users", users);
return "rooms/new";
}
@PostMapping("/rooms/new")
public String postRoomsNew(Model model, @Validated @ModelAttribute("form") RoomForm form, BindingResult result, TRoomUser roomUser, @AuthenticationPrincipal UserDetailServiceImpll loginUser) {
//入力チェック
if(result.hasErrors()) {
/* NG:チャットルーム作成画面に戻る*/
return "redirect:/rooms/new";
}
log.info(form.toString());
//チャットルーム・roomUserTBL登録
roomUserService.registRoomUser(form, roomUser, loginUser);
return "redirect:/";
}
}
コントローラーの処理ですが、postRoomsNew()メソッド
から解説します。
ここでは単純にバリデーションのチェックと、ロジックの呼び出しを行うだけです。
次に、getRoomsNew()メソッド
の解説ですが、UserServiceの
getUsers()メソッド`がプルダウンを実装する上で重要になってくるので、解説します。
先に結論から言うと、このメソッドの処理はログインユーザー以外のユーザーを取得するメソッドです。
###UserMapper
@Mapper
public interface UserMapper {
/**ログインユーザー以外のユーザー取得(複数件)*/
public List<MUser> findMany(int id);
}
<?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とxmlのマッピング -->
<mapper namespace="com.example.demo.repository.UserMapper">
<!-- マッピング定義(ユーザー) -->
<resultMap type="com.example.demo.entity.MUser" id="user">
<id column="id" property="id"></id>
<result column="email" property="email"></result>
<result column="password" property="password"></result>
<result column="password_confirmation" property="passwordConfirmation"></result>
<result column="name" property="name"></result>
<result column="role" property="role"></result>
<result column="created_at" property="createdAt"></result>
<result column="updated_at" property="updatedAt"></result>
</resultMap>
<!-- ログインユーザー以外のユーザー取得(複数件) -->
<select id="findMany" resultType="MUser">
select
*
from users
where not
id = #{id}
</select>
</mapper>
ポイントはxmlファイルのSQLです。
WHERE NOT句
で条件を指定することで、引数に指定されるID以外の値を取得することができます。
###UserService
public interface UserService {
/**ログインユーザー以外のユーザー取得(複数件)*/
public List<MUser> getUsers(int id);
}
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper mapper;
/**ログインユーザー以外のユーザー取得(複数件)*/
@Override
public List<MUser> getUsers(int id) {
return mapper.findMany(id);
}
}
##View
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>ChatApp</title>
<link rel="stylesheet" th:href="@{/css/rooms/room.css}">
</head>
<body>
<div class='chat-room-form'>
<h1>新規チャットルーム</h1>
<form th:action="@{/rooms/new}" method="post" th:object="${form}">
<div class='chat-room-form__field'>
<div class='chat-room-form__field--left'>
<label for="roomName" th:text="#{roomName}" class="chat-room-form__label"></label>
</div>
<div class='chat-room-form__field--right'>
<input type="text" th:field="*{roomName}" th:errorclass="is-invalid" class="chat__room_name chat-room-form__input" placeholder="チャットルーム名を入力してください">
</div>
<div class="invalid-feedback" th:errors="*{roomName}"></div>
</div>
<div class='chat-room-form__field'></div>
<div class='chat-room-form__field'>
<div class='chat-room-form__field--left'>
<label class='chat-room-form__label' for='chat_room_チャットメンバー'>チャットメンバー</label>
</div>
<div class='chat-room-form__field--right'>
<select id="userId" name="userId">
<option value="">チャットするユーザーを選択してください</option>
<option th:each="user: ${users}" th:value="${user.id}" th:text="${user.name}"></option>
</select>
</div>
</div>
<div class='chat-room-form__field'>
<div class='chat-room-form__field--left'></div>
<div class='chat-room-form__field--right'>
<input type="submit" name="commit" class="chat-room-form__action-btn">
</div>
</div>
</form>
</div>
</body>
</html>
ポイントはプルダウンのselectタグ
の部分です。
通常のフォームタグ内ならth:field="*{userId}"
としますが、それだとエラーになるため、selectタグ
の場合はid属性とname属性に入力させたい(DBに送りたい)値を設定します。
プルダウンの初期値を設定する方法は色々ありますが、自分の場合は、シンプルにoptionタグを二つ作り、一つ目のvalue属性の値を空にして設定しています。
二つ目のoptionタグでは、送信したい値をth:value属性
に指定し、表示させたい内容をth:name属性
に指定します。
先ほど、コントローラーで呼び出したgetUsers()
メソッドはList型のため、th:each属性
で値を一つずつ表示・取得できるようにしています。
これで、中間テーブル+αの実装は完了です。
自分の場合はこのロジックを考え出すのに半日くらいかかり、実装するのに3時間くらいかかりました。
まだまだJavaであったりSpringBootの文献は非常に少ないので、同志がいたら参考になれば幸いかと思います。
Discussion