今回も引き続き、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を指定する。





