SpringBoot × MyBatis中間テーブルからINNER JOINで値を取得する方法
#はじめに
今回の実装は、「ログインユーザーの保有するN件のチャットルームを取得する」という実装を行いました。
私の場合、roomsテーブルにはusersを参照する外部キーカラムがなく、中間テーブルであるroom_usersテーブルにroom_idカラムと、ログインユーザーのidが入るcurrent_user_idカラム、ログインユーザーとチャットしているユーザーのidが入るuser_idカラムをカラムとして設定しています。
##usersテーブル
id(PK) | password | password_conf | name | role | created_at | updated_at | |
---|---|---|---|---|---|---|---|
int | varchar | varchar | varchar | varchar | varchar | Timestamp | Timestamp |
##roomsテーブル
id(PK) | room_name | created_at |
---|---|---|
int | varchar | Timestamp |
##room_usersテーブル
id(PK) | room_id(FK) | current_user_id(FK) | user_id | created_at |
---|---|---|---|---|
int | int | int | int | Timestamp |
#環境
- Spring2.5.5
- gradle
- MyBatis2.2.0
- MySQL
#実装
##エンティティ
@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;
private List<TRoomUser> roomUserList;
}
@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;
}
ここでのポイントはMRoomクラス
に定義している**List<TRoomUser> roomUserList:
**です。
なぜなら、後ほどでてくるSQLの記述でJOINするため(アソシエーションを設定するため)に必要だからです。
そして、room_usersテーブルの親テーブルはroomsテーブルになる(1:多)ので、その実質クラスに関係性になるフィールドを設定しています。
##Mapper
@Mapper
public interface RoomMapper {
/**ログインユーザーのチャットルーム複数件取得*/
public List<MRoom> findLoginUserRooms(int currentUserId);
}
<?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>
<!-- マッピング定義(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>
<!-- ログインユーザーのチャットルーム複数件取得 -->
<select id="findLoginUserRooms" resultMap="room">
select
room_name
from rooms r
inner join room_users ru
on r.id = ru.room_id
inner join users u
on ru.current_user_id = u.id
where
ru.current_user_id = #{currentUserId,jdbcType=INTEGER}
</select>
</mapper>
ここでのポイントはxmlのマッピング定義部分です。
roomsテーブルのマッピング定義部分では、collectionタグ
でアソシエーションを明示しています。
そして、collectionタグ
でroom_usersテーブルを指定しるために、room_usersテーブルのマッピング定義も記述します。
そうすることで、JOINでテーブル結合を使用することができます。
次にSQL定義部分ですが、解説すると、今回の実装仕様は、「ログインユーザーが保有するチャットルーム一覧を表示する」なので、select文でroomsテーブルのroom_nameカラムを取得します。
その時の条件に「ログインユーザーが保有する」というものをSQLに加えるので、中間テーブルのcurrent_user_idとusersテーブルにidを参照する必要があります。
また、今回はroomsテーブルから参照するため、roomsテーブルからusersテーブルを参照するには中間テーブルを挟む必要があるので、それに合致するようにINNER JOINとON句で結合と結合条件を設定するようにします。
##Service
public interface RoomService {
/**ログインユーザーのチャットルーム複数件取得*/
public List<MRoom> getLoginUserRooms(@AuthenticationPrincipal UserDetailServiceImpll loginUser);
}
@Service
public class RoomServiceImpl implements RoomService {
@Autowired
private RoomMapper mapper;
/**
*ログインユーザーのチャットルーム複数件取得
*/
@Override
public List<MRoom> getLoginUserRooms(@AuthenticationPrincipal UserDetailServiceImpll loginUser) {
//ログインユーザーのユーザーIDを取得
int currentUserId = loginUser.getUser().getId();
return mapper.findLoginUserRooms(currentUserId);
};
}
ここで重要になるのは、ログインユーザーのユーザーIDを取得することです。
これを引数に渡すことで、SQLのWHERE句に値が渡され、テーブルの参照が行われます。
##Controller
@Controller
@RequestMapping("/")
@Slf4j
public class RoomController {
@Autowired
private RoomService roomService;
@GetMapping("/")
public String getRoomsIndex(Model model, @AuthenticationPrincipal UserDetailServiceImpll loginUser) {
//ログインユーザーのユーザー名取得
String username = loginUser.getUser().getName();
model.addAttribute("username", username);
List<MRoom> rooms =roomService.getLoginUserRooms(loginUser);
model.addAttribute("rooms", rooms);
return "rooms/index";
}
コントローラーでは一覧表示するため、List型の変数としてサービルロジックを定義して、Modelに登録することでビューに一覧を渡しています。
##View
<div class="rooms">
<div class="room" th:each="room: ${rooms}">
<div class="room-name">
<a th:href="@{/messages}" th:text="${room.roomName}">techroom1</a>
</div>
</div>
</div>
th:each属性
で一覧を一つずつ表示できるので、そのようにして、一覧表示してあげます。
#まとめ
特に重要なのが、エンティティとMapper、SQLの部分になるので、テーブル同士のリレーションを考えつつサービスを作成しないといけないです。
若干難しい部分もありますが、SpringBootはテーブル設計をしっかりと行えば、オリジナリティの高いSQLで様々なサービスを作成できますので、その辺を自分も今後はマスターしていければと思います。
Discussion