目次
はじめに
MyBatisとSpring Bootを利用して「検索画面」「登録画面」「編集画面」の作成方法を紹介します。
本記事で作成する画面は次のとおりです。
■検索画面
■新規登録画面
■編集画面
開発環境
開発環境 | 名称 | 説明 |
開発言語 | Java | 人気の開発言語 |
開発ツール | Eclipse | Javaでの定番開発ツール |
フレームワーク | Spring Boot | 人気のSpringフレームワークをベースとしたフレームワーク |
テンプレートエンジン | Thymeleaf | Spring Bootと相性が良いテンプレートエンジン |
データベース | MySQL | フリーで利用できる人気データベース |
ORM(オブジェクト関係マッピング) | MyBatis | DBとJavaオブジェクトのマッピングを行ってくれる人気のORM |
画面デザイン | Bootstrap | 作られた雛形を使うことで簡単にWebのデザインができるWebフレームワーク |
スポンサーリンク
本記事で使用するテーブル定義は次のとおり。
物理名 | 論理名 | データ型 | NOT NULL | 説明 |
id | ID | BIGINT | 〇 | 主キー(AUTO_INCREMENT) |
name | 名前 | VARCHAR(100) | 〇 | ユーザーの名前 |
address | 住所 | VARCHAR(255) | ユーザーの住所 | |
phone | 電話番号 | VARCHAR(50) | ユーザーの電話番号 | |
update_date | 更新日時 | DATETIME | 〇 | 最終更新日時 |
create_date | 作成日時 | DATETIME | 〇 | 登録日時 |
delete_date | 削除日時 | DATETIME | 論理削除した日時 |
■CREATE文
CREATE TABLE `sampledb`.`userinfo` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `name` VARCHAR(100) NOT NULL, `address` VARCHAR(255) NULL, `phone` VARCHAR(50) NULL, `update_date` DATETIME NOT NULL, `create_date` DATETIME NOT NULL, `delete_date` DATETIME NULL, PRIMARY KEY (`id`));
ディレクトリ構成
MybatisSample | |___src.main.java | |___com.example.demo | | | |___controller | | | | | |__UserInfoController.java | | | |____dto | | | | | |__UserAddRequest.java | | | | | |__UserSearchRequest.java | | | | | |__UserUpdateRequest.java | | | |___entity | | | | | |___UserInfo.java | | | |___dao | | | | | |___UserInfoMapper.java | | | |___service | | | |___UserInfoService.java | |___src.main.resources | |___com.example.demo | | | |___dao | | | |__UserInfoMapper.xml | |___templates | | | |___common | | | | | |___head.html | | | |___user | | | |___add.html | | | |___edit.html | | | |___search.html | |___application.properties
設定ファイル
使用しているライブライとその依存関係は次のとおり。
■pom.xml
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.2.2</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> <scope>provided</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> </dependencies>
データベースの接続情報とmybatisの設定は次のとおり。(mybatisで自動的にスネーク→キャメルに変換してくれる設定をtrueにしています)
■application.properties
spring.datasource.url=jdbc:mysql://localhost/sampledb spring.datasource.username=root spring.datasource.password=password spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver mybatis.configuration.map-underscore-to-camel-case=true
バックエンド(サーバ)側のソースコード
コントローラークラス
画面とビジネスロジックを繋ぐコントローラークラスは次のとおり。
package com.example.demo.controller; import java.util.ArrayList; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.validation.BindingResult; import org.springframework.validation.ObjectError; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import com.example.demo.dto.UserAddRequest; import com.example.demo.dto.UserSearchRequest; import com.example.demo.dto.UserUpdateRequest; import com.example.demo.entity.UserInfo; import com.example.demo.service.UserInfoService; /** * ユーザー情報 Controller */ @Controller public class UserInfoController { /** * ユーザー情報 Service */ @Autowired private UserInfoService userInfoService; /** * ユーザー情報一覧画面を表示 * @param model Model * @return ユーザー情報一覧画面 */ @GetMapping(value = "/user/list") public String displayList(Model model) { List<UserInfo> userList = userInfoService.findAll(); model.addAttribute("userlist", userList); model.addAttribute("userSearchRequest", new UserSearchRequest()); return "user/search"; } /** * ユーザー新規登録画面を表示 * @param model Model * @return ユーザー情報一覧画面 */ @GetMapping(value = "/user/add") public String displayAdd(Model model) { model.addAttribute("userAddRequest", new UserAddRequest()); return "user/add"; } /** * ユーザー編集画面を表示 * @param id ユーザーID * @param model Model * @return ユーザー編集画面 */ @GetMapping("/user/{id}/edit") public String displayEdit(@PathVariable Long id, Model model) { UserInfo user = userInfoService.findById(id); UserUpdateRequest userUpdateRequest = new UserUpdateRequest(); userUpdateRequest.setId(user.getId()); userUpdateRequest.setId(user.getId()); userUpdateRequest.setName(user.getName()); userUpdateRequest.setPhone(user.getPhone()); userUpdateRequest.setAddress(user.getAddress()); model.addAttribute("userUpdateRequest", userUpdateRequest); return "user/edit"; } /** * ユーザー情報検索 * @param userSearchRequest リクエストデータ * @param model Model * @return ユーザー情報一覧画面 */ @RequestMapping(value = "/user/search", method = RequestMethod.POST) public String search(@ModelAttribute UserSearchRequest userSearchRequest, Model model) { List<UserInfo> userList = userInfoService.search(userSearchRequest); model.addAttribute("userlist", userList); return "user/search"; } /** * ユーザー情報削除(論理削除) * @param id ユーザーID * @param model Model * @return ユーザー情報一覧画面 */ @GetMapping("/user/{id}/delete") public String delete(@PathVariable Long id, Model model) { // ユーザー情報の削除 userInfoService.delete(id); return "redirect:/user/list"; } /** * ユーザー新規登録 * @param userRequest リクエストデータ * @param model Model * @return ユーザー情報一覧画面 */ @RequestMapping(value = "/user/create", method = RequestMethod.POST) public String create(@Validated @ModelAttribute UserAddRequest userRequest, BindingResult result, Model model) { if (result.hasErrors()) { // 入力チェックエラーの場合 List<String> errorList = new ArrayList<String>(); for (ObjectError error : result.getAllErrors()) { errorList.add(error.getDefaultMessage()); } model.addAttribute("validationError", errorList); return "user/add"; } // ユーザー情報の登録 userInfoService.save(userRequest); return "redirect:/user/list"; } /** * ユーザー更新 * @param userRequest リクエストデータ * @param model Model * @return ユーザー情報詳細画面 */ @RequestMapping(value = "/user/update", method = RequestMethod.POST) public String update(@Validated @ModelAttribute UserUpdateRequest userUpdateRequest, BindingResult result, Model model) { if (result.hasErrors()) { List<String> errorList = new ArrayList<String>(); for (ObjectError error : result.getAllErrors()) { errorList.add(error.getDefaultMessage()); } model.addAttribute("validationError", errorList); return "user/edit"; } // ユーザー情報の更新 userInfoService.update(userUpdateRequest); return "redirect:/user/list"; } }
サービスクラス
ビジネスロジックを記述するサービスクラスは次のとおり。
[com.example.demo.service.UserInfoService.java]
package com.example.demo.service; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import com.example.demo.dao.UserInfoMapper; import com.example.demo.dto.UserAddRequest; import com.example.demo.dto.UserSearchRequest; import com.example.demo.dto.UserUpdateRequest; import com.example.demo.entity.UserInfo; /** * ユーザー情報 Service */ @Service public class UserInfoService { /** * ユーザー情報 Mapper */ @Autowired private UserInfoMapper userInfoMapper; /** * ユーザー情報全件検索 * @return 検索結果 */ public List<UserInfo> findAll() { return userInfoMapper.findAll(); } /** * ユーザー情報主キー検索 * @return 検索結果 */ public UserInfo findById(Long id) { return userInfoMapper.findById(id); } /** * ユーザー情報検索 * @param userSearchRequest リクエストデータ * @return 検索結果 */ public List<UserInfo> search(UserSearchRequest userSearchRequest) { return userInfoMapper.search(userSearchRequest); } /** * ユーザ情報登録 * @param userAddRequest リクエストデータ */ public void save(UserAddRequest userAddRequest) { userInfoMapper.save(userAddRequest); } /** * ユーザ情報更新 * @param userEditRequest リクエストデータ */ public void update(UserUpdateRequest userUpdateRequest) { userInfoMapper.update(userUpdateRequest); } /** * ユーザー情報論理削除 * @param id */ public void delete(Long id) { userInfoMapper.delete(id); } }
エンティティクラス
テーブルのエンティティクラスは次のとおり。
[com.example.demo.entity.UserInfo.java]
package com.example.demo.entity; import java.io.Serializable; import java.util.Date; import lombok.Data; /** * ユーザー情報 Entity */ @Data public class UserInfo implements Serializable { /** * ID */ private Long id; /** * 名前 */ private String name; /** * 住所 */ private String address; /** * 電話番号 */ private String phone; /** * 更新日時 */ private Date updateDate; /** * 登録日時 */ private Date createDate; /** * 削除日時 */ private Date deleteDate; }
DAO(Data Access Object)
SQL文を呼び出すためのDAOクラスは以下のとおり。
[com.example.demo.dao.UserInfoMapper.java]
package com.example.demo.dao; import java.util.List; import org.apache.ibatis.annotations.Mapper; import com.example.demo.dto.UserAddRequest; import com.example.demo.dto.UserSearchRequest; import com.example.demo.dto.UserUpdateRequest; import com.example.demo.entity.UserInfo; /** * ユーザー情報 Mapper */ @Mapper public interface UserInfoMapper { /** * ユーザー情報全件検索 * @param user 検索用リクエストデータ * @return 検索結果 */ List<UserInfo> findAll(); /** * ユーザー情報主キー検索 * @param id 主キー * @return 検索結果 */ UserInfo findById(Long id); /** * ユーザー情報検索 * @param user 検索用リクエストデータ * @return 検索結果 */ List<UserInfo> search(UserSearchRequest user); /** * ユーザー情報登録 * @param userRequest 登録用リクエストデータ */ void save(UserAddRequest userRequest); /** * ユーザー情報更新 * @param userUpdateRequest 更新用リクエストデータ */ void update(UserUpdateRequest userUpdateRequest); /** * ユーザー情報の論理削除 * @param id ID */ void delete(Long id); }
SQL文(XMLファイル)
実行するSQL文は次のとおり。
[resources.com.example.demo.dao.UserInfoMapper.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.dao.UserInfoMapper"> <!-- 全件検索 --> <select id="findAll" resultType="com.example.demo.entity.UserInfo"> SELECT * FROM userinfo WHERE delete_date IS NULL </select> <!-- 主キー検索 --> <select id="findById" resultType="com.example.demo.entity.UserInfo"> SELECT * FROM userinfo WHERE id=#{id} AND delete_date IS NULL </select> <!-- 条件指定検索 --> <select id="search" resultType="com.example.demo.entity.UserInfo"> SELECT * FROM userinfo WHERE delete_date IS NULL <if test="id != null and id != ''"> AND id = #{id} </if> <if test="name != null and name != ''"> AND name LIKE CONCAT('%', #{name}, '%') </if> </select> <!-- 新規登録 --> <insert id="save"> INSERT INTO userInfo (name, address, phone, update_date, create_date) VALUES (#{name}, #{address}, #{phone}, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) </insert> <!-- 更新 --> <update id="update"> UPDATE userinfo SET name = #{name}, address = #{address}, phone = #{phone}, update_date = CURRENT_TIMESTAMP WHERE id = #{id} </update> <!-- 論理削除 --> <update id="delete"> UPDATE userinfo SET delete_date = CURRENT_TIMESTAMP WHERE id = #{id} </update> </mapper>
XMLファイルは、src/main/resources配下にUserInfoMapperクラス(Mapperインタフェース)と同じパスの構成で配置します。同じパスで配置することで、MyBatisが自動でマッピングファイルを読み込んでくれます。
同じパスではなく自由な場所に配置したい場合は、「yaml」もしくは「properties」に「mybatis.mapper-locations」でXMLファイルの格納位置を指定します。
mybatis.mapper-locations=classpath*:sample/*.xml
DTO(Data Transfer Object)
データの受け渡しで利用するDTOクラスは次のとおり。
[com.example.demo.dto.UserAddRequest]
package com.example.demo.dto; import java.io.Serializable; import javax.validation.constraints.NotEmpty; import javax.validation.constraints.Pattern; import javax.validation.constraints.Size; import lombok.Data; /** * ユーザー情報登録 リクエストデータ */ @Data public class UserAddRequest implements Serializable { /** * 名前 */ @NotEmpty(message = "名前を入力してください") @Size(max = 100, message = "名前は100桁以内で入力してください") private String name; /** * 住所 */ @Size(max = 255, message = "住所は255桁以内で入力してください") private String address; /** * 電話番号 */ @Pattern(regexp = "0\\d{1,4}-\\d{1,4}-\\d{4}", message = "電話番号の形式で入力してください") private String phone; }
[com.example.demo.dto.UserSearchRequest]
package com.example.demo.dto; import java.io.Serializable; import lombok.Data; /** * ユーザー情報 検索用リクエストデータ */ @Data public class UserSearchRequest implements Serializable { /** * ユーザーID */ private String id; /** * ユーザー名 */ private String name; }
[com.example.demo.dto.UserUpdateRequest]
package com.example.demo.dto; import java.io.Serializable; import javax.validation.constraints.NotNull; import lombok.Data; import lombok.EqualsAndHashCode; /** * ユーザー情報更新リクエストデータ * */ @Data @EqualsAndHashCode(callSuper = false) public class UserUpdateRequest extends UserAddRequest implements Serializable { /** * ユーザーID */ @NotNull private Long id; }
スポンサーリンク
フロントエンド(クライアント)側のソースコード
共通ヘッダ(head.html)
共通のヘッダは次のとおり。ヘッダ情報は同じなので共通で管理しています。
[templates.common.head.html]
<head th:fragment="head_fragment(title, scripts, links)"> <title th:text="${title}"></title> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous"> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script> <meta charset="utf-8" /> </head>
ユーザー情報一覧画面(search.html)
ユーザー情報一覧画面のHTMLは次のとおり。
[templates.user.search.html]
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"> <head th:replace="common/head :: head_fragment(title = 'ユーザー情報一覧', scripts = ~{::script}, links = ~{::link})"></head> <body> <div class="container"> <h1>ユーザー情報一覧</h1> <div class="float-end"> <a th:href="@{/user/add}" class="btn btn-primary">新規登録はこちら</a> </div> <form th:action="@{/user/search}" th:object="${userSearchRequest}" th:method="post"> <div class="form-group"> <label for="id">ID</label> <input type="text" class="w-25 form-control" th:field="*{id}"> <label for="name">名前</label> <input type="text" class="w-50 form-control" th:field="*{name}"> </div><br /> <button type="submit" class="btn btn-primary">検索</button> </form> <div th:if="${userlist}"> <table class="table table-striped"> <thead> <tr> <th>ID</th> <th>名前</th> <th>住所</th> <th>電話番号</th> <th>更新日時</th> <th>登録日時</th> <th>削除日時</th> <th></th> </tr> </thead> <tbody> <tr th:each="user : ${userlist}" th:object="${user}" class="align-middle"> <td th:text="*{id}"></td> <td th:text="*{name}"></td> <td th:text="*{address}"></td> <td th:text="*{phone}"></td> <td th:text="${#dates.format(user.updateDate, 'yyyy/MM/dd')}"></td> <td th:text="${#dates.format(user.createDate, 'yyyy/MM/dd')}"></td> <td th:text="${#dates.format(user.deleteDate, 'yyyy/MM/dd')}"></td> <td> <a th:href="@{/user/{id}/edit(id=*{id})}" class="btn btn-primary">編集</a> <a th:href="@{/user/{id}/delete(id=*{id})}" class="btn btn-secondary">削除</a> </td> </tr> </tbody> </table> </div> </div> </body> </html>
ユーザー新規登録画面(add.html)
ユーザー新規登録画面のHTMLは次のとおり。
[templates.user.add.html]
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"> <head th:replace="common/head :: head_fragment(title = 'ユーザー新規登録', scripts = ~{::script}, links = ~{::link})"></head> <body> <div class="container"> <div th:if="${validationError}" th:each="error : ${validationError}"> <label class="text-danger" th:text="${error}"></label> </div> <h1>ユーザー新規登録</h1> <form th:action="@{/user/create}" th:object="${userAddRequest}" th:method="post"> <div class="form-group"> <label>名前:<span class="text-danger">※</span></label> <input type="text" th:field="*{name}" class="form-control"> </div> <div class="form-group"> <label>住所:</label> <input type="text" th:field="*{address}" class="form-control"> </div> <div class="form-group"> <label>電話番号:<span class="text-danger">※</span></label> <input type="text" th:field="*{phone}" class="form-control"> </div> <br /> <div class="text-center"> <input type="submit" value="登録" class="btn btn-primary"> <a href="/user/list" class="btn btn-secondary">キャンセル</a> </div> </form> </div> </body> </html>
ユーザー情報編集(edit.html)
ユーザー情報編集画面のHTMLは次のとおり。
[templates.user.edit.html]
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"> <head th:replace="common/head :: head_fragment(title = 'ユーザー情報編集', scripts = ~{::script}, links = ~{::link})"></head> <body> <div class="container"> <div th:if="${validationError}" th:each="error : ${validationError}"> <label class="text-danger" th:text="${error}"></label> </div> <h1>ユーザー情報編集</h1> <form th:action="@{/user/update}" th:object="${userUpdateRequest}" th:method="post"> <input type="hidden" th:field="*{id}" /> <div> <div class="row mx-md-n5"> <div class="col-2 pt-3 border bg-light"> 名前<span class="text-danger">※</span> </div> <div class="col py-2 border"> <input type="text" class="form-control" th:field="*{name}"> </div> </div> <div class="row mx-md-n5"> <div class="col-2 pt-3 border bg-light">住所</div> <div class="col py-2 border"> <input type="text" class="form-control" th:field="*{address}"> </div> </div> <div class="row mx-md-n5"> <div class="col-2 pt-3 border bg-light"> 電話番号<span class="text-danger">※</span> </div> <div class="col py-2 border"> <input type="text" class="form-control" th:field="*{phone}"> </div> </div> </div> <br /> <div class="text-center"> <input type="submit" class="btn btn-primary" value="保存"> <a href="/user/list" class="btn btn-secondary">キャンセル</a> </div> </form> </div> </body> </html>
スポンサーリンク
動作確認
Spring Bootプロジェクトを起動して「http://localhost:8080/user/list」にアクセスします。
一覧画面が表示されればOKです。
「新規登録」ボタンを押下すると「ユーザー新規登録画面」に遷移。
登録画面では入力チェックを実行しているため「登録」ボタン押下時に入力チェックエラーの場合は、画面上部にエラーメッセージを表示しています。
ユーザー情報を入力して「登録」ボタンを押下すると、ユーザー情報を登録し一覧画面に戻ります。
検索結果一覧にある「削除」ボタンを押下するとユーザー情報を論理削除、「編集」ボタンを押下すると「ユーザー情報編集画面」に遷移します。
「保存」ボタンを押下すると、ユーザー情報を更新し一覧画面に戻ります。