Spring Boot DB連携

Spring BootでMyBatisを利用しSQL Serverに接続するアプリケーションで楽観ロックを利用してみた

これまでこのブログで、Spring BootでMyBatisを利用しDB更新するアプリケーションをいくつか取り上げてきたが、DB更新時に、楽観ロックを利用することもできる。

今回は、Spring BootでMyBatisを利用しSQL Serverに接続するアプリケーションで楽観ロックを利用してみたので、そのサンプルプログラムを共有する。

なお、楽観ロックを含む排他制御については、以下のサイトを参照のこと。
https://qiita.com/NagaokaKenichi/items/73040df85b7bd4e9ecfc

前提条件

下記記事の実装が完了していること。

Spring BootでSQL Serverに接続しMyBatisを利用してみた今回は、Spring Bootアプリケーションで接続するデータベースをSQL Serverに変更してみたので、そのサンプルプログラムを共...

また、SQL ServerのUSER_DATAテーブルに、以下の記事の「書き込み先テーブルのカラム追加」に記載のカラム追加が完了していること。

Azure Function上でCSVファイルのデータをDBに取り込むプログラムで楽観ロックを使ってみたこれまでこのブログで、Spring BatchのChunkモデルを用いて、Blob上のCSVファイルをDBのテーブルに書き込む処理を作成...

サンプルプログラムの作成

作成したサンプルプログラムの構成は、以下の通り。
サンプルプログラムの構成
なお、上記の赤枠は、前提条件のプログラムから追加・変更したプログラムである。

エンティティクラス・フォームクラスの内容は以下の通りで、カラム「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



サラリーマン型フリーランスSEという働き方でお金の不安を解消しよう先日、「サラリーマン型フリーランスSE」という働き方を紹介するYouTube動画を視聴しましたので、その内容をご紹介します。 「サ...

サンプルプログラムの実行結果(正常時)

サンプルプログラムの実行結果(正常時)は、以下の通り。

1) 実行前のUSER_DATAテーブルの中身は、以下の通り。

SELECT * FROM dbo.USER_DATA ORDER BY id ASC
サンプルプログラムの実行結果(正常時)_1

2) Spring Bootアプリケーションを起動し、「http:// (ホスト名):(ポート番号)/」にアクセスすると、以下の画面が表示される。ここで「データ追加」ボタンを押下する。
サンプルプログラムの実行結果(正常時)_2

3) 以下のように入力後、「確認」ボタンを押下する。
サンプルプログラムの実行結果(正常時)_3

4) そのまま「送信」ボタンを押下する。
サンプルプログラムの実行結果(正常時)_4

5) 以下のように、完了画面が表示される。ここで「一覧に戻る」ボタンを押下する。
サンプルプログラムの実行結果(正常時)_5

6) この時点でのUSER_DATAテーブルの中身は以下の通りで、id=3のデータがVERSION=0で追加されていることが確認できる。

SELECT * FROM dbo.USER_DATA ORDER BY id ASC
サンプルプログラムの実行結果(正常時)_6

7) 一覧画面の表示は、以下の通り。ここで、ID=3の「更新」リンクを押下する。
サンプルプログラムの実行結果(正常時)_7

8) 以下の内容に変更後、「確認」ボタンを押下する。
サンプルプログラムの実行結果(正常時)_8

9) そのまま「送信」ボタンを押下する。
サンプルプログラムの実行結果(正常時)_9

10) 以下のように、完了画面が表示される。ここで「一覧に戻る」ボタンを押下する。
サンプルプログラムの実行結果(正常時)_10

11) この時点でのUSER_DATAテーブルの中身は以下の通りで、id=3のデータがVERSION=1で更新されていることが確認できる。

SELECT * FROM dbo.USER_DATA ORDER BY id ASC
サンプルプログラムの実行結果(正常時)_11

12) 一覧画面の表示は、以下の通り。ここで、ID=3の「削除」リンクを押下する。
サンプルプログラムの実行結果(正常時)_12

13) そのまま「送信」ボタンを押下する。
サンプルプログラムの実行結果(正常時)_13

14) 一覧画面に戻り、以下のようにID=3のデータが削除されていることが確認できる。
サンプルプログラムの実行結果(正常時)_14

15) この時点でのUSER_DATAテーブルの中身は以下の通りで、id=3のデータ削除されていることが確認できる。

SELECT * FROM dbo.USER_DATA ORDER BY id ASC
サンプルプログラムの実行結果(正常時)_15



ウズウズカレッジJavaコースはわかりやすい動画教材と充実した就業サポートで優良企業を目指せるプログラミングスクールだったJavaは、世界中で広く使われていて、現在の需要が高く将来性もある開発言語になります。 https://www.acrovision....

サンプルプログラムの実行結果(エラー時)

サンプルプログラムの実行結果(エラー時)は、以下の通り。なお、下図では更新時のエラーを表示しているが、削除時も同様にエラーメッセージが表示される。

1) 実行前のUSER_DATAテーブルの中身は、以下の通り。

SELECT * FROM dbo.USER_DATA ORDER BY id ASC
サンプルプログラムの実行結果(異常時)_1_1

2) Spring Bootアプリケーションを起動し、「http:// (ホスト名):(ポート番号)/」にアクセスすると、以下の画面が表示される。ここでID=2のデータの「更新」リンクを押下する。
サンプルプログラムの実行結果(異常時)_1_2

3) 以下のように変更後、「確認」ボタンを押下する。
サンプルプログラムの実行結果(異常時)_1_3

4) 以下のSQLを実行し、入力画面を開いた時と違うVERSIONになるようにする。

UPDATE dbo.USER_DATA SET VERSION = 1 WHERE ID = 2
サンプルプログラムの実行結果(異常時)_1_4

5) 4)のSQL実行後、ID=2のUSER_DATAテーブルのデータは以下のようになる。

SELECT * FROM dbo.USER_DATA WHERE ID = 2
サンプルプログラムの実行結果(異常時)_1_5

6) そのまま「送信」ボタンを押下する。
サンプルプログラムの実行結果(異常時)_1_6

7) 以下のように、楽観ロックエラーになることが確認できる。
サンプルプログラムの実行結果(異常時)_1_7

8) 再度「http:// (ホスト名):(ポート番号)/」にアクセスすると、以下の画面が表示される。ここでID=2のデータの「更新」リンクを押下する。
サンプルプログラムの実行結果(異常時)_1_8

9) 以下のように変更後、「確認」ボタンを押下する。
サンプルプログラムの実行結果(異常時)_1_9

10) 更新対象データに行ロックをかけるため、トランザクションを開始する。
サンプルプログラムの実行結果(異常時)_1_10

11) 以下のSQLを実行し、更新対象データに行ロックをかける。

SELECT * FROM dbo.USER_DATA WITH(ROWLOCK, UPDLOCK, NOWAIT) WHERE ID = 2
サンプルプログラムの実行結果(異常時)_1_11

12) そのまま「送信」ボタンを押下する。
サンプルプログラムの実行結果(異常時)_1_12

13) 以下のように、DBロック取得エラーになることが確認できる。
サンプルプログラムの実行結果(異常時)_1_13

14) 更新対象データの行ロックを解除するため、トランザクションを終了する。
サンプルプログラムの実行結果(異常時)_1_14_1

サンプルプログラムの実行結果(異常時)_1_14_2



「FlexClip」はテンプレートとして利用できる動画・画像・音楽などが充実した動画編集ツールだったテンプレートとして利用できるテキスト・動画・画像・音楽など(いずれも著作権フリー)が充実している動画編集ツールの一つに、「FlexCli...

サンプルプログラムの実行結果(更新件数エラー時)

サンプルプログラムの実行結果(更新件数エラー時)は、以下の通り。なお、下図では更新時のエラーを表示しているが、削除時も同様にエラーメッセージが表示される。

1) 以下のように、バージョンが異なっている場合にエラーにする処理をコメントアウトする。
サンプルプログラムの実行結果(異常時)_2_1

2) 実行前のUSER_DATAテーブルの中身は、以下の通り。

SELECT * FROM dbo.USER_DATA ORDER BY id ASC
サンプルプログラムの実行結果(異常時)_2_2

3) Spring Bootアプリケーションを起動し、「http:// (ホスト名):(ポート番号)/」にアクセスすると、以下の画面が表示される。ここでID=2のデータの「更新」リンクを押下する。
サンプルプログラムの実行結果(異常時)_2_3

4) 以下のように変更後、「確認」ボタンを押下する。
サンプルプログラムの実行結果(異常時)_2_4

5) 以下のSQLを実行し、入力画面を開いた時と違うVERSIONになるようにする。

UPDATE dbo.USER_DATA SET VERSION = 1 WHERE ID = 2
サンプルプログラムの実行結果(異常時)_2_5

6) 5)のSQL実行後、ID=2のUSER_DATAテーブルのデータは以下のようになる。

SELECT * FROM dbo.USER_DATA WHERE ID = 2
サンプルプログラムの実行結果(異常時)_2_6

7) そのまま「送信」ボタンを押下する。
サンプルプログラムの実行結果(異常時)_2_7

8) 以下のように、楽観ロックエラーになることが確認できる。
サンプルプログラムの実行結果(異常時)_2_8

要点まとめ

  • 画面上で楽観ロックを実装するには、登録・更新の直前でバージョンをチェックすると共に、登録・更新時の同一バージョンの更新件数が1件であることをチェックすればよい。