Hibernate 入門記 セッションその1 save(),load(),get(),refresh()

今日から「Chapter 9. Manipulating Persistent Data」へと進みます.
ようやく,よーーーーーーっやく,Session の使い方を学習できます.いい加減セッションの後始末を学習しないと,何もしないのが身に付いてしまいます.やばいやばい.


ということでまずは「9.1. Creating a persistent object」です.
永続クラスのインスタンスは,「一時的」か「永続的」かのどちらかだそうです.その状態を司るのが

  • Session

です.
新規に作成されたインスタンスは,まずは一時的な状態となります.それを永続的な状態にするには,

    • Serializable save(Object object)

を使います.というか,これまでもさんざん使ってきましたね.
このメソッドを使った場合,永続化されたインスタンスの ID プロパティ (主キー) は,マッピングファイルで指定されたジェネレータにより割り当てられることになります.このメソッドの戻り値は,その ID プロパティの値です.
もし,ID プロパティ (主キー) が無意味な値ではなく,業務的に意味のある値を持つ場合は,インスタンスを永続化する際に明示的に指定することができます.そのために使うのが,

    • void save(Object object, Serializable id)

です.
インスタンスを永続化すると,関連づけられたインスタンスも永続化される場合があります.その場合,永続化される順番はコントロールできないようです.このため,関連を表す外部キーに NOT NULL 制約が付いていると悲しいことになってしまう可能性があるので気をつけろとのこと.らじゃ.


保存の次は読み込みということで,「9.2. Loading an object」です.
もし,取得したい永続オブジェクトの ID プロパティ (主キー) の値が分かっているのであれば,

    • Object load(Class theClass, Serializable id)

を使うことができます.
この場合,指定したクラスの新しいインスタンスが作成されます (キャッシュにない場合).
すでにあるインスタンスに永続化された状態を設定することもできます.それには,

    • Object load(Object object, Serializable id)

を使います.これは,BMP な Entity Bean や,独自のインスタンスプールを利用する場合に使うとのことです.自分には不要かな.
もし,指定したIDにマッチする行が DB に存在しなかった場合,Hibernate は「回復不能な例外」をスローするとのことです.
それで,ちょっとよく分からないのですが,もし Proxy を使っている場合 (<class> 要素の proxy 属性などで指定している場合でしょう),load() は「初期化されていない Proxy」を返すそうです.こいつは,決して DB をアクセスしないので,DB にアクセスすることなく関連を構築できて便利なんだとか.うーみゅ.(;_;)
これはつまり... 関連づけするには,関連先の永続オブジェクトが必要です.でもそれはまだなくて,作りたくもない.でも関連だけは作っておきたい (外部キーに値を設定したい).っていうこと???
うーみゅ,先の NOT NULL 制約と併せて考えると意味があるのでしょうか? ちょっと理解できない... 無念だ.
Proxy はまだ学習できていないので... またいつかということで.心より恥じる.


ともあれ,例外きらい! 変な Proxy なんか欲しくない! って場合には

    • Object get(Class clazz, Serializable id)

を使えとのこと.この場合,指定した ID にマッチする行が DB になければ,素直に null が返ってきます.
あう,return null 撲滅運動はどうしたんだろう?


読み込む際に,SELECT 〜 FOR UPDATE みたいにロックをかけたい場合もありますよね.そんな場合には,

    • Object get(Class clazz, Serializable id, LockMode lockMode)

を使うことができるとのこと.
LockMode には,次のものがあります.

NONE
ロックを要求しません.
もし該当のインスタンスがキャッシュになく,本当に DB から読み込む場合には, READ ロックが獲得されるようです.
デフォルトです.
READ
共有ロックを獲得します.
インスタンスがキャッシュに存在したとしても,DB から読み込むようです.
UPGRADE
昇格ロックを獲得します.
昇格ロック? な感じですが,このモードがいわゆる SELECT 〜 FOR UPDATE みたいです.
UPGRADE_NOWAIT
昇格ロックを獲得しますが,獲得できない場合に待機しません.
Oracle の SELECT 〜 FOR UPDATE NOWAIT です.
WRITE
オブジェクトが更新または挿入された場合に獲得されます.
よく分かりません...心より恥じる.
load()lock() では正しいモードではないとのことなので,無視してよさげ.

UPGRADE を指定した場合でも,一緒にロードされる関連づけられたオブジェクトは SELECT 〜 FOR UPDATE にならないとのことです.ちょっと注意です.


セッションの外部で DB が更新される場合もあります.トリガー使ったりとか SQL*PLUS でごぞごそやったりとか.
そんな場合に,永続オブジェクトを最新の状態にするのが

    • void refresh(Object object)

です.特にどうということもなさげ.


あまりたいしたことは学んでいませんが,一応このあたりでお試ししておきます.
load()get() といえば,以前悩んだ auter-join="true" が有効になったりもします.これもついでにやっておきましょう.
ということで,またしても雑誌とモデルを例にします.

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

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

今回,主キーとなる ID カラムには IDENTITY を指定していません.せっかく学習したので,明示的に ID を指定することにします.
それから,雑誌とモデルは1対多で.紗世ちゃん許して.
ということで,雑誌の永続クラス.

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

public class Magazine implements Lifecycle {
    int id = -1;
    String name;
    Set model;

    public Magazine() {
    }
    public Magazine(String name) {
        this.name = name;
        this.model = new HashSet();
    }
    public String toString() {
        return name + "(" + id + ") : " + model;
    }

    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="Magazine">
        <id name="id" access="field" unsaved-value="-1">
            <generator class="increment"/>
        </id>
        <property name="name" access="field"/>
        <set name="model" access="field" outer-join="true">
            <key column="MAGAZINE"/>
            <one-to-many class="Model"/>
        </set>
    </class>
</hibernate-mapping>

<set> 要素に outer-join 属性を指定しています.
続いてモデルの永続クラス.

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;

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

    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="increment"/>
        </id>
        <property name="name" access="field"/>
    </class>
</hibernate-mapping>

二つのマッピングファイルを,hibernate.cfg.xmlに記述します.
それから今回は,外部結合を有効にした HSQLDialect を用意して,それを使うように hibernate.cfg.xml に記述します.
そして実行用のクラス.

package study;
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");
            session.save(yuri, new Integer(0));

            Model asami = new Model("Asami Usuda");
            session.save(asami, new Integer(1));

            Magazine cancam = new Magazine("CanCam");
            cancam.model.add(yuri);
            cancam.model.add(asami);
            session.save(cancam, new Integer(0));

            session.flush();
            session.connection().commit();

            session = factory.openSession();

            yuri = (Model) session.get(Model.class, new Integer(0));
            System.out.println(yuri);

            asami = new Model();
            session.load(asami, new Integer(1));
            System.out.println(asami);
            //ここで止めて裏でDBを更新
            session.refresh(asami);
            System.out.println(asami);

            cancam = (Magazine) session.get(Magazine.class, new Integer(0));
            System.out.println(cancam);

            try {
                   session.load(Model.class, new Integer(2));
                   System.out.println("???");
            }
            catch (Throwable e) {
                System.out.println(e);
            }

            Model unknown = (Model) session.get(Model.class, new Integer(2));
            System.out.println(unknown);
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }
}

ちょっとだるーいコードですが,前半では明示的に ID プロパティの値を指定してモデルと雑誌を永続化しています.
その後,別のセッションでその ID プロパティを指定して永続オブジェクトを取得しています.あさ美ちゃんはインスタンスを渡してロードします.
さらに,コメントの位置にブレークポイントを付けて止めておいて,裏でレコードを更新します.その後リフレッシュして,更新された内容がロードされることを確認します.
それから,ID プロパティに存在しないはずの値を指定して,load()get() の違いを確認します.
本当はロックモードも確認したかったのですが,HSQLDB は SELECT 〜 FOR UPDATE をサポートしていないようなのでやめました.
ということで,実行!!!!!!

onSave() : Yuri Ebihara
onSave() : Asami Usuda
onSave() : CanCam
Hibernate: insert into Model (name, id) values (?, ?)
Hibernate: insert into Magazine (name, id) values (?, ?)
Hibernate: update Model set MAGAZINE=? where id=?
Hibernate: select model0_.id as id0_, model0_.name as name0_ 
           from Model model0_ where model0_.id=?
onLoad() : Yuri Ebihara
Yuri Ebihara(0)
Hibernate: select model0_.id as id0_, model0_.name as name0_ 
           from Model model0_ where model0_.id=?
onLoad() : Asami Usuda
Asami Usuda(1)
Hibernate: select model0_.id as id0_, model0_.name as name0_ 
           from Model model0_ where model0_.id=?
onLoad() : Yu Yamada
Yu Yamada(1)
Hibernate: select magazine0_.id as id1_, magazine0_.name as name1_, 
               model1_.id as id__, model1_.MAGAZINE as MAGAZINE__, 
               model1_.id as id0_, model1_.name as name0_ 
           from Magazine magazine0_ left outer join 
               Model model1_ on magazine0_.id=model1_.MAGAZINE 
           where magazine0_.id=?
onLoad() : CanCam
CanCam(0) : [Yu Yamada(1), Yuri Ebihara(0)]
Hibernate: select model0_.id as id0_, model0_.name as name0_ 
           from Model model0_ where model0_.id=?
net.sf.hibernate.ObjectNotFoundException: 
    No row with the given identifier exists: 2, of class: study.Model
null

ふむ.
指定したとおり,エビちゃんの ID は 0,あさ美ちゃんは 1,CanCam は 0 で INSERT されたようです.
そのロードもバッチリ.リフレッシュもOK.
注目は CanCam をロードするときの SELECT 文 (赤字).外部結合してくれてますね.今回の場合モデルはロード済みなのでメリットはありませんけど.それに今回の学習テーマじゃなかったりしますけど (苦笑).
それから,太字になっている最後の2行.load() は例外が飛んできていますが,get() では null が返ってきています.
ということで,ドキュメントに書いてあるとおりのようです.っていうか,ちゃんとドキュメントを読むことができたようです.ばっちり!!


でも,相変わらず後かたづけしていないのであった.心より恥じる.