Doma2の楽観ロックで排他制御する

Doma2の@Versionアノテーションを使用した楽観ロック方式で実現します。

Doma2の楽観ロックについては、以下の公式ドキュメントを参照してください。

以下のサンプルコードの動作確認環境については、 動作確認環境と依存ライブラリについて を参照してください。

処理フロー

  • 初期表示

    • 画面表示するデータをDBから取得します。

    • 取得したデータをセッションに格納します。

  • 更新

    • 画面の入力値をバリデーションします。

    • バリデーション後の入力値とセッションに格納したバージョン番号を使用して、DBのデータを更新します。

      • データの更新に成功した場合は、初期表示にリダイレクトします。

      • 楽観ロック例外が発生した場合は、HTTPステータスコードに409を設定して、エラーページを表示します。

実装例

Controllerでの userService.update(User) の実行時にDoma2の楽観排他制御が行われます。楽観排他に失敗した場合は、Doma2がOptimisticLockExceptionを送出します。 OptimisticLockExceptionは、 doma-spring-boot-starter がSpringのOptimisticLockingFailureExceptionに変換して再送出します。 詳細は、 Doma2が送出する例外の変換 を参照してください。

送出されたOptimisticLockingFailureExceptionはアプリケーション全体で横断的に処理することで、個別の機能で例外処理する必要がなくなります。詳細は、 例外ハンドリング を参照してください。

Controller
@Controller
@RequestMapping("/user")
@SessionAttributes(names = "form") // Formクラスをセッションに格納します
public class UserUpdateController {

    private final UserService userService;

    public UserUpdateController(UserService userService) {
        this.userService = userService;
    }

    @ModelAttribute(name = "form")
    public UserUpdateForm setup() {
        return new UserUpdateForm();
    }

    @GetMapping("/edit")
    public String edit(@ModelAttribute(name = "form") UserUpdateForm form) {
        User user = userService.find();
        // セッションに格納されているFormクラスに取得した情報を設定します
        form.setUserId(user.userId);
        form.setUserName(user.userName);
        form.setVersionNo(user.versionNo);
        return "user/edit";
    }

    @PostMapping("/update")
    public String update(@ModelAttribute(name = "form") @Validated UserUpdateForm form, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            return "user/edit";
        }
        // 画面の入力値と、セッションに格納されている情報を使用してデータを更新します
        userService.update(new UserDto(form.getUserId(), form.getUserName(), form.getVersionNo()));
        // データ更新に成功した場合は、初期表示にリダイレクトします
        return "redirect:/user/edit?success";
    }
}
Dao
/**
 * 楽観ロックエラーが発生した場合は、{@link org.springframework.dao.OptimisticLockingFailureException}が送出されます。
 * 呼出し元で例外を補足してハンドリングしてください。
 * <p/>
 * なお、サンプルアプリケーションでは{@link org.springframework.web.bind.annotation.ControllerAdvice}を使用して、
 * 全てのControllerを横断したエラーハンドリングを実施しています。
 * @see keel.controller.ErrorControllerAdvice
 */
@Update
Result<User> update(User user);
Entity
// 楽観ロック用のバージョンカラムには、@Versionを付与します。
@Version
public final Long versionNo;
ControllerAdvice
@ControllerAdvice
public class ErrorControllerAdvice {

    private final Logger LOGGER = LoggerFactory.getLogger(ErrorControllerAdvice.class);

    /**
     * 楽観ロック例外が発生した場合は、HTTPステータスコードに409を設定します。
     *
     * @param e 楽観ロック例外
     */
    @ExceptionHandler(OptimisticLockingFailureException.class)
    @ResponseStatus(value = HttpStatus.CONFLICT, reason = "楽観排他エラー")
    public void optimisticLockingFailureExceptionHandler(OptimisticLockingFailureException e) {
        LOGGER.debug("排他制御エラーが発生しました", e);
    }
}

サンプル全体は doma2-optimistic-lock-sample を参照してください。