3
2

More than 1 year has passed since last update.

SpringBoot × MyBatis 中間テーブルへ登録する方法

Posted at

はじめに

今回は、自作している学習用チャットアプリで中間テーブルを実装したところ、かなり詰まったので、中間テーブルへの登録方法を備忘録として残そうと思います。
同じように悩んでいる方がいたら参考にしてみてください。

アプリ概要

ログインユーザーが他の登録ユーザーを選択してチャットを開始するという、DMのようなものが主機能のアプリです。

  • リレーション test.png

usersテーブルとroomsテーブルが多対多の関係性のため、N+1問題が懸念されるので、中間テーブルとしてroom_usersテーブルを作成しました。

環境

  • Spring2.5.5
  • gradle
  • MyBatis2.2.0
  • MySQL

中間テーブルへのinsertを実装

今回の実装では、

①チャットルーム作成時に、roomsテーブルにチャットルーム情報を登録する
②作成者のユーザーIDと作成者が選択したユーザーのユーザーID、作成したチャットルームのIDを中間テーブルに登録する

上記2つの処理が必要です。

ということで実装内容を解説します。

entity

MUser.java
@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;

}
MRoom.java
@Data
public class MRoom {

    private int id;
    private String roomName;

    @DateTimeFormat(pattern = "yyyy_MM_dd HH:mm:ss")
    private LocalDateTime createdAt;

}
TRoomUser.java
@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

RoomMapper.java
@Mapper
public interface RoomMapper {

    /**チャットルーム登録*/
    public int insertOneRoom(MRoom room);

}
RoomMapper.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と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

RoomUserMapper.java
@Mapper
public interface RoomUserMapper {

    public int insertRoomUser(TRoomUser roomUser);

}
RoomUserMapper.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と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

RoomService.java
public interface RoomService {

    /**チャットルーム登録*/
    public void insertRoom(MRoom room, RoomForm form);

}
RoomServiceImpl.java
@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

RoomForm.java
@Data
public class RoomForm {

    @NotBlank
    private String roomName;

    private int userId;
}

チャットルーム登録画面のフォームクラスです。
自分の実装はプルダウンから、選択したユーザーとチャットができる仕様のため、プルダウンに入力されるユーザーの情報を取得するためuserIdを定義しています。

RoomUserService

RoomUserService
public interface RoomUserService {

    /**room_user登録*/
    public void registRoomUser(RoomForm form, TRoomUser roomUser, @AuthenticationPrincipal UserDetailServiceImpll loginUser);
}
RoomUserServiceImpl.java
@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テーブルの登録と同時に、中間テーブルへも値を登録できるというロジックが完成します。

なお、ログインユーザーの取得については、以下の記事で詳しく解説しているので、参考にしてみてください。

SpringSecurityとSpringBootでログイン認証と投稿機能を実装する

RoomController

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

UserMapper.java
@Mapper
public interface UserMapper {

    /**ログインユーザー以外のユーザー取得(複数件)*/
    public List<MUser> findMany(int id);
}
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と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

UserService.java
public interface UserService {

/**ログインユーザー以外のユーザー取得(複数件)*/
    public List<MUser> getUsers(int id);
}
UserServiceImpl.java
@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserMapper mapper;

/**ログインユーザー以外のユーザー取得(複数件)*/
    @Override
    public  List<MUser> getUsers(int id) {
        return mapper.findMany(id);
    }
}

View

new.html
<!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の文献は非常に少ないので、同志がいたら参考になれば幸いかと思います。

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2