JPA 2.0の新機能 悲観的ロックを試す

JPA1.0では楽観的ロックだけ仕様が定義されていた。実際は各プロバイダが独自にヒント等で悲観的ロックを用意していたのだが、それもやっと定義されたために安心して使うことが可能だ。


楽観的ロックはバージョン番号を用意し、トランザクションの開始直前に取得した番号を元に更新にしくというもの。もしそれが存在しない場合、すでに更新されたものとみなし、トランザクションを最初からリトライさせる。

たまに楽観的ロックのようにバージョン番号を利用した値を画面表示時にあらかじめ持っておくといった、アプリケーションでのロックをかけてるところも多いけど、それは楽観的ロックではなく、そういう仕様のアプリケーション。あくまでもトランザクションをどう整合性とるかというだけが楽観的ロックだ。


そもそも更新時にはどのみち行ロック等がかかるわけで。更新方法によってロックがかかる順番が変わることも多いため、デッドロックの可能性が大幅に増える気がして楽観的ロックは個人的には好きではないかな。何を元に排他するべきかというのは一番大事な基本的なとこだと思うので普通はあらかじめ決めておくはず。楽観的ロックの場合テーブルアクセスの順番に固定されて更新するのは実はつらいはずなのだが、あまりそういうことは聞かないのはなんだろ。

というわけで、今回はデータアクセスで超重要な悲観的ロックを試す。

findでロック

まずは1件取得時にロックしてみる。主キーでアクセスするfindというメソッドがあるのでこれを使おう。

サンプルコードは2つのスレッドを起動し、同じデータにアクセスしている。完全なコード。

package jpa2test;

import java.util.HashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.LockModeType;
import javax.persistence.Persistence;
import jpa2test.entity.Employee;


public class Main  {

    private static class Task extends Thread{
        EntityManager em;
        int value;

        Task(EntityManager em,int value) {
            this.em = em;
            this.value = value;
        }

        @Override
        public void run() {
            Long id = 1l;

            em.getTransaction().begin();

            //データを1件取得と同時にロック
            Employee data = em.find(Employee.class, id , LockModeType.PESSIMISTIC_WRITE);
            
            System.out.println("ロック開始:"+value);

            data.setSalary(value);//データ変更

            try {
                Thread.sleep(2*1000);
            } catch (InterruptedException ex) {
                Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex);
            }

            System.out.println("コミット直前:"+value);
            em.getTransaction().commit();
            em.close();

        }


    }
    public static void main(String[] args) throws Exception{
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("JPA2TestPU");
        Task task1 = new Task(emf.createEntityManager() , 123);
        Task task2 = new Task(emf.createEntityManager() , 456);

        //どっちが先に実行されるかはわからない
        task1.start();
        task2.start();
    }
}

見てわかるとおりロックモードを指定可能な引数が増えているメソッドが用意された。LockModeTypeはenumでいろいろと用意されているので見てみるとよい。

ログは以下のようになっている。わかりやすいように共有キャッシュNONEにして実行している。キャッシュがあると発行されるSQLが大幅に減ってしまう(ちゃんと整合性は保たれているのでよいけど)。

[EL Fine]: 2009-11-09 23:03:52.734--ClientSession(26542488)
  --Connection(5862378)--Thread(Thread[Thread-0,5,main])
  --SELECT ID, ENTDATE, NAME, SALARY FROM EMPLOYEE WHERE (ID = ?) FOR UPDATE WITH RS
        bind => [1]
[EL Fine]: 2009-11-09 23:03:52.734--ClientSession(24744797)
  --Connection(17152415)--Thread(Thread[Thread-1,5,main])
  --SELECT ID, ENTDATE, NAME, SALARY FROM EMPLOYEE WHERE (ID = ?) FOR UPDATE WITH RS
        bind => [1]
ロック開始:456
コミット直前:456
[EL Fine]: 2009-11-09 23:03:54.765--ClientSession(24744797)
  --Connection(17152415)--Thread(Thread[Thread-1,5,main])
  --UPDATE EMPLOYEE SET SALARY = ? WHERE (ID = ?)
        bind => [456, 1]
ロック開始:123
コミット直前:123
[EL Fine]: 2009-11-09 23:03:56.781--ClientSession(26542488)
  --Connection(5862378)--Thread(Thread[Thread-0,5,main])
  --UPDATE EMPLOYEE SET SALARY = ? WHERE (ID = ?)
        bind => [123, 1]

コミット時にSQL発行するのでちょっとわかりにくいがちゃんと1秒まってくれている。

しかし、EclipselinkもToplinkと同様にログが見やすいね。発効しているSQLとパラメータがよくわかる。

取得後にロック

取得時にロックするのはわかりやすいが、先にデータを取得した後、特定のタイミングでロックをかけたいということもある。そういうときには以下のようにするとよい。

Employee data = em.find(Employee.class, id );
em.lock(data, LockModeType.PESSIMISTIC_WRITE );

エンティティとロックモードを指定するだけ。簡単。

ログは以下のようになる。

[EL Fine]: 2009-11-09 22:57:45.265--ServerSession(3912376)
  --Connection(17152415)--Thread(Thread[Thread-1,5,main])
  --SELECT ID, ENTDATE, NAME, SALARY FROM EMPLOYEE WHERE (ID = ?)
        bind => [1]
[EL Fine]: 2009-11-09 22:57:45.265--ServerSession(3912376)
  --Connection(5862378)--Thread(Thread[Thread-0,5,main])
  --SELECT ID, ENTDATE, NAME, SALARY FROM EMPLOYEE WHERE (ID = ?)
        bind => [1]
[EL Fine]: 2009-11-09 22:57:45.296--ClientSession(7896426)
  --Connection(17152415)--Thread(Thread[Thread-0,5,main])
  --SELECT ID, ENTDATE, NAME, SALARY FROM EMPLOYEE WHERE (ID = ?) FOR UPDATE WITH RS
        bind => [1]
[EL Fine]: 2009-11-09 22:57:45.296--ClientSession(23414511)
  --Connection(5862378)--Thread(Thread[Thread-1,5,main])
  --SELECT ID, ENTDATE, NAME, SALARY FROM EMPLOYEE WHERE (ID = ?) FOR UPDATE WITH RS
        bind => [1]
ロック開始:456
コミット直前:456
[EL Fine]: 2009-11-09 22:57:47.312--ClientSession(23414511)
--Connection(5862378)--Thread(Thread[Thread-1,5,main])
--UPDATE EMPLOYEE SET SALARY = ? WHERE (ID = ?)
        bind => [456, 1]
ロック開始:123
コミット直前:123
[EL Fine]: 2009-11-09 22:57:49.312--ClientSession(7896426)
--Connection(17152415)--Thread(Thread[Thread-0,5,main])
--UPDATE EMPLOYEE SET SALARY = ? WHERE (ID = ?)
        bind => [123, 1]

単純なSELECTのあとにFOR UPDATEつきのSQLが発行されているのがわかる。

複数のエンティティをまとめてロック

では複数のエンティティをまとめてロックする場合はどうだろうか。

これもQueryにロックモードを指定するメソッドが追加されているのでそれを使うだけ。

String jpql = "select e from Employee e where e.id < 5";
List<Employee> result = em.createQuery(jpql).
            setLockMode(LockModeType.PESSIMISTIC_WRITE).getResultList();

ログは以下のようになっている。

2009-11-09 23:01:01.359--ClientSession(1043272)
  --Connection(17152415)--Thread(Thread[Thread-0,5,main])
  --SELECT ID, ENTDATE, NAME, SALARY FROM EMPLOYEE WHERE (ID < 5) FOR UPDATE WITH RS

そのまんまだね。

NOWAIT

ちなみにタイムアウト時間を0にして実行するとFOR UPDATE NOWAITが実行される。

オプションでMapをパラメータに渡す命令もロックと一緒に用意されているため、それを併用すると個別に設定できる。

HashMap<String,Object> option = new HashMap<String,Object>();
option.put("javax.persistence.lock.timeout","0");

のだが、JavaDB(Dervy)ではNOWAITという命令はないのでエラーがでとる(^-^;
PostgreSQLOracleなら問題ないと思う。

0以外だとどうも動きが怪しい。そこでロック確保をあきらめるようで、LockTimeoutExceptio例外が飛んでくれないようだ。これじゃつかえん。未実装なのかな。

バージョン番号との併用

あと、悲観的ロックでもバージョン番号を更新するモードも用意されている。これで大概のところは楽観的ロック、大事な更新の多いところだけは意識して悲観的ロックを選択するといったことも可能だ。更新が何度もぶつかる場合楽観的ロックだとリトライをどう制御すればよいかというのは実は難しいので大概の業務系は悲観的ロックを使うことになるだろう。


とりあえず、一番重要な排他制御が標準化されたのは大きい。これくらべればCriteriaなんておまけみたいなもん。