今回も引き続き、排他制御の1つである「悲観的排他制御」を利用した実装について述べる。ここでは、具体的なサンプルプログラムのソースコードを共有する。
前提条件
下記記事を参照のこと。
Spring BootのWEB画面上で排他制御を実装してみた(完成イメージと前提条件)今回は、データ更新処理内に排他制御を加えてみたので、そのサンプルプログラムを共有する。 排他制御は、1つのデータに複数のアクセスが...
作成したサンプルプログラムの内容
作成したサンプルプログラムの構成は以下の通り。
なお、上図の赤枠は、前提条件に記載したソースコードと比較し、変更になったソースコードを示す。赤枠のソースコードについては今後記載する。
まず、ユーザーデータ(USER_DATA)テーブルにアクセスするMapperクラスは以下の通り。今回は、悲観ロックをかけてデータ取得するfindByIdForUpdateメソッドを追加していて、ここでは5秒待つようになっている。
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秒待機し、更新処理を行うようにしている。
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のタイムアウトエラー等が発生した場合は、システムエラー画面に遷移するようにしている。
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ソースは以下の通り。
<!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」オプションを付与することができる。