今回も引き続き、Spring BootのWEB画面上で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') <= 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') <= #{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を指定する。