Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

ロングトランザクションにおける楽観ロック(Spring+JPA)

More than 3 years have passed since last update.

JPAに楽観ロックがサポートされているのでそれを使おうとしたけれど、ロングトランザクションの場合、どうやるのか不明でここ3日ほど悩みました。
※ロングトランザクションという言葉もこの調査で初めて知りましたw

やりたかったのは以下のようなこと。

①ID入力→検索ボタン押下→DBからデータ取得→画面にデータ表示
②画面入力でデータを書き換える
③更新ボタン押下→DBのデータ更新

この①~③の処理をひとつのトランザクションでやるようなイメージで①~③の間に他の誰かがデータ更新していたら異常としたい。

というわけで、今回はロングトランザクション(複数リクエスト)時の楽観ロック方法
まずは楽観ロックするためのバージョニング用カラムを追加してEntityにも対応するプロパティと@Versionアノテーションを設定

NameT.java
package jp.test.spring.entity;

import java.io.Serializable;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Version;

@Entity
@Table(name = "NAMET")
public class NameT implements Serializable {

    @Id
    @Column(nullable = false)
    private int id;

    @Column
    private String name;

    @Version                // ←@Versionアノテーション追加
    private int version;    // ←バージョニング用のカラムを追加

    // getter/setter
}

@Transactionalアノテーションが設定されたDB更新処理関数でEntityManager.lock()を使って楽観or悲観ロックを取得。
※第2引数に渡すパラメータでロック種別が設定可能

TestServiceImpl.java
@Service("testService")
public class TestServiceImpl implements TestService {

    @Transactional
    @Override
    public void updateNameT(NameT nameT) {
        NameT updateNameT = entityManager.find(NameT.class, nameT.getId());
        entityManager.lock(updateNameT, LockModeType.OPTIMISTIC);
        updateNameT.setName(nameT.getName());

        entityManager.merge(updateNameT);
    }
}

けど、これだと一つのリクエスト内でしかトランザクション張れないので、ロングトランザクションの場合どうやるの?ってずっと悩んでました。。。

ロック取得前に手動でバージョン情報を確認するしかないのでは?と思っていろいろ調べたらTERASOLUNAさんのサイト(https://terasolunaorg.github.io/guideline/public_review/ArchitectureInDetail/ExclusionControl.html#id12 )にそのような記載がありました。
手動で確認してorg.springframework.orm.ObjectOptimisticLockingFailureExceptionをthrowすればいいみたいです。
この場合、関数にthrows ObjectOptimisticLockingFailureExceptionをつけなくてもOKでした。

というわけで、修正したソースが以下になります。
ObjectOptimisticLockingFailureExceptionコンストラクタの第1引数にEntityのClass<?>、第2引数にEntityのPrimaryKeyのObjectを設定するみたいです。

TestServiceImpl(修正後).java
@Service("testService")
public class TestServiceImpl implements TestService {

    @Transactional
    @Override
    public void update(NameT nameT) {
        NameT updateNameT = entityManager.find(NameT.class, nameT.getId());
        if (updateNameT.getVersion() != nameT.getVersion()) {
            throw new ObjectOptimisticLockingFailureException(NameT.class, nameT.getId());
        }

        entityManager.lock(updateNameT, LockModeType.OPTIMISTIC);
        updateNameT.setName(nameT.getName());

        entityManager.merge(updateNameT);
    }
}

update()に渡すNameTは検索ボタン押下でデータ取得したときのバージョン情報が格納されたものである必要あります。
更新時にバージョン情報が違うとObjectOptimisticLockingFailureExceptionが発生してデータ更新されません。しかもロールバックが実行されるぽい。

やり方がわかったので今回のプロジェクトは楽観ロックで実装できそうです。
めでたしめでたし

zateon
組み込み系開発(C++)→Java開発→Web開発(Java、php、Angular) インフラ周りも扱っています(主にApache)
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away