Spring Boot DB連携

Spring BootのWEB画面上でnull許可項目のDB更新機能を追加してみた(ソースコード編)

今回も引き続き、Spring BootのWEB画面上でDB更新項目がNULL更新できるパターンの実装について述べる。ここでは、具体的なサンプルプログラムのソースコードを共有する。

前提条件

下記記事を参照のこと。

Spring BootのWEB画面上でnull許可項目のDB更新機能を追加してみた(完成イメージと前提条件)これまで本ブログで、Spring BootのWEB画面で、MyBatisによるDB更新機能を何度か取り上げてきたが、DB更新はNull更...

作成したサンプルプログラムの内容

作成したサンプルプログラムの構成は以下の通り。
サンプルプログラムの構成
なお、上図の赤枠は、前提条件に記載したソースコードと比較し、変更になったソースコードを示す。赤枠のソースコードについては今後記載する。

まず、DemoForm・UserDataに、項目「memo」を追加する対応を行った。そのソースコードは以下の通り。

package com.example.demo;

import lombok.Data;
import javax.validation.constraints.NotEmpty;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * Formオブジェクトのクラス
 */
@Data
public class DemoForm {

    /** ID */
    private String id;

    /** 名前 */
    @NotEmpty
    private String name;

    /** 生年月日_年 */
    private String birthYear;

    /** 生年月日_月 */
    private String birthMonth;

    /** 生年月日_日 */
    private String birthDay;

    /** 性別 */
    @NotEmpty
    private String sex;

    /** メモ */
    private String memo;

    /** 確認チェック */
    @NotEmpty
    private String checked;

    /** 性別(文字列) */
    private String sex_value;

    /** 生年月日_月のMapオブジェクト */
    public Map<String,String> getMonthItems(){
        Map<String, String> monthMap = new LinkedHashMap<String, String>();
        for(int i = 1; i <= 12; i++){
            monthMap.put(String.valueOf(i), String.valueOf(i));
        }
        return monthMap;
    }

    /** 生年月日_日のMapオブジェクト */
    public Map<String,String> getDayItems(){
        Map<String, String> dayMap = new LinkedHashMap<String, String>();
        for(int i = 1; i <= 31; i++){
            dayMap.put(String.valueOf(i), String.valueOf(i));
        }
        return dayMap;
    }

    /** 性別のMapオブジェクト */
    public Map<String,String> getSexItems(){
        Map<String, String> sexMap = new LinkedHashMap<String, String>();
        sexMap.put("1", "男");
        sexMap.put("2", "女");
        return sexMap;
    }

}
package com.example.demo;

import lombok.Data;

/**
 * ユーザーデータテーブル(user_data)アクセス用エンティティ
 */
@Data
public class UserData {

    /** ID */
    private long id;

    /** 名前 */
    private String name;

    /** 生年月日_年 */
    private int birthY;

    /** 生年月日_月 */
    private int birthM;

    /** 生年月日_日 */
    private int birthD;

    /** 性別 */
    private String sex;

    /** メモ */
    private String memo;

    /** 性別(文字列) */
    private String sex_value;

}



次に、サービスクラスにmemoをDemoForm, UserData間で受け渡す修正を行った。そのソースコードは以下の通りで、「getDemoForm」「getUserData」メソッドを変更している。

package com.example.demo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.BindingResult;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

@Service
public class DemoServiceImpl implements DemoService{

    /**
     * ユーザーデータテーブル(user_data)へアクセスするマッパー
     */
    @Autowired
    private UserDataMapper mapper;

    /**
     * {@inheritDoc}
     */
    @Override
    public List<DemoForm> demoFormList(SearchForm searchForm) {
        List<DemoForm> demoFormList = new ArrayList<>();
        //ユーザーデータテーブル(user_data)から検索条件に合うデータを取得する
        Collection<UserData> userDataList = mapper.findBySearchForm(searchForm);
        for (UserData userData : userDataList) {
            demoFormList.add(getDemoForm(userData));
        }
        return demoFormList;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public DemoForm findById(String id) {
        Long longId = stringToLong(id);
        UserData userData = mapper.findById(longId);
        return getDemoForm(userData);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Transactional(readOnly = false)
    public void deleteById(String id){
        Long longId = stringToLong(id);
        mapper.deleteById(longId);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Transactional(readOnly = false)
    public void createOrUpdate(DemoForm demoForm){
        //更新・追加処理を行うエンティティを生成
        UserData userData = getUserData(demoForm);
        //追加・更新処理
        if(demoForm.getId() == null){
            userData.setId(mapper.findMaxId() + 1);
            mapper.create(userData);
        }else{
            mapper.update(userData);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String checkForm(DemoForm demoForm, BindingResult result, String normalPath){
        //formオブジェクトのチェック処理を行う
        if(result.hasErrors()){
            //エラーがある場合は、入力画面のままとする
            return "input";
        }
        //生年月日の日付チェック処理を行う
        //エラーがある場合は、エラーメッセージ・エラーフィールドの設定を行い、
        //入力画面のままとする
        int checkDate = DateCheckUtil.checkDate(demoForm.getBirthYear()
                , demoForm.getBirthMonth(), demoForm.getBirthDay());
        switch(checkDate){
            case 1:
                //生年月日_年が空文字の場合のエラー処理
                result.rejectValue("birthYear", "validation.date-empty"
                        , new String[]{"生年月日_年"}, "");
                return "input";
            case 2:
                //生年月日_月が空文字の場合のエラー処理
                result.rejectValue("birthMonth", "validation.date-empty"
                        , new String[]{"生年月日_月"}, "");
                return "input";
            case 3:
                //生年月日_日が空文字の場合のエラー処理
                result.rejectValue("birthDay", "validation.date-empty"
                        , new String[]{"生年月日_日"}, "");
                return "input";
            case 4:
                //生年月日の日付が不正な場合のエラー処理
                result.rejectValue("birthYear", "validation.date-invalidate");
                //生年月日_月・生年月日_日は、エラーフィールドの設定を行い、
                //メッセージを空文字に設定している
                result.rejectValue("birthMonth", "validation.empty-msg");
                result.rejectValue("birthDay", "validation.empty-msg");
                return "input";
            case 5:
                //生年月日の日付が未来日の場合のエラー処理
                result.rejectValue("birthYear", "validation.date-future");
                //生年月日_月・生年月日_日は、エラーフィールドの設定を行い、
                //メッセージを空文字に設定している
                result.rejectValue("birthMonth", "validation.empty-msg");
                result.rejectValue("birthDay", "validation.empty-msg");
                return "input";
            default:
                //性別が不正に書き換えられていないかチェックする
                if(!demoForm.getSexItems().keySet().contains(demoForm.getSex())){
                    result.rejectValue("sex", "validation.sex-invalidate");
                    return "input";
                }
                //エラーチェックに問題が無いので、正常時の画面遷移先に遷移
                return normalPath;
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String checkSearchForm(SearchForm searchForm, BindingResult result){
        int checkDate =DateCheckUtil.checkSearchForm(searchForm);
        switch (checkDate){
            case 1:
                //生年月日_fromが不正な場合のエラー処理
                result.rejectValue("fromBirthYear", "validation.date-invalidate-from");
                result.rejectValue("fromBirthMonth", "validation.empty-msg");
                result.rejectValue("fromBirthDay", "validation.empty-msg");
                return "search";
            case 2:
                //生年月日_toが不正な場合のエラー処理
                result.rejectValue("toBirthYear", "validation.date-invalidate-to");
                result.rejectValue("toBirthMonth", "validation.empty-msg");
                result.rejectValue("toBirthDay", "validation.empty-msg");
                return "search";
            case 3:
                //生年月日_from>生年月日_toの場合のエラー処理
                result.rejectValue("fromBirthYear", "validation.date-invalidate-from-to");
                result.rejectValue("fromBirthMonth", "validation.empty-msg");
                result.rejectValue("fromBirthDay", "validation.empty-msg");
                result.rejectValue("toBirthYear", "validation.empty-msg");
                result.rejectValue("toBirthMonth", "validation.empty-msg");
                result.rejectValue("toBirthDay", "validation.empty-msg");
                return "search";
            default:
                //正常な場合はnullを返却
                return null;
        }
    }

    /**
     * DemoFormオブジェクトに引数のユーザーデータの各値を設定する
     * @param userData ユーザーデータ
     * @return DemoFormオブジェクト
     */
    private DemoForm getDemoForm(UserData userData){
        if(userData == null){
            return null;
        }
        DemoForm demoForm = new DemoForm();
        demoForm.setId(String.valueOf(userData.getId()));
        demoForm.setName(userData.getName());
        demoForm.setBirthYear(String.valueOf(userData.getBirthY()));
        demoForm.setBirthMonth(String.valueOf(userData.getBirthM()));
        demoForm.setBirthDay(String.valueOf(userData.getBirthD()));
        demoForm.setSex(userData.getSex());
        demoForm.setMemo(userData.getMemo());
        demoForm.setSex_value(userData.getSex_value());
        return demoForm;
    }

    /**
     * UserDataオブジェクトに引数のフォームの各値を設定する
     * @param demoForm DemoFormオブジェクト
     * @return ユーザーデータ
     */
    private UserData getUserData(DemoForm demoForm){
        UserData userData = new UserData();
        if(!DateCheckUtil.isEmpty(demoForm.getId())){
            userData.setId(Long.valueOf(demoForm.getId()));
        }
        userData.setName(demoForm.getName());
        userData.setBirthY(Integer.valueOf(demoForm.getBirthYear()));
        userData.setBirthM(Integer.valueOf(demoForm.getBirthMonth()));
        userData.setBirthD(Integer.valueOf(demoForm.getBirthDay()));
        userData.setSex(demoForm.getSex());
        userData.setMemo(demoForm.getMemo());
        userData.setSex_value(demoForm.getSex_value());
        return userData;
    }

    /**
     * 引数の文字列をLong型に変換する
     * @param id ID
     * @return Long型のID
     */
    private Long stringToLong(String id){
        try{
            return Long.parseLong(id);
        }catch(NumberFormatException ex){
            return null;
        }
    }

}



さらに、SQLのxmlファイルを変更した。「memo」を追加・更新するcreate,updateメソッドでは、jdbcType指定を追加している。また、select句にjdbcType指定を追加するため、「userDataResultMap」というresultMapを定義し、それをfindBySearchForm,findByIdメソッドで利用するようにしている。

<?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.UserDataMapper">
    <resultMap id="userDataResultMap" type="com.example.demo.UserData" >
        <id column="id" property="id" jdbcType="BIGINT" />
        <result column="name" property="name" jdbcType="VARCHAR" />
        <result column="birthY" property="birthY" jdbcType="VARCHAR" />
        <result column="birthM" property="birthM" jdbcType="VARCHAR" />
        <result column="birthD" property="birthD" jdbcType="VARCHAR" />
        <result column="sex" property="sex" jdbcType="VARCHAR" />
        <result column="memo" property="memo" jdbcType="VARCHAR" />
        <result column="sex_value" property="sex_value" jdbcType="VARCHAR" />
    </resultMap>
    <select id="findBySearchForm" parameterType="com.example.demo.SearchForm" 
            resultMap="userDataResultMap">
        SELECT u.id, u.name, u.birth_year as birthY, u.birth_month as birthM
               , u.birth_day as birthD, u.sex, u.memo, m.sex_value
        FROM USER_DATA u, M_SEX m
        WHERE u.sex = m.sex_cd
        <if test="searchName != null and searchName != ''">
            AND u.name like '%' || #{searchName} || '%'
        </if>
        <if test="fromBirthYear != null and fromBirthYear != ''">
            AND #{fromBirthYear} || lpad(#{fromBirthMonth}, 2, '0') 
                   || lpad(#{fromBirthDay}, 2, '0')
               &lt;= u.birth_year || lpad(u.birth_month, 2, '0') 
                   || lpad(u.birth_day, 2, '0')
        </if>
        <if test="toBirthYear != null and toBirthYear != ''">
            AND u.birth_year || lpad(u.birth_month, 2, '0') 
                   || lpad(u.birth_day, 2, '0')
               &lt;= #{toBirthYear} || lpad(#{toBirthMonth}, 2, '0') 
                   || lpad(#{toBirthDay}, 2, '0')
        </if>
        <if test="searchSex != null and searchSex != ''">
            AND u.sex = #{searchSex}
        </if>
        ORDER BY u.id
    </select>
    <select id="findById" resultMap="userDataResultMap">
        SELECT id, name, birth_year as birthY, birth_month as birthM
             , birth_day as birthD, sex, memo
        FROM USER_DATA
        WHERE id = #{id}
    </select>
    <delete id="deleteById" parameterType="java.lang.Long">
        DELETE FROM USER_DATA WHERE id = #{id}
    </delete>
    <insert id="create" parameterType="com.example.demo.UserData">
        INSERT INTO USER_DATA ( id, name, birth_year, birth_month
              , birth_day, sex, memo )
        VALUES (#{id}, #{name}, #{birthY}, #{birthM}
              , #{birthD}, #{sex}, #{memo,jdbcType=VARCHAR})
    </insert>
    <update id="update" parameterType="com.example.demo.UserData">
        UPDATE USER_DATA SET name = #{name}, birth_year = #{birthY}
            , birth_month = #{birthM}, birth_day = #{birthD}
            , sex = #{sex}, memo = #{memo,jdbcType=VARCHAR}
        WHERE id = #{id}
    </update>
    <select id="findMaxId" resultType="long">
        SELECT NVL(max(id), 0) FROM USER_DATA
    </select>
</mapper>



また、変更したHTMLファイルは以下の通り。「input.html」ではメモ欄のテキストエリアを追加し、「confirm.html」「confirm_delete.html」ではメモ欄の表示を追加している。さらに、表示形式を整えるため、tableタグを利用するように修正している。

<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <link th:href="@{/demo.css}" rel="stylesheet" type="text/css" />
    <title>入力画面</title>
</head>
<body>
  <p>下記必要事項を記載の上、「確認」ボタンを押下してください。</p><br/>
   <form method="post" th:action="@{/confirm}" th:object="${demoForm}">
       <table border="0">
           <tr>
               <td align="left" valign="top">名前:</td>
               <td>
                   <input type="text" th:value="*{name}"
                          th:field="*{name}" th:errorclass="fieldError" />
                   <span th:if="*{#fields.hasErrors('name')}"
                         th:errors="*{name}" class="errorMessage"></span>
               </td>
           </tr>
           <tr>
               <td align="left" valign="top">生年月日:</td>
               <td>
                   <input type="text" th:value="*{birthYear}" size="4"
                          maxlength="4" th:field="*{birthYear}" th:errorclass="fieldError" />年
                   <select th:field="*{birthMonth}" th:errorclass="fieldError">
                       <option value="">---</option>
                       <option th:each="item : *{getMonthItems()}"
                               th:value="${item.key}" th:text="${item.value}"/>
                   </select>月
                   <select th:field="*{birthDay}" th:errorclass="fieldError">
                       <option value="">---</option>
                       <option th:each="item : *{getDayItems()}"
                               th:value="${item.key}" th:text="${item.value}"/>
                   </select>日
                   <span th:if="*{#fields.hasErrors('birthYear')}"
                         th:errors="*{birthYear}" class="errorMessage"></span>
                   <span th:if="*{#fields.hasErrors('birthMonth')}"
                         th:errors="*{birthMonth}" class="errorMessage"></span>
                   <span th:if="*{#fields.hasErrors('birthDay')}"
                         th:errors="*{birthDay}" class="errorMessage"></span>
               </td>
           </tr>
           <tr>
               <td align="left" valign="top">性別:</td>
               <td>
                   <for th:each="item : *{getSexItems()}">
                       <input type="radio" name="sex" th:value="${item.key}" th:text="${item.value}"
                              th:field="*{sex}" th:errorclass="fieldError" />
                   </for>
                   <span th:if="*{#fields.hasErrors('sex')}"
                         th:errors="*{sex}" class="errorMessage"></span>
               </td>
           </tr>
           <tr>
               <td align="left" valign="top">メモ:</td>
               <td>
                   <textarea rows="6" cols="40" th:value="*{memo}" th:field="*{memo}"></textarea>
               </td>
           </tr>
           <tr>
               <td align="left" valign="top">入力確認:</td>
               <td>
                   <input type="checkbox" name="checked" th:value="確認済"
                          th:field="*{checked}" th:errorclass="fieldError" />
                   <span th:if="*{#fields.hasErrors('checked')}"
                         th:errors="*{checked}" class="errorMessage"></span>
               </td>
           </tr>
       </table>
       <br/><br/>
       <input type="submit" name="next" value="確認" />
       <input type="submit" name="back" value="戻る" />
   </form>
</body>
</html>
<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>確認画面</title>
</head>
<body>
   <p>入力内容を確認し、問題なければ「送信」ボタンを押下してください。</p>
    <form method="post" th:action="@{/send}" th:object="${demoForm}">
        <table border="0">
            <tr>
                <td align="left" valign="top">名前: </td>
                <td>
                    <span th:text="*{name}">
                        ここに名前が表示されます
                    </span>
                </td>
            </tr>
            <tr>
                <td align="left" valign="top">生年月日: </td>
                <td>
                    <span th:text="*{birthYear} + '年'
                     + *{getMonthItems().get('__*{birthMonth}__')} + '月'
                     + *{getDayItems().get('__*{birthDay}__')} + '日'">
                        ここに生年月日が表示されます
                    </span>
                </td>
            </tr>
            <tr>
                <td align="left" valign="top">性別: </td>
                <td>
                    <span th:text="*{getSexItems().get('__*{sex}__')}">
                        ここに性別が表示されます
                    </span>
                </td>
            </tr>
            <tr>
                <td align="left" valign="top">メモ: </td>
                <td>
                    <th:block th:if="*{memo}">
                        <th:block th:each="memoStr, memoStat : *{memo.split('\r\n|\r|\n', -1)}">
                            <th:block th:text="${memoStr}"/>
                            <br th:if="${!memoStat.last}"/>
                        </th:block>
                    </th:block>
                </td>
            </tr>
            <tr>
                <td align="left" valign="top">確認チェック: </td>
                <td>
                    <span th:text="*{checked}">
                        ここに確認チェック内容が表示されます
                    </span>
                </td>
            </tr>
        </table>
        <br/><br/>
        <input type="submit" name="next" value="送信" />
        <input type="submit" name="back" value="戻る" />
    </form>
</body>
</html>
<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>削除確認画面</title>
</head>
<body>
   <p>下記内容を削除してよろしいでしょうか?問題なければ「送信」ボタンを押下してください。</p>
    <form method="post" th:action="@{/delete}" th:object="${demoForm}">
        <table border="0">
            <tr>
                <td align="left" valign="top">名前: </td>
                <td>
                    <span th:text="*{name}">
                        ここに名前が表示されます
                    </span>
                </td>
            </tr>
            <tr>
                <td align="left" valign="top">生年月日: </td>
                <td>
                    <span th:text="*{birthYear} + '年'
                     + *{getMonthItems().get('__*{birthMonth}__')} + '月'
                     + *{getDayItems().get('__*{birthDay}__')} + '日'">
                        ここに生年月日が表示されます
                    </span>
                </td>
            </tr>
            <tr>
                <td align="left" valign="top">性別: </td>
                <td>
                    <span th:text="*{getSexItems().get('__*{sex}__')}">
                        ここに性別が表示されます
                    </span>
                </td>
            </tr>
            <tr>
                <td align="left" valign="top">メモ: </td>
                <td>
                    <th:block th:if="*{memo}">
                        <th:block th:each="memoStr, memoStat : *{memo.split('\r\n|\r|\n', -1)}">
                            <th:block th:text="${memoStr}"/>
                            <br th:if="${!memoStat.last}"/>
                        </th:block>
                    </th:block>
                </td>
            </tr>
        </table>
        <br/><br/>
        <input type="submit" name="next" value="送信" />
        <input type="submit" name="back" value="戻る" />
    </form>
</body>
</html>

その他のソースコード内容は、以下のサイトを参照のこと。
https://github.com/purin-it/java/tree/master/spring-boot-null-update-2/demo

要点まとめ

  • NULL更新するDB項目がある場合は、jdbcTypeの指定が必要になる。
  • select句にjdbcType指定を追加するには、resultMapを定義し、そこでjdbcTypeを指定する。