Hibernate 入門記 トランザクション & 楽観的ロック

今日は「10. Transactions And Concurrency」です.このリファレンスドキュメント,現時点では 19 章まであります.ということは,この 10 章が折り返し点ですね.ずいぶん長いこと入門してきたような気がするのですが,やっと折り返し点に到着ですか... なんちゅー長さだ.心が折れそうです (苦笑).


ともあれ,10 章の学習スタート!!!!
まず,HibernateDBMS じゃないから自分でトランザクション制御はしないよ,JDBC コネクションに委譲するんだよってなことが書かれています.それから,JTA にも対応しているよ〜ってことらしいです.


ちうことで「10.1. Configurations, Sessions and Factories」.なぜか今頃になって SessionFactorySession の話.軽くまとめると...

SessionFactory
インスタンス化のコストが高い.
スレッドセーフである.
全てのアプリケーションスレッドから共有される.
Session
インスタンス化のコストは低い.
スレッドセーフではない.
一つのビジネスプロセスで利用された後,破棄される.

らじゃあ.
後は,ちょっとした注意書き.フラッシュはメモリの状態を RDBMS に反映するのであって,その逆 (RDBMS の状態をメモリに反映) するのではないとかなんとか.


ということでスイスイと「10.2. Threads and connections」へ.
Session を使う上での注意書き.

  • 一つの JDBC コネクションに対して,複数の SessionTransaction を同時に作成してはならない.
  • 一つのトランザクションに対して,複数の Session を作成してはならない.
  • Session はスレッドセーフではない! ので,複数のスレッドから同時にアクセスしてはならない.

らじゃあ.


ガンガン行きますよぉ.次は「10.3. Considering object identity」.
アプリケーションは,同じ永続オブジェクトに異なったトランザクションからアクセスすることが出来ます.しかし,同一の永続オブジェクトが複数のセッションで共有されることはありません.どういうことかというと...
Java 的には,二つのオブジェクトが同一かどうかは,次のように判断することができます.

foo == bar

この場合,RDBMS 上の同じレコードを表現している永続オブジェクト同士でも,同一とは見なされるとは限りません.
一方,二つの永続オブジェクトが RDBMS 上で同じレコードを表しているかどうかは,次のように判断することが出来ます.

foo.getId().equals(bag.getId())

同一性ではなく,ID プロパティの同値性で判断するわけですね.
そんなわけで (どんなわけで?),永続オブジェクトが RDBMS 上の同じレコードを表現する場合でも,JVM 上で同一のインスタンスになるとは限らないということですね.ただし,一つの Session の中では,== を使った同一性の判定を使っても大丈夫とのことです.
また,永続オブジェクトは複数のスレッドで共有されないので,排他制御 (synchronized) を行う必要もないとのこと.らじゃあ.


そして次は「10.4. Optimistic concurrency control」.ついに出たな,楽観的同時実行制御!! ここがこの章の目玉でしょう,きっと.
通常のアプリケーションは,ユーザと対話を行いながら一連のデータベースアクセスを行うわけですが,その一連の DB アクセスをそのまま DB のトランザクションにするわけにはいきません.ユーザが考えている間中ロックをかけっぱなしにするわけにはいかないとかありますからね.
Hibernate では,ユーザと対話しながらの一連の処理全体を,「アプリケーション・トランザクション」と呼ぶらしいです.一つのアプリケーション・トランザクションは通常,複数のデータベース・トランザクションに分割されます.
分割された最後のデータベース・トランザクションだけが DB を更新し,その他のデータベース・トランザクションでは DB に参照のみ行うという場合に限り,トランザクションの原子性 (ACID の A) が保てるとのことです.最初のデータベース・トランザクションで画面を表示して,後のデータベース・トランザクションで DB を更新してっていう場合は大丈夫ってことですね.
それでその,楽観的な同時実行制御を高い並行性とスケーラビリティで実現する唯一の手段はバージョニングであるとのことです.
Hibernate では,そのバージョニングを使ったアプリケーションの書き方について3つのアプローチがあるそうです.


ということで,まずは「10.4.1. Long session with automatic versioning」.
このアプローチは,一つの Sessionインスタンスを使い回すというものです.ただし,ユーザとのやりとりをする際には, SessionJDBC コネクションから切り離します.
この場合,永続オブジェクトのバージョンチェックや,以前のセッションで使用して切り離されている永続オブジェクトを再接続するなどの操作は不要とのことです.
SessionJDBC コネクションから切り離すには,Sesshion

    • Connection disconnect()

を呼び出します.戻り値は,Hibernate の外から JDBC コネクションを与えていた場合にはそのコネクションが返されます.Hibernate がコネクションを割り当てた場合には,null が返されます.
切り離された SessionJDBC コネクションに再接続するには,Session

    • void reconnect()
    • void reconnect(Connection connection)

を使います.
このアプローチの問題点は,Session が大きくなりすぎるかもしれないということらしいです.その Session でアクセスした永続オブジェクトを抱え込んでしまうためでしょう.そのため,HTTP セッションなどでは扱える容量が小さすぎて Session を保存できないかもしれないとかなんとか.
ということで,リクエスト/レスポンスのサイクルが少ない,負荷の軽い状況でのみ使いましょうとのことです.らじゃあ.


続いて「10.4.2. Many sessions with automatic versioning」.
こちらのアプローチは,データベース/トランザクションごとに新たな Session を作成します.ただし,複数の Session にまたがって同一の永続オブジェクトを使い回します.
そのためには,すでに学習済みの Session#update() または Session#saveOrUpdate() を使います.あるいは,更新するわけではなく,単にバージョンチェックをしたいだけという場合には,引数に LockMode.READ を指定して Session#lock() を呼び出すことも出来ます.
その他の (Session#update() などでデタッチしていない) 永続オブジェクトは,新たに DB からロードされます.


そしt「10.4.3. Application version checking」.
最後のアプローチでも,データベース・トランザクションごとに新たな Session を作成しますが,アプリケーションで明示的にバージョンのチェックを行います.その場合でも,バージョンの更新そのものは Hibernate におまかせ出来ます.


という感じなのですが,肝心のバージョンそのもの説明が出てきません.バージョンってどうやって定義するの? 説明しよう.それは「5.1.7. version (optional)」に説明されているのだ.ぐはぁっ,スキップしてますがな.
ということで,ここで 5 章に逆戻り.バージョンのマッピングについて学習しましょう.
バージョンもまた,永続オブジェクトのプロパティであり,テーブル上のカラムにマッピングされます.そのマッピングを指定するのが

  • <version> 要素

です.
こいつは次の属性を持っています.

name
プロパティ名を指定します.必須です.
column
カラム名を指定します.
デフォルトはプロパティ名です.
type
カラムの型を指定します.
デフォルトは integer です.
access
Hibernate がプロパティにアクセスする方法を指定します.
デフォルトは property です.
unsaved-value
まだ永続化されていないインスタンスにおける値を指定します.nullnegativeundefined のいずれかです.
デフォルトは undefinedK です.

この <version> 要素とそっくりなものとして,

  • <timestamp> 要素

というものもあります.こちらはバージョンを数値 (番号) ではなく,タイムスタンプで表現するもので,type 属性を除いて同じ属性を持っています.
<version> 要素または <timestamp> 要素は,どちらか一方を一つだけ,<discrimenator> 要素 (覚えていますか? 継承で使ったやつです) と <property> 要素の間に記述します.
よし,これでお試しが出来るぞ.
でも,10章も残り少しなので,一気に突っ走りましょう.


ということで「10.5. Session disconnection」です.
でもこれ,さっき見たばっかりなんですが.特に新しい情報はなさそう.あ,SessionTransaction は1対多だと書いてある.そりゃそうだろうとは思いますが.


最後は「10.6. Pessimistic Locking」です.
これも大部分はすでに学習済み.Session#lock(LockMode) の解説です.軽くまとめると...

  • Session#load() にロックモード UPGRADE または UPGRADE_NOWAIT を指定した場合,該当のオブジェクトがまだロードされていなければ,SELECT ... FOR UPDATE (NOWAIT) を使ってロードされます.永続オブジェクトがすでにロード済みで,そのロックモードが指定されたものより低レベルの場合は,Session#lock() が呼び出されます.
  • Session#lock() にロックモード READUPGRADEUPGRADE_NOWAIT を指定した場合,バージョンのチェックが行われます.


最後かなーり駆け足になってしまいましたが,こんなものかなぁ.
そんなわけで (どんなわけで?) 今度こそお試しです.
ネタはたくさんありそうですが,実際に使いそうなものというと楽観的ロックの 2 番目のアプローチだと思います.ということで,やってみましょう.
まずはテーブル.今回はモデルのみ.

CREATE TABLE MODEL (
    ID INTEGER IDENTITY PRIMARY KEY,
    VERSION INTEGER,
    NAME VARCHAR,
    MAGAZINE VARCHAR
)

バージョン番号を保持するカラムを付け加えています.
そしてモデルの永続クラス.

package study;
import java.io.Serializable;
import net.sf.hibernate.CallbackException;
import net.sf.hibernate.Lifecycle;
import net.sf.hibernate.Session;

public class Model implements Lifecycle {
    int id = -1;
    int version = -1;
    String name;
    String magazine;

    public Model() {
    }
    public Model(String name, String magazine) {
        this.name = name;
        this.magazine = magazine;
    }
    public String toString() {
        return name + " (" + version + ") : " + magazine;
    }

    public void onLoad(Session s, Serializable id) {
        System.out.println("onLoad() : " + toString());
    }
    public boolean onSave(Session s) throws CallbackException {
        System.out.println("onSave() : " + toString());
        return false;
    }
    public boolean onUpdate(Session s) throws CallbackException {
        System.out.println("onUpdate() : " + toString());
        return false;
    }
    public boolean onDelete(Session s) throws CallbackException {
        System.out.println("onDelete() : " + toString());
        return false;
    }
}

今回は LifiCycleimplements しています.
モデルのマッピングファイル.

<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
    "-//Hibernate/Hibernate Mapping DTD 2.0//EN"
    "http://hibernate.sourceforge.net/hibernate-mapping-2.0.dtd"
>
<hibernate-mapping auto-import="false" package="study" default-access="field">
    <class name="Model">
        <id name="id" unsaved-value="-1">
            <generator class="identity"/>
        </id>
        <version name="version" unsaved-value="negative"/>
        <property name="name"/>
        <property name="magazine"/>
    </class>
</hibernate-mapping>

<version> 要素を指定しています.
こいつを hibernate.cfg.xml に記述します.
そして実行用のクラス.

package study;
import java.util.Iterator;
import java.util.List;
import net.sf.hibernate.LockMode;
import net.sf.hibernate.Session;

public class Main {
    public static void main(String[] args) {
        try {
            HibernateTemplate template = new HibernateTemplate();

            System.out.println("\n*** create models ***");
            template.process(new HibernateCallback() {
                public Object doProcess(Session session) throws Exception {
                    session.save(new Model("Yuri Ebihara", "CanCam"));
                    session.save(new Model("Sayo Aizawa", "ViVi"));
                    session.save(new Model("Emi Suzuki", "Seventeen"));

                    return null;
                }
            });

            System.out.println("\n*** session 1-1 ***");
            final List session1Models = (List) template
                    .process(new HibernateCallback() {
                        public Object doProcess(Session session)
                                throws Exception {
                            return session.find("from study.Model model");
                        }
                    });
            Iterator it = session1Models.iterator();
            while (it.hasNext()) {
                System.out.println(it.next());
            }

            System.out.println("\n*** session 2-1 ***");
            final List session2Models = (List) template
                    .process(new HibernateCallback() {
                        public Object doProcess(Session session)
                                throws Exception {
                            return session.find("from study.Model model");
                        }
                    });
            it = session1Models.iterator();
            while (it.hasNext()) {
                System.out.println(it.next());
            }

            System.out.println("\n*** session 1-2 ***");
            template.process(new HibernateCallback() {
                public Object doProcess(Session session) throws Exception {
                    Iterator it = session1Models.iterator();
                    while (it.hasNext()) {
                        Model model = (Model) it.next();
                        if ("Yuri Ebihara".equals(model.name)) {
                            session.lock(model, LockMode.READ);
                        }
                        if ("Sayo Aizawa".equals(model.name)) {
                            model.magazine = "BOAO";
                            session.update(model);
                        }
                        else if ("Emi Suzuki".equals(model.name)) {
                            model.magazine = "PINKY";
                            session.update(model);
                        }
                    }
                    return null;
                }
            });
            it = session1Models.iterator();
            while (it.hasNext()) {
                System.out.println(it.next());
            }

            System.out.println("\n*** session 2-2 ***");
            template.process(new HibernateCallback() {
                public Object doProcess(Session session) throws Exception {
                    Iterator it = session2Models.iterator();
                    while (it.hasNext()) {
                        Model model = (Model) it.next();
                        if ("Yuri Ebihara".equals(model.name)) {
                            session.lock(model, LockMode.READ);
                        }
                        if ("Sayo Aizawa".equals(model.name)) {
                            model.magazine = "WEB+DB PRESS";
                            session.update(model);
                        }
                        else if ("Emi Suzuki".equals(model.name)) {
                            model.magazine = "Java World";
                            session.update(model);
                        }
                    }
                    return null;
                }
            });
            it = session1Models.iterator();
            while (it.hasNext()) {
                System.out.println(it.next());
            }
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }
}

最初のセッションでは,モデルのインスタンスを生成しています.
次の2つのセッションは,単純にモデルのリストを取得しています.この二つのセッションは,並行して動く二つのアプリケーション・トランザクションの前半のセッションを模したものです.
その次の二つのセッションでは,前のセッションで取得した永続オブジェクトを参照・更新しています.友里ちゃんはバージョンのチェックだけしています.紗世ちゃんとえみちいは雑誌を更新しています.先に更新するセッション 1-1 の方は問題なく更新できるはずですが,後から更新するセッション 2-2 の方はどうなっちゃうのでしょうか?
ということで,実行!!!!!!

*** create models ***
onSave() : Yuri Ebihara (-1) : CanCam
Hibernate: insert into Model (version, name, magazine, id) values (?, ?, ?, null)
Hibernate: call identity()
onSave() : Sayo Aizawa (-1) : ViVi
Hibernate: insert into Model (version, name, magazine, id) values (?, ?, ?, null)
Hibernate: call identity()
onSave() : Emi Suzuki (-1) : Seventeen
Hibernate: insert into Model (version, name, magazine, id) values (?, ?, ?, null)
Hibernate: call identity()

*** session 1-1 ***
Hibernate: select model0_.id as id, model0_.version as version, 
               model0_.name as name, model0_.magazine as magazine 
           from Model model0_
onLoad() : Yuri Ebihara (0) : CanCam
onLoad() : Sayo Aizawa (0) : ViVi
onLoad() : Emi Suzuki (0) : Seventeen
Yuri Ebihara (0) : CanCam
Sayo Aizawa (0) : ViVi
Emi Suzuki (0) : Seventeen

*** session 2-1 ***
Hibernate: select model0_.id as id, model0_.version as version, 
               model0_.name as name, model0_.magazine as magazine 
           from Model model0_
onLoad() : Yuri Ebihara (0) : CanCam
onLoad() : Sayo Aizawa (0) : ViVi
onLoad() : Emi Suzuki (0) : Seventeen
Yuri Ebihara (0) : CanCam
Sayo Aizawa (0) : ViVi
Emi Suzuki (0) : Seventeen

*** session 1-2 ***
Hibernate: select id from Model where id =? and version =?
onUpdate() : Sayo Aizawa (0) : BOAO
onUpdate() : Emi Suzuki (0) : PINKY
Hibernate: update Model set version=?, name=?, magazine=? where id=? and version=?
Hibernate: update Model set version=?, name=?, magazine=? where id=? and version=?
Yuri Ebihara (0) : CanCam
Sayo Aizawa (1) : BOAO
Emi Suzuki (1) : PINKY

*** session 2-2 ***
Hibernate: select id from Model where id =? and version =?
onUpdate() : Sayo Aizawa (0) : WEB+DB PRESS
onUpdate() : Emi Suzuki (0) : Java World
Hibernate: update Model set version=?, name=?, magazine=? where id=? and version=?
- An operation failed due to stale data
net.sf.hibernate.StaleObjectStateException: 
        Row was updated or deleted by another transaction 
            (or unsaved-value mapping was incorrect) 
                for study.Model instance with identifier: 1
    at net.sf.hibernate.persister.AbstractEntityPersister.check(AbstractEntityPersister.java:506)
    at net.sf.hibernate.persister.EntityPersister.update(EntityPersister.java:687)
    at net.sf.hibernate.persister.EntityPersister.update(EntityPersister.java:642)
    at net.sf.hibernate.impl.ScheduledUpdate.execute(ScheduledUpdate.java:52)
    at net.sf.hibernate.impl.SessionImpl.executeAll(SessionImpl.java:2418)
    at net.sf.hibernate.impl.SessionImpl.execute(SessionImpl.java:2372)
    at net.sf.hibernate.impl.SessionImpl.flush(SessionImpl.java:2240)
    at net.sf.hibernate.transaction.JDBCTransaction.commit(JDBCTransaction.java:61)
    at study.HibernateTemplate.process(HibernateTemplate.java:40)
    at study.Main.main(Main.java:78)
- Could not synchronize database state with session
net.sf.hibernate.StaleObjectStateException: 
        Row was updated or deleted by another transaction 
            (or unsaved-value mapping was incorrect) 
                for study.Model instance with identifier: 1
    at net.sf.hibernate.persister.AbstractEntityPersister.check(AbstractEntityPersister.java:506)
    at net.sf.hibernate.persister.EntityPersister.update(EntityPersister.java:687)
    at net.sf.hibernate.persister.EntityPersister.update(EntityPersister.java:642)
    at net.sf.hibernate.impl.ScheduledUpdate.execute(ScheduledUpdate.java:52)
    at net.sf.hibernate.impl.SessionImpl.executeAll(SessionImpl.java:2418)
    at net.sf.hibernate.impl.SessionImpl.execute(SessionImpl.java:2372)
    at net.sf.hibernate.impl.SessionImpl.flush(SessionImpl.java:2240)
    at net.sf.hibernate.transaction.JDBCTransaction.commit(JDBCTransaction.java:61)
    at study.HibernateTemplate.process(HibernateTemplate.java:40)
    at study.Main.main(Main.java:78)
net.sf.hibernate.StaleObjectStateException: 
        Row was updated or deleted by another transaction 
            (or unsaved-value mapping was incorrect) 
                for study.Model instance with identifier: 1
    at net.sf.hibernate.persister.AbstractEntityPersister.check(AbstractEntityPersister.java:506)
    at net.sf.hibernate.persister.EntityPersister.update(EntityPersister.java:687)
    at net.sf.hibernate.persister.EntityPersister.update(EntityPersister.java:642)
    at net.sf.hibernate.impl.ScheduledUpdate.execute(ScheduledUpdate.java:52)
    at net.sf.hibernate.impl.SessionImpl.executeAll(SessionImpl.java:2418)
    at net.sf.hibernate.impl.SessionImpl.execute(SessionImpl.java:2372)
    at net.sf.hibernate.impl.SessionImpl.flush(SessionImpl.java:2240)
    at net.sf.hibernate.transaction.JDBCTransaction.commit(JDBCTransaction.java:61)
    at study.HibernateTemplate.process(HibernateTemplate.java:40)
    at study.Main.main(Main.java:78)

ぐはぁっ.
いやその,「ぐはぁっ」って書いてみたかっただけです.たぶん例外が吹っ飛んでくるだろうと予想していたので,全く問題ありません.
これを見ると,セッション 2-2 で永続オブジェクトの onLoad() まではちゃんと呼ばれています.つまり,Session#update() の時点では,バージョンのチェックは行われないのですね.そして,Transaction#commit() により,セッションがフラッシュされるところで UPDATE が発行されるのですが,すでにセッション 1-2 で DB の該当行が更新されているため,実際に更新できた行が 0 となり,この例外がスローされた,ということらしいです.


ということで,無事に楽観的なロックも学習できました.ちょっと駆け足だった気もするわけですが,disconnect/reconnectはそんなに使うとは思えないし,必要になれば別途試すということで十分かな,と.
これで折り返し点通過です.やったね♪
あれ? JTA がらみの話がなかったぞ? まいっか.いつか出てくるだろう.
次は「11. HQL: The Hibernate Query Language」へ進みます.んー,なんだか退屈そう.(^^; おまけに時間かかりそう...(;_;)