今回も引き続き、排他制御の1つである「悲観的排他制御」を利用した実装について述べる。ここでは、具体的なサンプルプログラムのソースコードを共有する。
前提条件
下記記事を参照のこと。
Spring BootのWEB画面上で排他制御を実装してみた(完成イメージと前提条件)今回は、データ更新処理内に排他制御を加えてみたので、そのサンプルプログラムを共有する。 排他制御は、1つのデータに複数のアクセスが...
作成したサンプルプログラムの内容
作成したサンプルプログラムの構成は以下の通り。
なお、上図の赤枠は、前提条件に記載したソースコードと比較し、変更になったソースコードを示す。赤枠のソースコードについては今後記載する。
まず、ユーザーデータ(USER_DATA)テーブルにアクセスするMapperクラスは以下の通り。今回は、悲観ロックをかけてデータ取得するfindByIdForUpdateメソッドを追加していて、ここでは5秒待つようになっている。
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 | package com.example.demo; import org.apache.ibatis.annotations.Select; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Delete; import org.apache.ibatis.annotations.Insert; import org.apache.ibatis.annotations.Update; import java.util.Collection; @Mapper public interface UserDataMapper { /** * ユーザーデータテーブル(user_data)を全件取得する * @return ユーザーデータテーブル(user_data)を全データ */ @Select("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 " + " FROM USER_DATA u, M_SEX m WHERE u.sex = m.sex_cd ORDER BY u.id ") Collection<UserData> findAll(); /** * 指定したIDをもつユーザーデータテーブル(user_data)のデータを取得する * @param id ID * @return ユーザーデータテーブル(user_data)の指定したIDのデータ */ @Select("SELECT id, name, birth_year as birthY, birth_month as birthM" + ", birth_day as birthD, sex FROM USER_DATA WHERE id = #{id}" ) UserData findById(Long id); /** * 指定したIDをもつユーザーデータテーブル(user_data)のデータを * ロックをかけて取得する * @param id ID * @return ユーザーデータテーブル(user_data)の指定したIDのデータ */ @Select("SELECT id, name, birth_year as birthY, birth_month as birthM" + ", birth_day as birthD, sex FROM USER_DATA WHERE id = #{id}" + " FOR UPDATE WAIT 5 ") UserData findByIdForUpdate(Long id); /** * 指定したIDをもつユーザーデータテーブル(user_data)のデータを削除する * @param id ID */ @Delete("DELETE FROM USER_DATA WHERE id = #{id}") void deleteById(Long id); /** * 指定したユーザーデータテーブル(user_data)のデータを追加する * @param userData ユーザーデータテーブル(user_data)の追加データ */ @Insert("INSERT INTO USER_DATA (id, name, birth_year, birth_month, birth_day, sex)" + " VALUES (#{id}, #{name}, #{birthY}, #{birthM}, #{birthD}, #{sex})") void create(UserData userData); /** * 指定したユーザーデータテーブル(user_data)のデータを更新する * @param userData ユーザーデータテーブル(user_data)の更新データ */ @Update("UPDATE USER_DATA SET name = #{name}, birth_year = #{birthY}" + ", birth_month = #{birthM}, birth_day = #{birthD}" + ", sex = #{sex} WHERE id = #{id}") void update(UserData userData); /** * ユーザーデータテーブル(user_data)の最大値IDを取得する * @return ユーザーデータテーブル(user_data)の最大値ID */ @Select("SELECT NVL(max(id), 0) FROM USER_DATA") long findMaxId(); } |
なお、select ~ for update文についての詳細は、以下のサイトを参照のこと。
https://www.shift-the-oracle.com/sql/select-for-update.html
また、主要な処理を含むServiceImplクラスは以下の通り。createOrUpdateメソッド内で、更新時はMapperクラスのfindByIdForUpdateメソッドを呼び出し、その後10秒待機し、更新処理を行うようにしている。
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 | 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 public void deleteById(String id){ Long longId = stringToLong(id); mapper.deleteById(longId); } /** * {@inheritDoc} */ @Override @Transactional public void createOrUpdate(DemoForm demoForm){ //更新・追加処理を行うエンティティを生成 UserData userData = getUserData(demoForm); //IDが付与されていなければ追加し、 //付与されていれば更新処理を行う if(demoForm.getId() == null){ //追加処理 userData.setId(mapper.findMaxId() + 1); mapper.create(userData); }else{ Long longId = stringToLong(demoForm.getId()); try{ //更新時はロックをかけてユーザーデータを取得 mapper.findByIdForUpdate(longId); }catch (Exception ex){ throw new RuntimeException(ex); } //10秒ほど処理待ちする try{ Thread.sleep(10000); }catch (InterruptedException ex){ throw new RuntimeException(ex); } //更新処理 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()); 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()); return userData; } /** * 引数の文字列をLong型に変換する * @param id ID * @return Long型のID */ private Long stringToLong(String id){ try{ return Long.parseLong(id); }catch(NumberFormatException ex){ return null; } } } |
また、コントローラクラスは以下の通り。データ更新を行うsendメソッド内で、WAITのタイムアウトエラー等が発生した場合は、システムエラー画面に遷移するようにしている。
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 | package com.example.demo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.ModelAttribute; 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.RequestParam; import org.springframework.web.bind.annotation.SessionAttributes; import org.springframework.validation.BindingResult; import org.springframework.web.bind.support.SessionStatus; import org.springframework.validation.annotation.Validated; import java.util.ArrayList; import java.util.List; @Controller @SessionAttributes(types = {DemoForm.class}) public class DemoController { /** * Demoサービスクラスへのアクセス */ @Autowired private DemoService demoService; /** * ユーザーデータテーブル(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オブジェクト * @return 一覧画面の表示処理 */ @PostMapping(value = "/delete", params = "next") public String delete(DemoForm demoForm){ //指定したユーザーデータを削除 demoService.deleteById(demoForm.getId()); //一覧画面に遷移 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 完了画面 */ @RequestMapping(value = "/send", params = "next") public String send(@Validated DemoForm demoForm, Model model, BindingResult result){ //チェック処理を行い、エラーがなければ、更新・追加処理を行う String normalPath = "redirect:/complete"; String checkPath = demoService.checkForm(demoForm, result, "redirect:/complete"); if(normalPath.equals(checkPath)){ try{ //ユーザーデータがあれば更新し、無ければ追加する demoService.createOrUpdate(demoForm); }catch(RuntimeException ex){ //エラー時は、システムエラー画面に遷移 if(ex.toString().contains("ORA-30006")){ model.addAttribute("errClass", "WAITタイムアウトエラー"); }else{ model.addAttribute("errClass", ex.toString()); } return "system_error"; } } return checkPath; } /** * 完了画面に遷移する * @param sessionStatus セッションステータス * @return 完了画面 */ @GetMapping("/complete") public String complete(SessionStatus sessionStatus){ //セッションオブジェクトを破棄 sessionStatus.setComplete(); return "complete"; } /** * 入力画面に戻る * @return 入力画面 */ @RequestMapping(value = "/send", params = "back") public String sendBack(){ return "input"; } } |
また、今回追加したシステムエラー画面のHTMLソースは以下の通り。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <!DOCTYPE html> <html lang="ja" xmlns:th="http://www.thymeleaf.org"> <meta charset="UTF-8"> <title>エラーページ</title> </head> <body> <p><font color="red">システムエラーが発生しました。</font></p> <p th:text="'発生したエラー: ' + ${errClass}"> ここに発生したエラーのクラス名が設定されます </p> <br/><br/> <form method="post" th:action="@{/}"> <input type="submit" value="一覧に戻る" /> </form> </body> </html> |
その他のソースコード内容は、以下のサイトを参照のこと。
https://github.com/purin-it/java/tree/master/spring-boot-exclusive-2/demo
要点まとめ
- select ~ for update文を実行することで、悲観ロックの取得が行える。
- select ~ for update文には、「wait 秒数」や「nowait」オプションを付与することができる。