これまでこのブログで、Spring BootでMyBatisを利用しDB更新するアプリケーションをいくつか取り上げてきたが、DB更新時に、楽観ロックを利用することもできる。
今回は、Spring BootでMyBatisを利用しSQL Serverに接続するアプリケーションで楽観ロックを利用してみたので、そのサンプルプログラムを共有する。
なお、楽観ロックを含む排他制御については、以下のサイトを参照のこと。
https://qiita.com/NagaokaKenichi/items/73040df85b7bd4e9ecfc
前提条件
下記記事の実装が完了していること。
また、SQL ServerのUSER_DATAテーブルに、以下の記事の「書き込み先テーブルのカラム追加」に記載のカラム追加が完了していること。
サンプルプログラムの作成
作成したサンプルプログラムの構成は、以下の通り。
なお、上記の赤枠は、前提条件のプログラムから追加・変更したプログラムである。
エンティティクラス・フォームクラスの内容は以下の通りで、カラム「VERSION」を追加している。
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; }
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をもつデータを行ロックをかけ取得する処理を追加している。
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をもつデータを行ロックをかけ取得する処理を追加したりしている。
<?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型)を返却するようにしている。
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の値をチェックし、チェックエラーの場合に楽観ロックエラーに設定している。
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更新エラーの場合に、自画面にエラーメッセージを表示するようにしている。
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更新エラーのメッセージを追加している。
#メッセージ 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更新エラーの場合に、エラーメッセージを表示する処理を追加している。
<!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>
<!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テーブルの中身は、以下の通り。
SELECT * FROM dbo.USER_DATA ORDER BY id ASC
2) Spring Bootアプリケーションを起動し、「http:// (ホスト名):(ポート番号)/」にアクセスすると、以下の画面が表示される。ここで「データ追加」ボタンを押下する。
5) 以下のように、完了画面が表示される。ここで「一覧に戻る」ボタンを押下する。
6) この時点でのUSER_DATAテーブルの中身は以下の通りで、id=3のデータがVERSION=0で追加されていることが確認できる。
SELECT * FROM dbo.USER_DATA ORDER BY id ASC
7) 一覧画面の表示は、以下の通り。ここで、ID=3の「更新」リンクを押下する。
10) 以下のように、完了画面が表示される。ここで「一覧に戻る」ボタンを押下する。
11) この時点でのUSER_DATAテーブルの中身は以下の通りで、id=3のデータがVERSION=1で更新されていることが確認できる。
SELECT * FROM dbo.USER_DATA ORDER BY id ASC
12) 一覧画面の表示は、以下の通り。ここで、ID=3の「削除」リンクを押下する。
14) 一覧画面に戻り、以下のようにID=3のデータが削除されていることが確認できる。
15) この時点でのUSER_DATAテーブルの中身は以下の通りで、id=3のデータ削除されていることが確認できる。
SELECT * FROM dbo.USER_DATA ORDER BY id ASC
サンプルプログラムの実行結果(エラー時)
サンプルプログラムの実行結果(エラー時)は、以下の通り。なお、下図では更新時のエラーを表示しているが、削除時も同様にエラーメッセージが表示される。
1) 実行前のUSER_DATAテーブルの中身は、以下の通り。
SELECT * FROM dbo.USER_DATA ORDER BY id ASC
2) Spring Bootアプリケーションを起動し、「http:// (ホスト名):(ポート番号)/」にアクセスすると、以下の画面が表示される。ここでID=2のデータの「更新」リンクを押下する。
4) 以下のSQLを実行し、入力画面を開いた時と違うVERSIONになるようにする。
UPDATE dbo.USER_DATA SET VERSION = 1 WHERE ID = 2
5) 4)のSQL実行後、ID=2のUSER_DATAテーブルのデータは以下のようになる。
SELECT * FROM dbo.USER_DATA WHERE ID = 2
7) 以下のように、楽観ロックエラーになることが確認できる。
8) 再度「http:// (ホスト名):(ポート番号)/」にアクセスすると、以下の画面が表示される。ここでID=2のデータの「更新」リンクを押下する。
10) 更新対象データに行ロックをかけるため、トランザクションを開始する。
11) 以下のSQLを実行し、更新対象データに行ロックをかける。
SELECT * FROM dbo.USER_DATA WITH(ROWLOCK, UPDLOCK, NOWAIT) WHERE ID = 2
13) 以下のように、DBロック取得エラーになることが確認できる。
14) 更新対象データの行ロックを解除するため、トランザクションを終了する。
サンプルプログラムの実行結果(更新件数エラー時)
サンプルプログラムの実行結果(更新件数エラー時)は、以下の通り。なお、下図では更新時のエラーを表示しているが、削除時も同様にエラーメッセージが表示される。
1) 以下のように、バージョンが異なっている場合にエラーにする処理をコメントアウトする。
2) 実行前のUSER_DATAテーブルの中身は、以下の通り。
SELECT * FROM dbo.USER_DATA ORDER BY id ASC
3) Spring Bootアプリケーションを起動し、「http:// (ホスト名):(ポート番号)/」にアクセスすると、以下の画面が表示される。ここでID=2のデータの「更新」リンクを押下する。
5) 以下のSQLを実行し、入力画面を開いた時と違うVERSIONになるようにする。
UPDATE dbo.USER_DATA SET VERSION = 1 WHERE ID = 2
6) 5)のSQL実行後、ID=2のUSER_DATAテーブルのデータは以下のようになる。
SELECT * FROM dbo.USER_DATA WHERE ID = 2
8) 以下のように、楽観ロックエラーになることが確認できる。
要点まとめ
- 画面上で楽観ロックを実装するには、登録・更新の直前でバージョンをチェックすると共に、登録・更新時の同一バージョンの更新件数が1件であることをチェックすればよい。