Hibernate 入門記 セッションその5 問い合せその他

今回は問い合せの残りをまとめて片づけようと思います.


まずは「9.3.4. Filtering collections」です.
...
なんだかよく分かりません.(;_;)
分かんないけどとにかく問い合せらしいです.問い合せの対象は,永続化されたコレクションらしいです.
コレクションに対して問い合せするということは,メモリ上で問い合せをしてくれるのでしょうか? うーみゅ,そういうわけではないみたい.
なんとなく,問い合せ文字列で FROM 句を指定する代わりにコレクションを渡せるというか,そのコレクションから FROM 句を作ってくれるというか,そんなものなのかな?
ともかくそのフィルタを使うには,Session

    • Collection filter(Object collection, String filter)
    • Collection filter(Object collection, String filter, Object value, Type type)
    • Collection filter(Object collection, String filter, Object[] values, Type[] types)

を使います.それぞれ,問い合せ文字列にパラメータがない場合,パラメータが一つの場合,パラメータが複数の場合に使います.名前付きのパラメータは使えないのね.
第一引数で渡すコレクションが問い合せの対象となります.コレクションは,永続化されているコレクションつまり,永続クラスのプロパティでなければならないようで,問い合わせ結果のリストなんかは渡せないようです (「Hibernate Users FAQ - Advanced Problems」より).
第二引数で渡す問い合せ文字列は,おおむね Session#find(String) などで使ってきたものと似ているのですが,FROM 句を記述することはできません.それは第一引数のコレクションから決まってしまうためみたいです.SELECT 句や WHERE 句は使うことができます.その中では,通常なら FROM 句に記述する永続クラスのエイリアスとして, this を使います.
リファレンスドキュメントに記載されている二つの例はこんな感じ.

where this.color = ?
select this.mate where this.color = eg.Color.BLACK

戻り値のコレクションは (論理的には) バッグらしいです.ということは実質リストかな.
これがどううれしいのか分かりにくいのですが,Session#filter()JavaDoc によると,巨大で遅延初期化されているコレクションを効率的にアクセスできるとか何とか.その巨大なコレクションは,フィルタを使っても初期化されないそうです.
うーん,なんのことやらさっぱり分かってませんから! 残念!!!!
次行きますよ! 次,次!!(久々エビちゃん風)


次に「9.3.5. Criteria queries」へ進みます.
これまで Hibernate の問い合せ言語 HQL を使う場合,文字列で組み立ててきました.しかし,そんなのイヤだ,OO な API で組み立てたい!! という人も世の中にはいるらしいです.っていうか結構いるのでしょうね.RogueWave とか,その手のライブラリを提供しているベンダーってずいぶん見かけた記憶が...
そんなわけで,HibernateAPI による問い合せの組み立てをサポートしています.そのために使うのが

  • Criteria

です.
こいつを入手するには,Session

    • Criteria createCriteria(Class persistentClass)

を使います.
Criteria を入手したら,問い合せ条件などを API 呼び出しで付加していくことができます.
それには,

    • Criteria add(Criterion criterion)

を使います.
Criterion というのは判定基準とかいう意味らしいですが,こいつは比較式 (演算子と基準値) を持つものです.
その Criterion を入手するには,

  • Expression

を使います.こいつの static なメソッドの一部を羅列すると...

    • SimpleExpression eq(String propertyName, Object value)
    • SimpleExpression like(String propertyName, Object value)
    • SimpleExpression gt(String propertyName, Object value)
    • SimpleExpression lt(String propertyName, Object value)
    • Criterion between(String propertyName, Object lo, Object hi)
    • Criterion in(String propertyName, Object[] values)
    • Criterion isNull(String propertyName)
    • Criterion and(Criterion lhs, Criterion rhs)
    • Criterion or(Criterion lhs, Criterion rhs)
    • Criterion not(Criterion expression)

という感じで,簡単に使えそうではあります.でも,複雑な問い合せになると大変そう...
おそらく,画面などからの入力により動的に問い合せ条件が変わる場合に,文字列で組み立てるより間違いにくくていいかなって感じなのでしょうね.
そうでなければ普通に文字列で書いた方が可読性はずっと優れていると思います.外部化もできるし.
ともあれ,Criteria に問い合せ条件を付け加えたら,そのメソッド

    • List list()

で問い合せを実行することができます.


最後に「9.3.6. Queries in native SQL」です.
文字通り,HQL ではなくて SQL を指定して問い合せをすることができるようです.それには,Session

    • Query createSQLQuery(String sql, String returnAlias, Class returnClass)
    • Query createSQLQuery(String sql, String[] returnAliases, Class[] returnClasses)

を使います.
第一引数で指定する SQL なんですが,エイリアスを使う場合には {} で囲んであげないといけないらしいです.HibernateSQL をパースするのでしょうか.
そんなわけで,リファレンスに出てくる例.

SELECT {cat.*} FROM CAT {cat} WHERE ROWNUM<10
SELECT {cat}.ID AS {cat.id}, {cat}.SEX AS {cat.sex}, {cat}.MATE AS {cat.mate}, ... 

一番名の例では,{cat.*} という記述をしています.この * が何を意味しているのかというと,どうも永続クラスのプロパティみたい.カラムじゃなくて,プロパティ.
どうもですね,createSQLQuery() の第三引数で指定するのは,永続クラスなんですね.ID だけを Integer で取ってくるとかではなくて,永続クラスを取ってくるみたいです.
となると,そのクラスのプロパティを全部並べないといけないわけで... 大変!!!!
そのための簡略記法が {cat.*} らしい? うーみゅ...
わざわざ SQL を指定したくなるというと,RDBMS 固有の機能を使う場合でしょうか.あとヒントなんかを指定したいとか.よほど追いつめられた場合にはこいつにすがることになるのかもしれませんが,ちょっと取っつきにくいなぁ.
なお,SQL 文字列の中には,名前なし (?) と名前付き (:名前) のパラメータを含めることができるようです.
SQL を使う場合,問い合せ文字列の外部化は直接サポートされていないようです.残念!!!!


といったあたりでまとめてお試しです.
今回は久しぶりに雑誌登場♪

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

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

雑誌とモデルは 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 + 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="identity"/>
        </id>
        <property name="name" access="field"/>
        <set name="model" access="field" cascade="all">
            <key column="magazine"/>
            <one-to-many class="Model"/>
        </set>
    </class>
</hibernate-mapping>

カスケードと外部結合を指定しています.
次にモデルの永続クラス.

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;
    int age;

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

    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="age" access="field"/>
    </class>
</hibernate-mapping>

こっちはどうということもなく.
二つのマッピングファイルを hibernate.cfg.xml に記述します.
最後に実行用のクラス.

package study;

import java.util.Iterator;

import net.sf.hibernate.Criteria;
import net.sf.hibernate.Query;
import net.sf.hibernate.Session;
import net.sf.hibernate.SessionFactory;
import net.sf.hibernate.cfg.Configuration;
import net.sf.hibernate.expression.Expression;

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

            Session session = factory.openSession();

            Magazine cancam = new Magazine("cancam");
            session.save(cancam);

            Model yuri = new Model("Yuri Ebihara", 24);
            cancam.model.add(yuri);
            Model yu = new Model("Yu Yamada", 20);
            cancam.model.add(yu);
            Model moe = new Model("Moe Oshikiri", 24);
            cancam.model.add(moe);
            Model asami = new Model("Asami Usuda", 19);
            cancam.model.add(asami);
            Model naoko = new Model("Naoko Tokuzawa", 19);
            cancam.model.add(naoko);

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

            session = factory.openSession();

            Magazine magazine = (Magazine) session.get(Magazine.class,
                    new Integer(0));

            System.out.println("*** filter ***");
            Iterator it = session.filter(magazine.model, "where this.age = 24")
                    .iterator();
            while (it.hasNext()) {
                System.out.println(it.next());
            }

            System.out.println("*** criteria ***");
            Criteria criteria = session.createCriteria(Model.class);
            criteria.add(Expression.like("name", "Yu%"));
            it = criteria.list().iterator();
            while (it.hasNext()) {
                System.out.println(it.next());
            }

            System.out.println("*** sql ***");
            Query sqlQuery = session
                    .createSQLQuery(
                            "select {model.*} from model as {model} where {model}.age < :age",
                            "model", Model.class);
            sqlQuery.setInteger("age", 20);
            it = sqlQuery.list().iterator();
            while (it.hasNext()) {
                System.out.println(it.next());
            }
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }
}

まず雑誌を Session#get() で取得して,それに関連づけられたモデルのコレクションに対してフィルタを適用しています.
それから Criteria を使った問い合わせと,SQL を使った問い合わせを行っています.
こいつを実行!!!!

onSave() : cancam
Hibernate: insert into Magazine (name, id) values (?, null)
Hibernate: call identity()
onSave() : Moe Oshikiri
Hibernate: insert into Model (name, age, id) values (?, ?, null)
Hibernate: call identity()
onSave() : Asami Usuda
Hibernate: insert into Model (name, age, id) values (?, ?, null)
Hibernate: call identity()
onSave() : Yu Yamada
Hibernate: insert into Model (name, age, id) values (?, ?, null)
Hibernate: call identity()
onSave() : Yuri Ebihara
Hibernate: insert into Model (name, age, id) values (?, ?, null)
Hibernate: call identity()
onSave() : Naoko Tokuzawa
Hibernate: insert into Model (name, age, id) values (?, ?, null)
Hibernate: call identity()
Hibernate: update Model set magazine=? where id=?
Hibernate: update Model set magazine=? where id=?
Hibernate: update Model set magazine=? where id=?
Hibernate: update Model set magazine=? where id=?
Hibernate: update Model set magazine=? where id=?
Hibernate: select magazine0_.id as id0_, magazine0_.name as name0_ 
           from Magazine magazine0_ 
           where magazine0_.id=?
onLoad() : cancam
Hibernate: select model0_.magazine as magazine__, model0_.id as id__, 
               model0_.id as id0_, model0_.name as name0_, model0_.age as age0_ 
           from Model model0_ 
           where model0_.magazine=?
onLoad() : Moe Oshikiri
onLoad() : Asami Usuda
onLoad() : Yu Yamada
onLoad() : Yuri Ebihara
onLoad() : Naoko Tokuzawa
*** filter ***
Hibernate: select this.id as id, this.name as name, this.age as age 
           from Model this 
           where this.magazine = ? and ((this.age=24 ))
Moe Oshikiri (24)
Yuri Ebihara (24)
*** criteria ***
Hibernate: select this.id as id0_, this.name as name0_, this.age as age0_ 
           from Model this 
           where this.name like ?
Yu Yamada (20)
Yuri Ebihara (24)
*** sql ***
Hibernate: select model.id as id0_, model.name as name0_, model.age as age0_ 
           from model as model 
           where model.age < ?
Asami Usuda (19)
Naoko Tokuzawa (19)

うーみゅ...
まずフィルタですが,なんか普通に問い合せをしていますね.SQL を見ると,ちゃんと FROM 句がモデルになっています.これは,Session#filter() に渡したのがモデルのコレクションだからでしょう.そして,そのモデルを所有している雑誌は CanCam なので,WEHER 句に magazine0_.id=? が付く... ということでいいのかなぁ?
ともあれ,関連づけられた永続オブジェクトに対する条件検索を手軽に実行する手段ということで理解しておこうと思います.かなり怪しいですが.
なお,遅延初期化をするように,雑誌のマッピングファイルを次のように変更して

        <set name="model" access="field" cascade="all" lazy="true">

再度実行すると,

Hibernate: select magazine0_.id as id0_, magazine0_.name as name0_ 
           from Magazine magazine0_ where magazine0_.id=?
onLoad() : cancam
*** filter ***
Hibernate: select this.id as id, this.name as name, this.age as age 
           from Model this 
           where this.magazine = ? and ((this.age=24 ))
onLoad() : Moe Oshikiri
onLoad() : Yuri Ebihara
Moe Oshikiri (24)
Yuri Ebihara (24)

と変わりました.
雑誌がインスタンス化された時点では,モデルはインスタンス化されていません (toString() などでコレクションにアクセスするとインスタンス化されてしまいます).それからフィルタ問い合わせを行うと,フィルタにかかったものだけがインスタンス化されます.
うーん,これが効率的なのかちょっと疑問ですが... 外部結合を指定すれば問い合せ一回で済むんだけどなぁ... その場合,関連づけられたモデルが全てインスタンス化されてしまうので,メモリ効率は劣るということでしょうか.
ということは,関連づけられたオブジェクトを全てインスタンス化したい場合は外部結合して,その一部しか必要でなければ遅延初期化 + フィルタを使うのが Hibernate 流か? おぉ? ちょっといいかもぉ.


次の Criteria.これはまぁ,こんなもんですかね.
最後の SQL ですが,渡した SQL をそのまま実行してくれるわけではないのですね.やはり Hibernate が永続クラスを扱うためにごそごそと直しています.まぁ,そのための {} によるエスケープですが.


ちょっとフィルタに関して理解不足ではありますが,これで「9.3. Querying」は終了です.
次は「9.4. Updating objects」です.これからも淡々と続きます〜.