Spring Boot

MyBatis + SpringでWebアプリ(CRUD)を作成する

はじめに

MyBatisとSpring Bootを利用して「検索画面」「登録画面」「編集画面」の作成方法を紹介します。

本記事で作成する画面は次のとおりです。

■検索画面

検索画面のイメージ

■新規登録画面

新規登録画面のイメージ

■編集画面

編集画面のイメージ

開発環境

開発環境名称説明
開発言語Java人気の開発言語
開発ツールEclipseJavaでの定番開発ツール
フレームワークSpring Boot人気のSpringフレームワークをベースとしたフレームワーク
テンプレートエンジンThymeleafSpring Bootと相性が良いテンプレートエンジン
データベースMySQLフリーで利用できる人気データベース
ORM(オブジェクト関係マッピング)MyBatisDBとJavaオブジェクトのマッピングを行ってくれる人気のORM
画面デザインBootstrap作られた雛形を使うことで簡単にWebのデザインができるWebフレームワーク

スポンサーリンク

本記事で使用するテーブル定義は次のとおり。

物理名論理名データ型NOT NULL説明
idIDBIGINT主キー(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です。

検索画面のイメージ

「新規登録」ボタンを押下すると「ユーザー新規登録画面」に遷移。

登録画面では入力チェックを実行しているため「登録」ボタン押下時に入力チェックエラーの場合は、画面上部にエラーメッセージを表示しています。

入力チェックエラー

ユーザー情報を入力して「登録」ボタンを押下すると、ユーザー情報を登録し一覧画面に戻ります。

レコードの追加

検索結果一覧にある「削除」ボタンを押下するとユーザー情報を論理削除、「編集」ボタンを押下すると「ユーザー情報編集画面」に遷移します。

編集画面のイメージ

「保存」ボタンを押下すると、ユーザー情報を更新し一覧画面に戻ります。

helpful