Re: hibernateを利用してはいけない5つのシチュエーション

「執拗につっこみ」と言われてもなんのその,関心を持ったことは日記に書くのだ (苦笑).
今日は

シチュエーション2 Torque等のCriteria系になれている場合

hibernateのCriteriaはおまけ程度です(公式ドキュメントにもそう書いてある)。
ちょっと複雑になると対処できません(Join一つとってもver2のころはaddJoinという直感的な物があったようですが、ver3のJoinは何?ま、関連を削除した時点で利用できませんが)。
Torqueでは非常に簡単にできたのに・・・と泣くことになりますので、hibernateの使用は避けましょう。

について.
この前も書いたように,自分は「Torque 等の Criteria 系になれている場合」に該当しないわけですが (っていうか Torque は遊んだことすらない),ちょっと気になる点がいくつか (いくつも?) あるのです.


まずは公式ドキュメントって?? リファレンスのこと??
どこに「おまけ程度」って書いてあるんだろう??
たしかに Hibernate 2.x では

Hibernate now features an intuitive, extensible criteria query API. For now, this API is less powerful and than the more mature HQL query facilities. In particular, criteria queries do not support projection or aggregation.

と,HQL より見劣りするということが書いてありますが,それのこと?
でもでも,元記事の著者さんって 3.x を使ってるみたいなんですが,そっちには

Hibernate features an intuitive, extensible criteria query API.

としか書いてないんですけどねぇ.
まぁいいや.


続いて「ver2のころはaddJoinという直感的な物があった」というところ.
そんなのあったっけ? 入門記で使った記憶はないような気のせいが...
とりあえず,2.1.x の JavaDoc では見あたりませんね.2.0.x にはあったのでしょうか?
ともあれ (JW),3.x の結合は 2.1.x とは大きく変わっていないようです.それなのに「ver3のJoinは何?」って何?
そんなわけで (どんなわけで?),Hibernate の Criteria を軽くまとめてみるテスト.
サンプルは例によって ModelMagazine で,多対 1 の関連があります♪


まずは普通に Model を取得してみましょう.

		Criteria criteria = session.createCriteria(Model.class);
		List<Model> l = criteria.list();
		for (Model model : l) {
			System.out.println(model);
		}

珍しく Tiger 使ってたりして (苦笑).
ともあれ (JW),ModeltoString()

	public String toString() {
		return firstName + " " + lastName + "(" + magazine.getName() + ")";
	}

となっていて,関連先の Magazine を参照します.
この実行結果.

Hibernate: select 
               this_.id as id0_, 
               this_.firstName as firstName1_0_, 
               this_.lastName as lastName1_0_, 
               this_.magazine as magazine1_0_ 
           from 
               Model this_
Hibernate: select 
               magazine0_.id as id0_, 
               magazine0_.name as name0_0_ 
           from 
               Magazine magazine0_ 
           where magazine0_.id=?
Yuri Ebihara(CanCam)
Naoko Tokuzawa(CanCam)
Maki Nishiyama(CanCam)
Hibernate: select 
               magazine0_.id as id0_, 
               magazine0_.name as name0_0_ 
           from 
               Magazine magazine0_ 
           where magazine0_.id=?
Yumi Sakurai(JJ)

Magazine は遅延ロードされていることが分かります.


そこで,Model を取得する際に Magazine も結合して取得したいとします.
いろいろなやり方があるのですが,手っ取り早いところではマッピングファイルで指定するとか.

<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC 
	"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
	"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"
>
<hibernate-mapping auto-import="false" package="hoge">
	<class name="Model">
		<id name="id" unsaved-value="-1">
			<generator class="assigned"/>
		</id>

		<property name="firstName"/>
		<property name="lastName"/>
		<many-to-one name="magazine" fetch="join"/>
	</class>
</hibernate-mapping>

<many-to-one> 要素の fetch 属性で "join" を指定します.
その場合の結果.

Hibernate: select 
               this_.id as id1_, 
               this_.firstName as firstName1_1_, 
               this_.lastName as lastName1_1_, 
               this_.magazine as magazine1_1_, 
               magazine2_.id as id0_, 
               magazine2_.name as name0_0_ 
           from 
               Model this_ 
                   left outer join Magazine magazine2_ on this_.magazine=magazine2_.id
Yuri Ebihara(CanCam)
Naoko Tokuzawa(CanCam)
Maki Nishiyama(CanCam)
Yumi Sakurai(JJ)

外部結合により,Model の取得時に Magazine が一気に取得されていることが分かります.
fetch 属性の指定は Criteria の他,Session#get()などで永続オブジェクトを取得する際や遅延初期化の際に有効になりますが,HQL では完全に無視されるので注意が必要です.


え? これじゃ Criteria でやってるって言わない?
そうかも (笑).
それに,マッピングファイルの指定とは違う方法で結合したい場合もあるでしょう.
そんな場合は,Criteria で明示的に結合させることもできます.
その場合もいくつかやり方がありますが,その一つはこんな感じ.

		Criteria criteria = session.createCriteria(Model.class);
		criteria.setFetchMode("magazine", FetchMode.JOIN);
		criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);

関連 magazine を結合することを明示的に指定しています.
ちなみに Hibernate 2.x では FetchMode.EAGER でしたが,3.x では FetchMode.JOIN がオススメっぽい.
結合させる場合は setResultTransformer()Criteria.DISTINCT_ROOT_ENTRY の指定をお約束にするのがオススメです.
今回の場合,多対 1 の多の側 (Model) に 1 の側 (Magazine) を結合しているので指定しなくても全然構わないのですけどね.逆の場合は常に指定した方がよいかと.
ともあれ (JW),その結果.

Hibernate: select 
               this_.id as id1_, 
               this_.firstName as firstName1_1_, 
               this_.lastName as lastName1_1_, 
               this_.magazine as magazine1_1_, 
               magazine2_.id as id0_, 
               magazine2_.name as name0_0_ 
           from 
               Model this_ 
                   left outer join Magazine magazine2_ on this_.magazine=magazine2_.id
Yuri Ebihara(CanCam)
Naoko Tokuzawa(CanCam)
Maki Nishiyama(CanCam)
Yumi Sakurai(JJ)

バッチリ.
ちなみに,マッピングファイルの fetch 属性は外してあります.


え? 単にフェッチするだけじゃなくて,問い合わせ条件で使いたい?
そんな場合もいくつかやり方がありますが,ここでは Criteria#createCriteria() を使います.基本となる Criteria から子の Criteria を作るのです.
「ver3のJoinは何?」ってこのことかな? でもでも,これって 2.x から変わっていないんですが.入門記でも使ったしね.
ともあれ (JW),CanCam 専属モデルだけを問い合せてみましょう.

		Criteria criteria = session.createCriteria(Model.class);
		Criteria magazineCriteria = criteria.createCriteria("magazine");
		magazineCriteria.add(Restrictions.eq("name", "CanCam"));

Hibernate 2.x では Expression#eq() とかでしたが,3.x では Restrictions#eq() なんかに変わっています.でも違いはその程度.
ともあれ (JW),その結果.

Hibernate: select 
               this_.id as id1_, 
               this_.firstName as firstName1_1_, 
               this_.lastName as lastName1_1_, 
               this_.magazine as magazine1_1_, 
               magazine1_.id as id0_, 
               magazine1_.name as name0_0_ 
           from 
               Model this_ 
                   inner join Magazine magazine1_ on this_.magazine=magazine1_.id 
           where 
               magazine1_.name=?
Yuri Ebihara(CanCam)
Naoko Tokuzawa(CanCam)
Maki Nishiyama(CanCam)

バッチリ.


え? 雑誌毎の専属モデルの人数を数えたい?
ふっふっふ.
ご安心ください.Hibernate 2.x ではサポートされていなかった Criteria の集合関数が 3.x ではサポートされてますよっ♪
某巨大掲示板の 271 さんも喜んでるに違いない.

		Criteria criteria = session.createCriteria(Model.class);
		criteria.setProjection(
			Projections.projectionList()
				.add(Projections.groupProperty("magazine.name"))
				.add(Projections.rowCount()));
		criteria.createAlias("magazine", "magazine");
		List<Object[]> l = criteria.list();
		for (Object[] row : l) {
			System.out.println(row[0] + " : " + row[1]);
		}

Projections#groupProperty() を指定するとそれで GROUP BY されるようです.
Projections#rowCount() が COUNT(*) みたいな.
その結果.

Hibernate: select 
               magazine1_.name as y0_, 
               count(*) as y1_ 
           from 
               Model this_ 
                   inner join Magazine magazine1_ on this_.magazine=magazine1_.id 
           group by 
               magazine1_.name
CanCam : 3
JJ : 1

いいねぇ♪ (昔懐かしいアコム風で)


え? 結果が Object[] なんて許せない?
ふっふっふ.
そんな場合は結果を Map にしてもらうこともできます.

		Criteria criteria = session.createCriteria(Model.class);
		criteria.setProjection(
			Projections.projectionList()
				.add(Projections.alias(Projections.groupProperty("magazine.name"), "magazine"))
				.add(Projections.alias(Projections.rowCount(), "count")));
		criteria.createAlias("magazine", "magazine");
		criteria.setResultTransformer(Criteria.ALIAS_TO_ENTITY_MAP);
		List<Map<String, Object>> l = criteria.list();
		for (Map<String, Object> map : l) {
			System.out.println(map.get("magazine") + " : " + map.get("count"));
		}

リファレンスでは Criteria#returnMaps() なんてメソッドがあるように書いてあったりしますが,これは 2.1.x ですでに @deprecated になっていて,3.x では無くなったメソッドです.
なので,Criteria.ALIAS_TO_ENTITY_MAP を使います.
エイリアスの指定がちょっと面倒かなぁ.
それから,GROUP BY が使えるなら HAVING も欲しくなると思うけど,サポートしてるのかなぁ?
ちょっと見あたらない... HQL にはあるのに.まぁいいや.
ともあれ (JW),結果.

Hibernate: select 
               magazine1_.name as y0_, 
               count(*) as y1_ 
           from 
               Model this_ 
                   inner join Magazine magazine1_ on this_.magazine=magazine1_.id 
           group by 
               magazine1_.name
CanCam : 3
JJ : 1

バッチリ.


え? 副問い合わせも使いたい?
ふっふっふ.
Hibernate 3.x では Criteria での副問い合わせもできるようになりましたよっ♪
そんなわけで,各雑誌で姓が一番小さいモデルを問い合せてみましょう.つまり,副 (相関) 問い合わせの中で集合関数を使います.

		DetachedCriteria subQuery = DetachedCriteria.forClass(Model.class, "model2");
		subQuery.setProjection(Property.forName("model2.lastName").min());
		subQuery.add(Restrictions.eqProperty("model.magazine", "model2.magazine"));

		Criteria criteria = session.createCriteria(Model.class, "model");
		criteria.createAlias("magazine", "magazine");
		criteria.add(Property.forName("lastName").eq(subQuery));

		List<Model> l = criteria.list();
		for (Model model : l) {
			System.out.println(model);
		}

最初に DetachedCriteria で副問い合わせを作って,それを親の問い合わせで使います.
その結果.

Hibernate: select 
               this_.id as id1_, 
               this_.firstName as firstName1_1_, 
               this_.lastName as lastName1_1_, 
               this_.magazine as magazine1_1_, 
               magazine1_.id as id0_, 
               magazine1_.name as name0_0_ 
           from 
               Model 
                   this_ inner join Magazine magazine1_ on this_.magazine=magazine1_.id 
           where 
               this_.lastName = (
                   select 
                       min(this0__.lastName) as y0_ 
                   from 
                       Model this0__ 
                   where 
                       this_.magazine=this0__.magazine
               )
Yuri Ebihara(CanCam)
Yumi Sakurai(JJ)

バッチリ!!


集合関数や副問い合わせなど,3.x になって Criteria もずいぶんと強化されてますね.
まぁ,自分は必要がなければ Criteria よりも HQL を使いたいということに変わりはありませんが.
だって,HQL の方が分かりやすいし.最後の例と同等の HQL は次の通り.

from 
    hoge.Model model 
        left outer join fetch model.magazine magazine 
where 
    model.lastName = (
        select 
            min(model2.lastName) 
        from 
            hoge.Model model2 
        where 
            model.magazine = model2.magazine
    )

おいらにはこっちの方が Criteria よりもずっと分かりやすいっす.
HQL は SQL の方言みたいなものだけど,結合の指定が SQL よりも楽なのがいいところ♪
これをマッピングファイルに書いておけばメンテもそんなに苦じゃないし,Fetch Join する/しないのようなチューニングもやりやすいし.
問い合わせ条件等を実行時に組み立てるなら Criteria の方がいいかもしれませんけどね.


ともあれ (JW),複雑なことは試していないのでアレですが,Hibernate の Criteria ってそんなに Torque の Criteria に見劣りするんでしょうか?
Torque の「Advanced Criteria Techniques」をみた感じだと,そんなに差があるようにも見えませんが...
っていうか,ぐぐってみた限りだと Torque の Criteria では外部結合ができないとか集合関数が使えないとか副問い合わせができないとか出てきました.どれも古い情報だったので (最近は誰も Torque について書いてない?),最新版ではこれらもサポートされているのかもしれませんが,もしそのままだったらむしろ Hibernate よりも見劣りする部分もあるのでは?


「直感的」でないという点については何とも言い難いのですが,一つだけ言わせてもらうと,「Torque ではこうだった」という考えに縛られていてはいけないのではないかと思います.
例えば Torque の JavaDocCriteria#addJoin() を見たところ,どうやらこの API は引数の文字列で任意の結合条件を指定できるようです.
一方,Hibernate の場合はマッピングファイルに関連が定義されているのが前提であるため,その関連を指定する形で結合を指示します.
これは Hibernate と Torque ではそもそそもの成り立ちが違うことが原因でしょう.
その違いを受け入れずに Torque 流のやり方を求めてもしょうがないのではないかと思います.
例えて言うなら,Java を使っているのに「COBOL ではこうだった...」とか「COBOL で簡単にできたことが...」なんてことを言ってる間は Java をマスターできないよね,みたいな.
あう,Prolog をやりながら,「変数を書き換えられたら楽なのにぃ」とついつい思ってしまうことがあるのは内緒だ.


最後に「関連を削除した時点で利用できませんが」ってことなんですが...
これは「シチュエーション1 表示層(ビュー)がHTMLでない場合」から繋がっているわけですが,「関連を削除した時点」で Hibernate を使うメリットはほとんど無くなっているというか,O/R Mapper をマッピングしないで使うってどうよ? みたいな.
まずはその使い方に疑問を感じてしかるべきではないのかなぁ.
まぁ,世の中にはいろいろな人がいるもんだと改めて思った次第.
そんなわけで (どんなわけで?),明日か明後日かともかく後日,シチュエーション 1 についても書いてみるかもしれません.


ところで,元記事の著者さん,忙しいのかなぁ?
反応がないというか更新が止まってますね...
シチュエーション 4 について有力な説が見つかりはしましたが本当にそうなのか,どんな条件でああいうことになっているのか,是非とも知りたいのですけどねぇ.