これまでこのブログで、Spring BootでMyBatisを利用しDB更新するアプリケーションをいくつか取り上げてきたが、DB更新時に、楽観ロックを利用することもできる。
今回は、Spring BootでMyBatisを利用しSQL Serverに接続するアプリケーションで楽観ロックを利用してみたので、そのサンプルプログラムを共有する。
なお、楽観ロックを含む排他制御については、以下のサイトを参照のこと。
https://qiita.com/NagaokaKenichi/items/73040df85b7bd4e9ecfc
前提条件
下記記事の実装が完了していること。
また、SQL ServerのUSER_DATAテーブルに、以下の記事の「書き込み先テーブルのカラム追加」に記載のカラム追加が完了していること。
サンプルプログラムの作成
作成したサンプルプログラムの構成は、以下の通り。
なお、上記の赤枠は、前提条件のプログラムから追加・変更したプログラムである。
エンティティクラス・フォームクラスの内容は以下の通りで、カラム「VERSION」を追加している。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | 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 sex_value; /** バージョン */ private int version; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | 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; /** 確認チェック */ @NotEmpty private String checked; /** 性別(文字列) */ private String sex_value; /** バージョン */ private int version; /** 生年月日_月の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; } } |
また、Mapperインタフェースの内容は以下の通りで、登録・更新・削除処理の戻り値(更新件数・int型)を返却し、指定したIDをもつデータを行ロックをかけ取得する処理を追加している。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | package com.example.demo; import org.apache.ibatis.annotations.Mapper; import java.util.Collection; @Mapper public interface UserDataMapper { /** * ユーザーデータテーブル(user_data)を全件取得する * @return ユーザーデータテーブル(user_data)を全データ */ Collection<UserData> findAll(); /** * 指定したIDをもつユーザーデータテーブル(user_data)のデータを取得する * @param id ID * @return ユーザーデータテーブル(user_data)の指定したIDのデータ */ UserData findById(Long id); /** * 指定したIDをもつユーザーデータテーブル(user_data)のデータを削除する * @param userData ユーザーデータテーブル(user_data)の追加データ * @return 更新件数 */ int deleteById(UserData userData); /** * 指定したユーザーデータテーブル(user_data)のデータを追加する * @param userData ユーザーデータテーブル(user_data)の追加データ * @return 更新件数 */ int create(UserData userData); /** * 指定したユーザーデータテーブル(user_data)のデータを更新する * @param userData ユーザーデータテーブル(user_data)の更新データ * @return 更新件数 */ int update(UserData userData); /** * ユーザーデータテーブル(user_data)の最大値IDを取得する * @return ユーザーデータテーブル(user_data)の最大値ID */ long findMaxId(); /** * 指定したIDをもつユーザーデータテーブル(user_data)のデータを行ロックをかけ取得する * @param id ID * @return ユーザーデータテーブル(user_data)の指定したIDのデータ */ UserData findByIdRowLock(Long id); } |
さらに、Mapper XMLの内容は以下の通りで、VERSIONの更新処理や更新条件を追加したり、指定したIDをもつデータを行ロックをかけ取得する処理を追加したりしている。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | <?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"> <select id="findAll" resultType="com.example.demo.UserData"> SELECT u.id, u.name, u.birth_year as birthY, u.birth_month as birthM , u.birth_day as birthD, u.sex as sex, m.sex_value as sex_value , u.version FROM USER_DATA u, M_SEX m WHERE u.sex = m.sex_cd ORDER BY u.id </select> <select id="findById" resultType="com.example.demo.UserData"> SELECT id, name, birth_year as birthY , birth_month as birthM , birth_day as birthD, sex, version FROM USER_DATA WHERE id = #{id} </select> <delete id="deleteById" parameterType="com.example.demo.UserData"> <!-- 削除時の条件にversionを追加し、versionが違う場合は更新しない --> DELETE FROM USER_DATA WHERE id = #{id} AND version = #{version} </delete> <insert id="create" parameterType="com.example.demo.UserData"> INSERT INTO USER_DATA ( id, name, birth_year, birth_month , birth_day, sex, version ) VALUES (#{id}, #{name}, #{birthY}, #{birthM} , #{birthD}, #{sex}, #{version}) </insert> <update id="update" parameterType="com.example.demo.UserData"> <!-- 更新時に、versionを1インクリメントする --> <!-- また、更新時の条件にversionを追加し、versionが違う場合は更新しない --> UPDATE USER_DATA SET name = #{name}, birth_year = #{birthY} , birth_month = #{birthM}, birth_day = #{birthD}, sex = #{sex} , version = #{version} + 1 WHERE id = #{id} AND version = #{version} </update> <select id="findMaxId" resultType="long"> SELECT IsNull(MAX(id), 0) FROM USER_DATA </select> <select id="findByIdRowLock" resultType="com.example.demo.UserData"> SELECT id, name, birth_year as birthY , birth_month as birthM , birth_day as birthD, sex, version FROM USER_DATA WITH(ROWLOCK, UPDLOCK, NOWAIT) <!-- SQL Serverで行ロックをかける --> WHERE id = #{id} </select> </mapper> |
また、サービスインタフェースの内容は以下の通りで、登録・更新処理や削除処理で戻り値(更新件数・int型)を返却するようにしている。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | package com.example.demo; import org.springframework.validation.BindingResult; import java.util.List; public interface DemoService { /** * ユーザーデータリストを取得 * @return ユーザーデータリスト */ List<DemoForm> demoFormList(); /** * 引数のIDに対応するユーザーデータを取得 * @param id ID * @return ユーザーデータ */ DemoForm findById(String id); /** * 引数のIDに対応するユーザーデータを削除 * @param demoForm フォームオブジェクト * @return 更新件数 */ int deleteById(DemoForm demoForm); /** * 引数のユーザーデータがあれば更新し、無ければ追加 * @param demoForm フォームオブジェクト * @return 更新件数 */ int createOrUpdate(DemoForm demoForm); /** * フォームオブジェクトのチェック処理を行い、画面遷移先を返す * @param demoForm フォームオブジェクト * @param result バインド結果 * @param normalPath 正常時の画面遷移先 * @return 画面遷移先 */ String checkForm(DemoForm demoForm, BindingResult result, String normalPath); } |
さらに、サービスクラスの内容は以下の通りで、更新・削除処理の直前にVERSIONの値をチェックし、チェックエラーの場合に楽観ロックエラーに設定している。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 | 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() { List<DemoForm> demoFormList = new ArrayList<>(); //ユーザーデータテーブル(user_data)から全データを取得する Collection<UserData> userDataList = mapper.findAll(); 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 int deleteById(DemoForm demoForm){ //削除処理を行うエンティティを生成する UserData userData = getUserData(demoForm); //削除対象のデータを行ロックをかけ取得する Long longId = stringToLong(demoForm.getId()); UserData updData = mapper.findByIdRowLock(longId); //バージョンが異なっている場合は、楽観ロックエラーとし、削除しない if(updData.getVersion() != demoForm.getVersion()){ throw new RuntimeException("OptimisticLockingFailureException"); } //削除処理を行い、更新件数を返却 return mapper.deleteById(userData); } /** * {@inheritDoc} */ @Override @Transactional(readOnly = false) public int createOrUpdate(DemoForm demoForm){ //更新・追加処理を行うエンティティを生成する UserData userData = getUserData(demoForm); //追加・更新処理を行い、更新件数を返却 if(demoForm.getId() == null){ userData.setId(mapper.findMaxId() + 1); return mapper.create(userData); }else{ //更新対象のデータを行ロックをかけ取得する UserData updData = mapper.findByIdRowLock(userData.getId()); //バージョンが異なっている場合は、楽観ロックエラーとし、更新しない if (updData.getVersion() != userData.getVersion()) { throw new RuntimeException("OptimisticLockingFailureException"); } return 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; } } /** * 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.setSex_value(userData.getSex_value()); demoForm.setVersion(userData.getVersion()); 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.setSex_value(demoForm.getSex_value()); userData.setVersion(demoForm.getVersion()); return userData; } /** * 引数の文字列をLong型に変換する * @param id ID * @return Long型のID */ private Long stringToLong(String id){ try{ return Long.parseLong(id); }catch(NumberFormatException ex){ return null; } } } |
また、コントローラクラスの内容は以下の通りで、登録・更新処理で更新件数をチェックし、楽観ロックエラー・行ロックエラー・DB更新エラーの場合に、自画面にエラーメッセージを表示するようにしている。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 | package com.example.demo; import com.microsoft.sqlserver.jdbc.SQLServerException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.SessionAttributes; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.validation.BindingResult; import org.springframework.web.bind.support.SessionStatus; import java.util.ArrayList; import java.util.List; @Controller @SessionAttributes(types = {DemoForm.class}) public class DemoController { /** Demoサービスクラスへのアクセス */ @Autowired private DemoService demoService; /** DB取得時ロックエラーメッセージ */ private static final String DB_LOCK_ERROR = "ロック要求がタイムアウトしました。"; /** * ユーザーデータテーブル(user_data)のデータを取得して返却する * @return ユーザーデータリスト */ @ModelAttribute("demoFormList") public List<DemoForm> userDataList(){ List<DemoForm> demoFormList = new ArrayList<>(); return demoFormList; } /** * Formオブジェクトを初期化して返却する * @return Formオブジェクト */ @ModelAttribute("demoForm") public DemoForm createDemoForm(){ DemoForm demoForm = new DemoForm(); return demoForm; } /** * 初期表示(一覧)画面に遷移する * @param model Modelオブジェクト * @return 一覧画面へのパス */ @RequestMapping("/") public String index(Model model){ //ユーザーデータリストを取得 List<DemoForm> demoFormList = demoService.demoFormList(); //ユーザーデータリストを更新 model.addAttribute("demoFormList", demoFormList); return "list"; } /** * 更新処理を行う画面に遷移する * @param id 更新対象のID * @param model Modelオブジェクト * @return 入力・更新画面へのパス */ @GetMapping("/update") public String update(@RequestParam("id") String id, Model model){ //更新対象のユーザーデータを取得 DemoForm demoForm = demoService.findById(id); //ユーザーデータを更新 model.addAttribute("demoForm", demoForm); return "input"; } /** * 削除確認画面に遷移する * @param id 更新対象のID * @param model Modelオブジェクト * @return 削除確認画面へのパス */ @GetMapping("/delete_confirm") public String delete_confirm(@RequestParam("id") String id, Model model){ //削除対象のユーザーデータを取得 DemoForm demoForm = demoService.findById(id); //ユーザーデータを更新 model.addAttribute("demoForm", demoForm); return "confirm_delete"; } /** * 削除処理を行う * @param demoForm Formオブジェクト * @param result バインド結果 * @return 一覧画面の表示処理 */ @PostMapping(value = "/delete", params = "next") public String delete(DemoForm demoForm, BindingResult result){ //指定したユーザーデータを削除 try{ int updCnt = demoService.deleteById(demoForm); if(updCnt != 1){ //更新件数が1件でない場合、楽観ロックエラーとし、削除確認画面に遷移 result.reject("exception.optimistic-locking-failure"); return "confirm_delete"; } }catch(RuntimeException ex){ //OptimisticLockingFailureException(楽観ロックエラーの場合) if("OptimisticLockingFailureException".equals(ex.getMessage())) { result.reject("exception.optimistic-locking-failure"); //DB更新時ロック取得時エラーが発生した場合 } else if(ex.getCause() instanceof SQLServerException && DB_LOCK_ERROR.equals(ex.getCause().getMessage())){ result.reject("exception.row-lock-failure"); //上記エラー以外の場合 } else { result.reject("exception.db-update-failure"); } //エラー発生時は、削除確認画面に遷移 return "confirm_delete"; } //一覧画面に遷移 return "redirect:/to_index"; } /** * 削除完了後に一覧画面に戻る * @param model Modelオブジェクト * @return 一覧画面 */ @GetMapping("/to_index") public String toIndex(Model model){ return index(model); } /** * 削除確認画面から一覧画面に戻る * @param model Modelオブジェクト * @return 一覧画面 */ @PostMapping(value = "/delete", params = "back") public String confirmDeleteBack(Model model){ return index(model); } /** * 追加処理を行う画面に遷移する * @param model Modelオブジェクト * @return 入力・更新画面へのパス */ @PostMapping("/add") public String add(Model model){ model.addAttribute("demoForm", new DemoForm()); return "input"; } /** * エラーチェックを行い、エラーが無ければ確認画面に遷移し、 * エラーがあれば入力画面のままとする * @param demoForm Formオブジェクト * @param result バインド結果 * @return 確認画面または入力画面へのパス */ @PostMapping(value = "/confirm", params = "next") public String confirm(@Validated DemoForm demoForm, BindingResult result){ //チェック処理を行い、画面遷移する return demoService.checkForm(demoForm, result, "confirm"); } /** * 一覧画面に戻る * @param model Modelオブジェクト * @return 一覧画面の表示処理 */ @PostMapping(value = "/confirm", params = "back") public String confirmBack(Model model){ return index(model); } /** * 完了画面に遷移する * @param demoForm Formオブジェクト * @param result バインド結果 * @return 完了画面または確認画面 */ @PostMapping(value = "/send", params = "next") public String send(@Validated DemoForm demoForm, BindingResult result){ //チェック処理を行い、エラーがなければ、更新・追加処理を行う String normalPath = "redirect:/complete"; String checkPath = demoService.checkForm( demoForm, result, "redirect:/complete"); if(normalPath.equals(checkPath)){ try{ int updCnt = demoService.createOrUpdate(demoForm); if(updCnt != 1){ //更新件数が1件でない場合、楽観ロックエラーとし、確認画面に遷移 result.reject("exception.optimistic-locking-failure"); return "confirm"; } }catch(RuntimeException ex){ //OptimisticLockingFailureException(楽観ロックエラーの場合) if("OptimisticLockingFailureException".equals(ex.getMessage())) { result.reject("exception.optimistic-locking-failure"); //DB更新時ロック取得時エラーが発生した場合 } else if(ex.getCause() instanceof SQLServerException && DB_LOCK_ERROR.equals(ex.getCause().getMessage())){ result.reject("exception.row-lock-failure"); //上記エラー以外の場合 } else { result.reject("exception.db-update-failure"); } //エラー発生時は、確認画面に遷移 return "confirm"; } } return checkPath; } /** * 完了画面に遷移する * @param sessionStatus セッションステータス * @return 完了画面 */ @GetMapping("/complete") public String complete(SessionStatus sessionStatus){ //セッションオブジェクトを破棄 sessionStatus.setComplete(); return "complete"; } /** * 入力画面に戻る * @return 入力画面 */ @PostMapping(value = "/send", params = "back") public String sendBack(){ return "input"; } } |
さらに、メッセージプロパティの内容は以下の通りで、楽観ロックエラー・DB更新エラーのメッセージを追加している。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | #メッセージ validation.date-empty={0}を入力してください。 validation.date-invalidate=生年月日が存在しない日付になっています。 validation.date-future=生年月日が未来の日付になっています。 validation.empty-msg= javax.validation.constraints.NotEmpty.message={0}を入力してください。 validation.sex-invalidate=性別に不正な値が入っています。 exception.optimistic-locking-failure=楽観ロックエラーが発生しました。 exception.row-lock-failure=DBロック取得エラーが発生しました。 exception.db-update-failure=DB更新に失敗しました。 #フィールド名 name=名前 sex=性別 checked=確認チェック |
また、確認画面・削除確認画面の内容は以下の通りで、楽観ロックエラー・DB更新エラーの場合に、エラーメッセージを表示する処理を追加している。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | <!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> <form method="post" th:action="@{/send}" th:object="${demoForm}"> <!-- エラーメッセージの表示 formタグ内に記載する --> <div class="errorMessage" th:if="${#fields.hasGlobalErrors()}"> <p th:each="err : ${#fields.globalErrors()}" th:text="${err}"> ここにエラーメッセージが表示されます </p> </div> <p th:text="'名前: ' + *{name}">ここに名前が表示されます</p> <p th:text="'生年月日: ' + *{birthYear} + '年' + *{getMonthItems().get('__*{birthMonth}__')} + '月' + *{getDayItems().get('__*{birthDay}__')} + '日'"> ここに生年月日が表示されます </p> <p th:text="'性別: ' + *{getSexItems().get('__*{sex}__')}"> ここに性別が表示されます </p> <p th:text="'確認チェック: ' + *{checked}"> ここに確認チェック内容が表示されます </p> <input type="submit" name="next" value="送信" /> <input type="submit" name="back" value="戻る" /> </form> </body> </html> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | <!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> <form method="post" th:action="@{/delete}" th:object="${demoForm}"> <!-- エラーメッセージの表示 formタグ内に記載する --> <div class="errorMessage" th:if="${#fields.hasGlobalErrors()}"> <p th:each="err : ${#fields.globalErrors()}" th:text="${err}"> ここにエラーメッセージが表示されます </p> </div> <p th:text="'名前: ' + *{name}">ここに名前が表示されます</p> <p th:text="'生年月日: ' + *{birthYear} + '年' + *{getMonthItems().get('__*{birthMonth}__')} + '月' + *{getDayItems().get('__*{birthDay}__')} + '日'"> ここに生年月日が表示されます </p> <p th:text="'性別: ' + *{getSexItems().get('__*{sex}__')}"> ここに性別が表示されます </p> <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-sqlserver-opt-lock/demo
サンプルプログラムの実行結果(正常時)
サンプルプログラムの実行結果(正常時)は、以下の通り。
1) 実行前のUSER_DATAテーブルの中身は、以下の通り。
1 | SELECT * FROM dbo.USER_DATA ORDER BY id ASC |
2) Spring Bootアプリケーションを起動し、「http:// (ホスト名):(ポート番号)/」にアクセスすると、以下の画面が表示される。ここで「データ追加」ボタンを押下する。
5) 以下のように、完了画面が表示される。ここで「一覧に戻る」ボタンを押下する。
6) この時点でのUSER_DATAテーブルの中身は以下の通りで、id=3のデータがVERSION=0で追加されていることが確認できる。
1 | SELECT * FROM dbo.USER_DATA ORDER BY id ASC |
7) 一覧画面の表示は、以下の通り。ここで、ID=3の「更新」リンクを押下する。
10) 以下のように、完了画面が表示される。ここで「一覧に戻る」ボタンを押下する。
11) この時点でのUSER_DATAテーブルの中身は以下の通りで、id=3のデータがVERSION=1で更新されていることが確認できる。
1 | SELECT * FROM dbo.USER_DATA ORDER BY id ASC |
12) 一覧画面の表示は、以下の通り。ここで、ID=3の「削除」リンクを押下する。
14) 一覧画面に戻り、以下のようにID=3のデータが削除されていることが確認できる。
15) この時点でのUSER_DATAテーブルの中身は以下の通りで、id=3のデータ削除されていることが確認できる。
1 | SELECT * FROM dbo.USER_DATA ORDER BY id ASC |
サンプルプログラムの実行結果(エラー時)
サンプルプログラムの実行結果(エラー時)は、以下の通り。なお、下図では更新時のエラーを表示しているが、削除時も同様にエラーメッセージが表示される。
1) 実行前のUSER_DATAテーブルの中身は、以下の通り。
1 | SELECT * FROM dbo.USER_DATA ORDER BY id ASC |
2) Spring Bootアプリケーションを起動し、「http:// (ホスト名):(ポート番号)/」にアクセスすると、以下の画面が表示される。ここでID=2のデータの「更新」リンクを押下する。
4) 以下のSQLを実行し、入力画面を開いた時と違うVERSIONになるようにする。
1 | UPDATE dbo.USER_DATA SET VERSION = 1 WHERE ID = 2 |
5) 4)のSQL実行後、ID=2のUSER_DATAテーブルのデータは以下のようになる。
1 | SELECT * FROM dbo.USER_DATA WHERE ID = 2 |
7) 以下のように、楽観ロックエラーになることが確認できる。
8) 再度「http:// (ホスト名):(ポート番号)/」にアクセスすると、以下の画面が表示される。ここでID=2のデータの「更新」リンクを押下する。
10) 更新対象データに行ロックをかけるため、トランザクションを開始する。
11) 以下のSQLを実行し、更新対象データに行ロックをかける。
1 | SELECT * FROM dbo.USER_DATA WITH(ROWLOCK, UPDLOCK, NOWAIT) WHERE ID = 2 |
13) 以下のように、DBロック取得エラーになることが確認できる。
14) 更新対象データの行ロックを解除するため、トランザクションを終了する。
サンプルプログラムの実行結果(更新件数エラー時)
サンプルプログラムの実行結果(更新件数エラー時)は、以下の通り。なお、下図では更新時のエラーを表示しているが、削除時も同様にエラーメッセージが表示される。
1) 以下のように、バージョンが異なっている場合にエラーにする処理をコメントアウトする。
2) 実行前のUSER_DATAテーブルの中身は、以下の通り。
1 | SELECT * FROM dbo.USER_DATA ORDER BY id ASC |
3) Spring Bootアプリケーションを起動し、「http:// (ホスト名):(ポート番号)/」にアクセスすると、以下の画面が表示される。ここでID=2のデータの「更新」リンクを押下する。
5) 以下のSQLを実行し、入力画面を開いた時と違うVERSIONになるようにする。
1 | UPDATE dbo.USER_DATA SET VERSION = 1 WHERE ID = 2 |
6) 5)のSQL実行後、ID=2のUSER_DATAテーブルのデータは以下のようになる。
1 | SELECT * FROM dbo.USER_DATA WHERE ID = 2 |
8) 以下のように、楽観ロックエラーになることが確認できる。
要点まとめ
- 画面上で楽観ロックを実装するには、登録・更新の直前でバージョンをチェックすると共に、登録・更新時の同一バージョンの更新件数が1件であることをチェックすればよい。