Hibernate 入門記 セッションその6 更新

今回は「9.4. Updating objects」へ進みます.


まずは「9.4.1. Updating in the same Session」.
あるセッション中で取得した永続オブジェクトを更新する場合ですが,この場合,永続オブジェクトに加えられた変更は,Session#flush() を呼び出すだけで勝手に DB に反映されるらしいです.
明示的に更新用の API を呼び出すとかしなくてもいいのですね.ラクチン.
Session#load()Session#find() などを使って,とにかく永続オブジェクトを入手して状態を変更すれば,それだけでいいよ,と.ふむ.
このようなプログラミングモデルは,同一のセッションで SELECT と UPDATE を行わなければならないので,効率的ではない場合もあるとのこと.確かにねぇ.SQL 直なら UPDATE 一発で更新できる場合って結構ありますからね.そんな場合にまず永続オブジェクトを取得しなければ更新できないのはちょっと不便っぽい.
そんなわけで,Hibernate では別のアプローチも用意しているそうです.


ということで「9.4.2. Updating detached objects」へ進みます.別のアプローチとやらの説明です.
たいていのアプリケーションでは,最初のトランザクションでデータを取得して画面に表示し,次のトランザクションでデータを更新するというケースが多いでしょう.
ここで先ほどのアプローチだと,一番目のトランザクションで永続オブジェクトを問い合せて表示,二番目のトランザクションでもまた永続オブジェクトを問い合せて更新,ということになるわけです.しかし,それだと二番目の問い合せが無駄っぽい...
というわけで別のアプローチでは,一番目のトランザクションで取得した永続オブジェクトを (HTTPセッションなどに) 取っておいて,二番目のトランザクションではそれを使って更新をします.その時に使うのが,Session

    • void update(Object)

です.
わーお,Session#update(Object) ってそういうものだったのか.なんか,普通にプロパティ更新した後呼び出すのかと思ってましたよ.まぁ,その場合は何もしなくても Session#flush() で DB 更新されると今学習したばかりですが.
当然,二つのトランザクションの間に別のトランザクションが DB を更新していることもあり得ます.そんな場合のために,タイムスタンプやバージョンを使って制御をするということのようです.でもその方法は... まだ出てこないらしい.(;_;)
気をつけなくてはならないことに,引数で渡したオブジェクトと同じ ID を持つ永続オブジェクトがすでの現在のセッションに存在すると例外が吹っ飛んでくるそうです.シビアですな...
さらに,引数で渡したオブジェクトに関連づけられたオブジェクトは,必要に応じて個別に Session#update(Object) を呼び出さなければならないようです.
もう一つ,Session には

    • void saveOrUpdate(Object)

もあります.こいつは,引数で渡したオブジェクトがまだ永続化されていなければ追加を,すでに永続化済みなら更新を行ってくれるというものです.
永続化されているかどうかの判定は,ID プロパティに有効な値が設定されているかどうかで判断されます.ID プロパティが有効でないというのは,マッピングファイルの <id> 要素の unsaved-value 属性から判断されます.
その値は次の通り.

any
常にセーブされます.
none
常に更新されます.
null
ID プロパティの値が null の場合にセーブされます.null でなければ保存されます.
デフォルトです.
undefined
version または timestamp の場合のデフォルトらしいですが,それがどんなものかまだ学習していませんから!! 残念!!!!
明示的に指定された値
ID プロパティの値が null または指定された値の場合,セーブされます.

くどいようですが,Session#update(Object)Session#saveOrUpdate(Object) で永続化済みのオブジェクトを渡すのは,そのオブジェクトを別のセッションから取得した場合だけとのことです.現在のセッションで取得した (本当の) 永続オブジェクトは,Session#update(Object) などを呼び出さなくても Session#flush() により更新されます.


続いて「9.4.3. Reattaching detached objects」へ進みます.
今度もセッションまたがりのお話みたいです.
あるセッションで取得した永続オブジェクトを別のセッションで使いたい場合,更新するなら前述の Session#update(Object) などを使えばいいことは分かりましたが,更新するわけじゃない場合には? そこから関連を辿りたいだけとか.
そんな場合に使うのが,Session

    • void lock(Object object, LockMode lockMode)

です.
こいつでオブジェクトをセッションと関連づけます.うーみゅ,名前と実体があってないような?
ここで,第二引数の LockMode は次のものを指定することができます.

LockMode.NONE
セッションへの関連づけだけを行います.
LockMode.READ
バージョンのチェックを行った上で (つまり SELECT する),セッションへの関連づけを行います.
LockMode.UPGRADE
SELECT ... FOR UPDATE を行った上で,セッションへの関連づけを行います.

バージョンのチェックで撥ねられるとどうなるのだろう? 例外が吹っ飛んでくるのかな?
ともあれ,これは EJB 3.0 (Early Draft) のマージなんだと理解.たぶん.


ということでお試しコーナー.
今回はモデルのみで.

CREATE TABLE MODEL (
    ID INTEGER IDENTITY PRIMARY KEY,
    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;
    String name;
    String magazine;

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

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

モデルのマッピングファイル.

<?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">
    <class name="Model">
        <id name="id" access="field" unsaved-value="-1">
            <generator class="identity"/>
        </id>
        <property name="name" access="field"/>
        <property name="magazine" access="field"/>
    </class>
</hibernate-mapping>

こいつを hibernate.cfg.xml に記述します.
そして実行用のクラス.

package study;

import java.util.Iterator;

import net.sf.hibernate.Session;
import net.sf.hibernate.SessionFactory;
import net.sf.hibernate.cfg.Configuration;

public class Main {
    public static void main(String[] args) {
        try {
            Configuration config = new Configuration();
            SessionFactory factory = config.configure().buildSessionFactory();

            Session session = factory.openSession();
            Model yuri = new Model("Yuri Ebihara", "CanCam");
            session.save(yuri);
            Model yu = new Model("Yu Yamada", "CanCam");
            session.save(yu);
            session.flush();
            session.connection().commit();
            printModels(factory);

            System.out.println("*** update same session ***");
            session = factory.openSession();
            Model model = (Model) session.get(Model.class, new Integer(1));
            model.name = "Moe Oshikiri";
            session.flush();
            session.connection().commit();
            printModels(factory);

            System.out.println("*** update another session ***");
            session = factory.openSession();
            model.name = "Naoko Tokuzawa";
            session.update(model);
            session.flush();
            session.connection().commit();
            printModels(factory);

            System.out.println("*** saveOrUpdate another session ***");
            session = factory.openSession();
            model.name = "Asami Usuda";
            session.saveOrUpdate(model);
            Model sayo = new Model("Sayo Aizawa", "ViVi");
            session.saveOrUpdate(sayo);
            session.flush();
            session.connection().commit();
            printModels(factory);
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }
}

最初にモデルを二人作成します.
次のセッションでは,モデルを一人取得してそれを更新しています.
その次のセッションでは,前のセッションのインスタンスを使ってさらに更新するために,Session#update(Object) を呼び出しています.
さらに次のセッションでは,またもや前のセッションのインスタンスを使った更新とともに,新しいインスタンスも追加するために,Session#saveOrUpdate(Object) を呼び出しています.
それぞれのセッションの後に,モデルをプリントするためにこれまた別のセッションを作っています.
相変わらずセッションは閉じていません (苦笑).閉じれるようになるまであと少し!!
ともあれ,こいつを実行!!!!

onSave() : Yuri Ebihara
Hibernate: insert into Model (name, magazine, id) values (?, ?, null)
Hibernate: call identity()
onSave() : Yu Yamada
Hibernate: insert into Model (name, magazine, id) values (?, ?, null)
Hibernate: call identity()
Hibernate: select model0_.id as id, model0_.name as name, model0_.magazine as magazine 
           from Model model0_
onLoad() : Yuri Ebihara
onLoad() : Yu Yamada
Yuri Ebihara (CanCam)
Yu Yamada (CanCam)
*** update same session ***
Hibernate: select model0_.id as id0_, model0_.name as name0_, model0_.magazine as magazine0_ 
           from Model model0_ where model0_.id=?
onLoad() : Yu Yamada
Hibernate: update Model set name=?, magazine=? where id=?
Hibernate: select model0_.id as id, model0_.name as name, model0_.magazine as magazine 
           from Model model0_
onLoad() : Yuri Ebihara
onLoad() : Moe Oshikiri
Yuri Ebihara (CanCam)
Moe Oshikiri (CanCam)
*** update another session ***
onUpdate() : Naoko Tokuzawa
Hibernate: update Model set name=?, magazine=? where id=?
Hibernate: select model0_.id as id, model0_.name as name, model0_.magazine as magazine 
           from Model model0_
onLoad() : Yuri Ebihara
onLoad() : Naoko Tokuzawa
Yuri Ebihara (CanCam)
Naoko Tokuzawa (CanCam)
*** saveOrUpdate another session ***
onUpdate() : Asami Usuda
onSave() : Sayo Aizawa
Hibernate: insert into Model (name, magazine, id) values (?, ?, null)
Hibernate: call identity()
Hibernate: update Model set name=?, magazine=? where id=?
Hibernate: select model0_.id as id, model0_.name as name, model0_.magazine as magazine 
           from Model model0_
onLoad() : Yuri Ebihara
onLoad() : Asami Usuda
onLoad() : Sayo Aizawa
Yuri Ebihara (CanCam)
Asami Usuda (CanCam)
Sayo Aizawa (ViVi)

どうだ? うまくできたっぽい.たぶん.
異なるセッションの更新では,SELECT なしでいきなり UPDATE が呼ばれてますね.
おぉ,onUpdate() が呼ばれてる!! もしかして初めてみたような??
なぁんだ,これならアッコちゃんネタをやってもよかったかなぁ.(^^; っていうか id:fumi0611 さんは元気かなぁ?
Session#saveOrUpdate(Object) では,前のセッションのインスタンスを使った場合は UPDATE が,新しいインスタンスを使った場合には INSERT が行われています.順番は逆になってますが.(^^;


ということで更新の学習終了です.ふー,間に合ったな.
アテネみるぜ!


01:55 追記
ぐはぁっ,Session#lock() のお試しを忘れていた...(ToT)
いつか機会があればやります...