MyBatis × SpringBoot チャットアプリでメッセージ・画像送信機能の作成
#はじめに
今回は、自作のチャットアプリで主機能となるメッセージ・画像送信機能の実装を行なったので、実装にあたり詰まったところと解説をアウトプットしていこうと思います。
#環境
- SpringBoot 2.5.5
- MySQL 5.6.51
- MyBatis 2.2.0
- thymeleaf
#ER図
#テーブル定義
usersテーブル
Column | Type | Options |
---|---|---|
id(PK・FK) | BIGINT | NOT NULL |
VARCHAR | NOT NULL | |
password | VARCHAR | NOT NULL |
password_conf | VARCHAR | NOT NULL |
role | VARCHAR | |
created_at | DATETIME | |
updated_at | DATETIME |
roomsテーブル
Column | Type | Options |
---|---|---|
id(PK・FK) | BIGINT | NOT NULL |
room_name | VARCHAR | NOT NULL |
created_at | DATETIME |
room_usersテーブル
Column | Type | Options |
---|---|---|
id(PK) | BIGINT | NOT NULL |
room_id(FK) | BIGINT | NOT NULL |
current_user_id(FK) | BIGINT | NOT NULL |
user_id | BIGINT | NOT NULL |
created_at | DATETIME |
messagesテーブル
Column | Type | Options |
---|---|---|
content | VARCHAR | NOT NULL |
image | LONGBLOB | |
room_id(FK) | BIGINT | NOT NULL |
user_id(FK) | BIGINT | NOT NULL |
created_at | DATETIME | |
updated_at | DATETIME |
#メッセージ送信機能実装
##Entity
@Data
public class TMessages {
private int id;
private String content;
private int roomId;
private int userId;
private byte[] image;
@DateTimeFormat(pattern = "yyyy_MM_dd HH:mm:ss")
private LocalDateTime createdAt;
@DateTimeFormat(pattern = "yyyy_MM_dd HH:mm:ss")
private LocalDateTime updatedAt;
ここでの注意点は、imageフィールド
の型をbyte[]
とすることです。
画像データはbyte[]で扱うことが多いので、このように定義します。
##Mapper
@Mapper
public interface MessageMapper {
/**メッセージ登録*/
public int insertOneMessage(TMessages message);
}
<?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.MessageMapper">
<!-- マッピング定義(messages) -->
<resultMap type="com.example.demo.entity.TMessages" id="message">
<id column="id" property="id"></id>
<result column="content" property="content"></result>
<result column="image" property="image"></result>
<result column="room_id" property="roomId"></result>
<result column="user_id" property="userId"></result>
<result column="created_at" property="createdAt"></result>
<result column="updated_at" property="updatedAt"></result>
</resultMap>
<!-- メッセージ登録 -->
<insert id="insertOneMessage">
insert into messages (
id,
content,
image,
room_id,
user_id,
created_at,
updated_at
) values (
#{id,jdbcType=INTEGER},
#{content,jdbcType=VARCHAR},
#{image,jdbcType=BLOB},
#{roomId,jdbcType=INTEGER},
#{userId,jdbcType=INTEGER},
#{createdAt,jdbcType=TIMESTAMP},
#{updatedAt,jdbcType=TIMESTAMP}
)
</insert>
</mapper>
ここは見たまんまかなと思います。
##Service
public interface MessageService {
/**メッセージ登録*/
public void insertMessage(TMessages message, MessageForm form, @AuthenticationPrincipal UserDetailServiceImpll loginUser, int roomId);
}
@Service
public class MessageServiceImpl implements MessageService {
@Autowired
private MessageMapper mapper;
@Transactional
@Override
public void insertMessage(TMessages message, MessageForm form,
@AuthenticationPrincipal UserDetailServiceImpll loginUser, int roomId) {
//フォームから入力値取得
message.setContent(form.getContent());
//チャットルームのID設定
message.setRoomId(roomId);
//ログインユーザーのID取得
int userId = loginUser.getUser().getId();
//ログインユーザーID設定
message.setUserId(userId);
//現在時刻の取得
LocalDateTime now = LocalDateTime.now();
message.setCreatedAt(now);
message.setUpdatedAt(now);
//メッセージ登録
mapper.insertOneMessage(message);
}
}
ここも見たまんまなので、解説は割愛させていただきます。
なお、@AuthenticationPrincipalアノテーション
の詳しい実装については、以下の記事にまとめてあるので、よかったら、読んでみてください
##form
@Data
public class MessageForm {
@NotBlank
private String content;
//画像入力時
private MultipartFile multiPartFile;
}
こちらがフォームクラスになります。
普通のフォームクラスなら、入力値とエンティティのフィールド名を合わせますが、画像データの入力となるため、MultipartFile型
を指定したフィールドを作成しています。
##Controller
@Controller
@RequestMapping("/")
@Slf4j
public class MessageController {
@Autowired
private MessageService service;
@Autowired
private RoomService roomService;
@PostMapping("/rooms/{roomId}/message")
public String postMessage(Model model, TMessages message, @Validated @ModelAttribute("form") MessageForm form,
BindingResult result, @AuthenticationPrincipal UserDetailServiceImpll loginUser,
@PathVariable("roomId") int roomId) throws IOException {
//チャットルーム1件取得
MRoom room = roomService.getRoomOne(roomId);
if (result.hasErrors()) {
//NG:メッセージ送信画面にリダイレクト
return "redirect:/rooms/{roomId}";
}
log.info(form.toString());
//画像データをフォームから取得し設定
message.setImage(form.getMultiPartFile().getBytes());
service.insertMessage(message, form, loginUser, room.getId());
return "redirect:/rooms/{roomId}";
}
}
私の場合は、このコントローラー部分の実装で詰まりました。
というのも、messagesテーブルには、外部キーで参照しているroom_idの登録が必要だったため、その値の取得が中々うまくいかず、サービスクラスと行ったり来たりを繰り返してました(笑)
ポイントとなるのは@PathVariableアノテーション
の部分です。
より詳しく言うと、「roomsテーブルのレコード1件取得するようなロジックにしないといけない」ということです。
例えば、roomsテーブルの値を1件取得するだけなら、@PathVariable("id") int id
という記述でもroomsテーブルの値自体は取得できます。
ただ、messagesテーブルに値を登録するとなると、上記の記述だと、1回目は登録できても、2回目のメッセージ登録時にエラーが出ます。
この理由としては、roomsテーブルのidカラム(PK)を参照しているがために、messagesテーブルのidカラム(PK)にも参照しているroomsテーブルのidカラムの値を登録してしまうためです。
つまり、一意であるはずの主キーカラムに同じ値が登録され続けてしまうという事態になり、**Duplicate entry 'x(数値)' for key 'PRIMARY'**とエラーが出ます。
ここで、チャットルーム詳細画面遷移のロジックを実装している、コントローラーを見てみましょう。
@Controller
@RequestMapping("/")
@Slf4j
public class RoomController {
@Autowired
private UserService userService;
@Autowired
private RoomService roomService;
@Autowired
private RoomUserService roomUserService;
@GetMapping("/rooms/{roomId}")
public String getRoom(Model model, @AuthenticationPrincipal UserDetailServiceImpll loginUser, @PathVariable("roomId") int id, @ModelAttribute("form") MessageForm form) {
//ログインユーザーの情報を取得
String username = loginUser.getUser().getName();
int loginUserId = loginUser.getUser().getId();
model.addAttribute("username", username);
//ログインユーザーと選択されたユーザーが保有するチャットルームを取得
List<MRoom> rooms = roomService.getLoginUserRooms(loginUser);
model.addAttribute("rooms", rooms);
//room_usersテーブルのレコード(1件)取得
TRoomUser roomUser = roomUserService.getRoomUserOne(id);
//room_usersに登録されているログインユーザーのIDを取得
int currentUserId = roomUser.getCurrentUserId();
//room_usersに登録されているチャットするユーザーのIDを取得
int userId = roomUser.getUserId();
//ログインユーザーとroom_usersのログインユーザーID、またはログインユーザーとチャット選択されたユーザーのIDが等しい時メッセー送信画面に遷移する
if(loginUserId == currentUserId || loginUserId == userId) {
return "redirect:/rooms/{roomId}";
}
return "redirect:/";
}
}
MessageControllerと@PathVariableアノテーション
部分を比較すると違っているのがわかると思います。
こちらは、Roomsのコントローラークラスであるために、@PathVariable("roomId") int id
としていますが、messagesのコントローラークラスでは@PathVariable("roomId") int roomId
としています。
これで、明確に参照しているカラムを分けることができるため、主キーがダブるエラーは発生しなくなります。
具体的には、RoomControllerはroomsのidカラムを参照し、MessageControllerではmessageのroom_idカラムを参照するようにしています。
あとはサービスクラスのメソッドを呼び出せば機能実装が完了するわけですが、あと1つ注意点があります。
それは、サービスクラスのメソッド呼び出し時の引数に room.getId()
を渡してあげることです。
さらにいうと、その前に、RoomServiceクラスのメソッドを呼び出し、roomsのレコードを1件取得するロジックを加える必要もあります。
そうしないと、メッセージ送信時にroom_idカラムがnullというエラーが出るので、記述する必要があります。
##View
<form class="form" th:action="@{'/rooms/'+${roomId}+'/message'}" method="post" th:object="${form}" enctype="multipart/form-data">
<div class="form-input">
<input th:field="*{content}" th:errorclass="is-invalid" class="form-message" placeholder="type a message">
<div class="invalid-feedback" th:errors="*{content}"></div>
<label class="form-image">
<span class="image-file">画像</span>
<input th:field="*{multiPartFile}" type="file" class="hidden">
</label>
</div>
<input class="form-submit" type="submit" name="commit" value="送信">
</form>
ポイントは画像投稿の部分です。formタグ
のenctype属性
をenctype="multipart/form-data"
とすることで.pngなどの画像データを送信できるようになります。
あとは、formクラスで定義しているフィールドをinputタグに指定するだけで画像の登録はできます。
以上で解説は終了です。
最後までありがとうございました。
#参考
Discussion