Home日記コラム書評HintsLinks自己紹介
 

Hibernate (2)

※注意: このドキュメントは執筆途中のため、 意味の通じない箇所等が至るところにありますが、 あらかじめご了承ください。

Site情報の保存

背景

私はかなり前から「らんだむCGりんく」というサイトというか、 CGI 系のサービスを公開していたのだが、 現在、サーバの都合で休眠状態である。 このサイトは、最初はよく覚えていないのだが、 途中で Servlet を使うように修正し、 現在に至っている。 Servlet は単純にデータをオブジェクトの形式で保存しているのだが、 これを Hibernate を使って DB に保存するように修正しようと企んだ訳だ。

サイトに関連する情報は多い。 サイトの名称(タイトル)、URI。 この2つは基本となるのだが、 重要な付加情報として、 存在を確認した日付や、 現在の状態がある。 現在の状態というのは、サイトによっては更新されていなかったり、 休眠状態、閉鎖を宣言したもの、などのバリエーションがあるし、 さらに、移転情報というのも重要だ。 CG系のサイトならこれに加えて、 画像のカテゴリーも情報として必要になる。

これらを属性として個々に持つのは大変だ。 例えば次のようなクラスを想像してみると、一見簡単なのだが。

-----------------------
     class SiteInfo
-----------------------
- id : int
- siteid : int
- uri : String
- title : String
- pageDate : Date
- checkDate : Date
- status : Status
- closed : boolean
- lost : boolean
-----------------------
-----------------------

問題は、 このような属性は後でいろいろ追加したくなる、 というところにある。 例えば18禁サイト、おすすめサイトなどの情報を付加したいとか、 コメントを書いておきたいとか、 アクセスが非常に多い有名サイトであるとか、 JavaScript が off だと見ることができないとか、 いろいろ情報が必要なことが後で分かるのだ。 さすがにこの「らんだむCGりんく」は何年かやっているから、 どのような情報が必要なのかだいたい分かっているのだが、 それでも、固定した属性だけを扱うというのは、 どうも汎用性に欠けるわけで、その方が気になる。

また、このような情報をメンバー変数で持つようにすれば、 属性が増えるにつれて、 1つのサイトあたりのデータが大きくなる。 全ての属性が全サイトにあるのなら全く問題ないのだが、 逆に、殆どの情報は特定のサイトにしかない、 というような場合には、殆どスカスカのデータを大量に持つことになり、 資源の無駄になってしまう。

データ構造の抽象化

例えば、 この属性 というものを、 Map を使って汎用的に、 数値-文字列、のペアを使って保持することを考えてみる。

-----------------------
     SiteInfo
-----------------------
- id : int
- siteid : int
- uri : String
- values : Map<int, Object>
-----------------------
-----------------------

id はこのデータに対するユニーク(一意的)な番号だから必要であり、 siteid は、サイトごとにユニークなidということで、 これも絶対に必要な情報である。 uri がないとサイトというのは考えられないから、 これも絶対にあると考えてよい。 その他の情報は全てオプション扱いにしてしまう、という発想なのだ。

しかし、データモデルとしては、 単に Map だけある、というのが最もシンプルだろう。 ただし、性能を考えた場合に、 必須項目を Map に書いておくというのはかなり損な気がする。 つまり、頻繁にキーにする項目は、 Map の外に出しておいた方がいいような気がするのだ。

ただ、実際にどの程度違うか測定したわけではない。 このあたりは、プログラマーの勘というやつだが、 結構外れることもある。

先程のモデルだと、 実際に属性を取り出すときのコードのイメージはこんな感じになるだろう。

    String title = (String) siteInfo.getValues.get(SiteInfo.ID_TITLE)

Note

整数をキーにするというのはプログラマーの性というか、 本当はキーも String にしていい位なのだが、 効率とか性能をちょっとだけ気にしてそうしたのだ。 あえてメリットを言うなら、 完全に String -String のペアでマップにした場合は、 忘れたころに別の意味で同じ文字列をキーにしてしまう危険があるかもしれない。 もちろん、 それを言うなら、 int - String のペアでマップしても、 既にキーが割り当てられているのに二重に割り当ててしまう、 というリスクはあるのだが。

考えてみると、 このようなモデルは一見いいような気もするが、 やはりどうも中途半端だ。 もっと徹底すれば、どうなるだろうか? まず、ある key に対して、属性を対応させた KeyValuePair というクラスを考える。

-----------------------
    KeyValuePair
    {abstract}
-----------------------
- key : integer
- value : Object
-----------------------
-----------------------

属性値を Object にしたのは、 何を入れていいのかこの段階でまだ決まっていないことを意味する。 KeyValuePair というクラスは私が発明したわけではない。 一般に、KeyValuePair という名前のクラスは、 key として String か int、 value として String を取るものが多い。 これは、実際に XML などを使ってキーと属性のペアを表現する場合に、 文字列としてそれぞれが出現するからであろう。

これを実装するには、 value のクラスを明確にする必要がある。

-----------------------
    KeyValuePair
    {abstract}
-----------------------
◇
|
-----------------------
    KVString
-----------------------
- value : String
-----------------------
-----------------------

Hibernate による永続化

Hibernate でこれを永続化してみよう。 このクラスを想定したマッピングファイルは、 例えば、次のようになる。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-mapping SYSTEM
          "file:///d:/java/dtd/hibernate-mapping-3.0.dtd">
          
<hibernate-mapping>
    <class name="com.phinloda.orm.KeyValuePair"
        table="kvbase">
        <id name="id" type="int" column="id">
            <generator class="increment"/>
        </id>
        
        <property name="parent" type="int"/>
        <property name="key" column="kvkey" type="int"></property>

    </class>
    
	<joined-subclass name="com.phinloda.orm.KVInteger"
        table="kvi"
        extends="com.phinloda.orm.KeyValuePair"> 
        <key column="int_id"/>
        <property name="value" type="int" column="value"/>     
    </joined-subclass>
    
	<joined-subclass name="com.phinloda.orm.KVString"
        table="kvs"
        extends="com.phinloda.orm.KeyValuePair"> 
        <key column="s_id"/>
        <property name="value" type="string" column="value"/>     
    </joined-subclass>
    
	<joined-subclass name="com.phinloda.orm.KVList"
        table="kvlist"
        extends="com.phinloda.orm.KeyValuePair"> 
        <key column="li_id"/>
        <list name="value" lazy="true" cascade="all">
        	<key column="parent"/>
        	<list-index column="lpos"/>
        	<one-to-many class="com.phinloda.orm.KeyValuePair"/>
        </list>
    </joined-subclass>
    
</hibernate-mapping>

Note

※間違っているかもしれません。 特に cascade が all というのは怪しい。 くれぐれも、鵜呑みにしないでください。

これに対して ant codegen を実行すると、 次の4つのクラスができる。

package com.phinloda.orm;

public class KeyValuePair implements java.io.Serializable {

    // Fields

    private int id;

    private int parent;

    private int key;

    // Constructors

    /** default constructor */
    public KeyValuePair() {
    }

    /** full constructor */
    public KeyValuePair(int parent, int key) {
        this.parent = parent;
        this.key = key;
    }

    // Property accessors

    public int getId() {
        return this.id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public int getParent() {
        return this.parent;
    }

    public void setParent(int parent) {
        this.parent = parent;
    }

    public int getKey() {
        return this.key;
    }

    public void setKey(int key) {
        this.key = key;
    }

}
package com.phinloda.orm;

public class KVInteger extends com.phinloda.orm.KeyValuePair implements
        java.io.Serializable {

    // Fields

    private int value;

    // Constructors

    /** default constructor */
    public KVInteger() {
    }

    /** full constructor */
    public KVInteger(int parent, int key, Integer value) {
        super(parent, key);
        this.value = value;
    }

    // Property accessors

    public int getValue() {
        return this.value;
    }

    public void setValue(int value) {
        this.value = value;
    }

}
package com.phinloda.orm;

import java.util.ArrayList;
import java.util.List;

public class KVList extends com.phinloda.orm.KeyValuePair implements
        java.io.Serializable {

    // Fields

    private List value = new ArrayList(0);

    // Constructors

    /** default constructor */
    public KVList() {
    }

    /** full constructor */
    public KVList(int parent, int key, List value) {
        super(parent, key);
        this.value = value;
    }

    // Property accessors

    public List getValue() {
        return this.value;
    }

    public void setValue(List value) {
        this.value = value;
    }

}
package com.phinloda.orm;

public class KVString extends com.phinloda.orm.KeyValuePair implements
        java.io.Serializable {

    // Fields

    private String value;

    // Constructors

    /** default constructor */
    public KVString() {
    }

    /** full constructor */
    public KVString(int parent, int key, String value) {
        super(parent, key);
        this.value = value;
    }

    // Property accessors

    public String getValue() {
        return this.value;
    }

    public void setValue(String value) {
        this.value = value;
    }

}

生成されたソースはそのままコンパイルすることができるが、 マッピングの書き方がよくないのか、 期待したものとは少し違うものが生成されている。 つまり、getValue() と setValue() という2つのメソッドが、 KeyValuePair という規定クラスを継承するような作りになっていないのだ。

そこで、強引だが、次のように修正する。 まず、KeyValuePair を abstract に変更する。

package com.phinloda.orm;

/**
 * KeyValuePair
 * base class.
 */
abstract public class KeyValuePair implements java.io.Serializable {

    // Fields

    private int id;

    private int parent;

    private int key;

    // Constructors

    /** default constructor */
    public KeyValuePair() {
    }

    /** full constructor */
    public KeyValuePair(int parent, int key) {
        this.parent = parent;
        this.key = key;
    }

    // Property accessors

    public int getId() {
        return this.id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public int getParent() {
        return this.parent;
    }

    public void setParent(int parent) {
        this.parent = parent;
    }

    public int getKey() {
        return this.key;
    }

    public void setKey(int key) {
        this.key = key;
    }
    
    
    abstract public Object getValue();
    
    abstract public void setValue(Object value);

}

サブクラス側は、 getter/setter を、 Object で操作するように変更する。 全て紹介しても冗長なので、 KVString のみ紹介する。

package com.phinloda.orm;

public class KVString extends com.phinloda.orm.KeyValuePair implements
        java.io.Serializable {

    private static final long serialVersionUID = 1L;

    // Fields

    private String value;

    // Constructors

    /** default constructor */
    public KVString() {
    }

    /** full constructor */
    public KVString(int parent, int key, String value) {
        super(parent, key);
        this.value = value;
    }

    // Property accessors

    public Object getValue() {
        return this.value;
    }

    public void setValue(Object value) {
        this.value = (String) value;
    }

}

Note

※これはかなり強引なやり方である。 もしかすると、よりエレガントな方法があるのかもしれない。

次に、 schema を実行し、 テーブルを生成する。

Buildfile: D:\java\eclipse\workspace\NetUtil\src\com\phinloda\orm\build.xml
prepare:
     [echo] target prepare entered.
schema:
[hibernatetool] Executing Hibernate Tool with a Standard Configuration
[hibernatetool] 1. task: hbm2ddl (Generates database schema)
[hibernatetool] 2006/02/16 4:00:35 org.hibernate.cfg.Environment <clinit>
[hibernatetool] 情報: Hibernate 3.1 rc3
[hibernatetool] 2006/02/16 4:00:35 org.hibernate.cfg.Environment <clinit>
[hibernatetool] 情報: hibernate.properties not found
[hibernatetool] 2006/02/16 4:00:35 org.hibernate.cfg.Environment <clinit>
[hibernatetool] 情報: using CGLIB reflection optimizer
[hibernatetool] 2006/02/16 4:00:35 org.hibernate.cfg.Environment <clinit>
[hibernatetool] 情報: using JDK 1.4 java.sql.Timestamp handling
[hibernatetool] 2006/02/16 4:00:35 org.hibernate.cfg.Configuration configure
[hibernatetool] 情報: configuring from file: hibernate.cfg.xml
[hibernatetool] 2006/02/16 4:00:36 org.hibernate.cfg.Configuration addResource
[hibernatetool] 情報: Reading mappings from resource: com/phinloda/orm/KeyValuePair.hbm.xml
[hibernatetool] 2006/02/16 4:00:36 org.hibernate.cfg.HbmBinder bindRootPersistentClassCommonValues
[hibernatetool] 情報: Mapping class: siteinfo -> kvbase
[hibernatetool] 2006/02/16 4:00:36 org.hibernate.cfg.HbmBinder bindJoinedSubclass
[hibernatetool] 情報: Mapping joined-subclass: com.phinloda.orm.KVInteger -> kvi
[hibernatetool] 2006/02/16 4:00:36 org.hibernate.cfg.HbmBinder bindJoinedSubclass
[hibernatetool] 情報: Mapping joined-subclass: com.phinloda.orm.KVString -> kvs
[hibernatetool] 2006/02/16 4:00:36 org.hibernate.cfg.HbmBinder bindJoinedSubclass
[hibernatetool] 情報: Mapping joined-subclass: com.phinloda.orm.KVList -> kvlist
[hibernatetool] 2006/02/16 4:00:36 org.hibernate.cfg.Configuration doConfigure
[hibernatetool] 情報: Configured SessionFactory: null
[hibernatetool] 2006/02/16 4:00:36 org.hibernate.cfg.Configuration secondPassCompile
[hibernatetool] 情報: processing extends queue
[hibernatetool] 2006/02/16 4:00:36 org.hibernate.cfg.Configuration secondPassCompile
[hibernatetool] 情報: processing collection mappings
[hibernatetool] 2006/02/16 4:00:36 org.hibernate.cfg.HbmBinder bindCollectionSecondPass
[hibernatetool] 情報: Mapping collection: com.phinloda.orm.KVList.value -> kvbase
[hibernatetool] 2006/02/16 4:00:36 org.hibernate.cfg.Configuration secondPassCompile
[hibernatetool] 情報: processing association property references
[hibernatetool] 2006/02/16 4:00:36 org.hibernate.cfg.Configuration secondPassCompile
[hibernatetool] 情報: processing foreign key constraints
[hibernatetool] 2006/02/16 4:00:36 org.hibernate.dialect.Dialect <init>
[hibernatetool] 情報: Using dialect: org.hibernate.dialect.MySQLDialect
[hibernatetool] 2006/02/16 4:00:36 org.hibernate.cfg.Configuration secondPassCompile
[hibernatetool] 情報: processing extends queue
[hibernatetool] 2006/02/16 4:00:36 org.hibernate.cfg.Configuration secondPassCompile
[hibernatetool] 情報: processing collection mappings
[hibernatetool] 2006/02/16 4:00:36 org.hibernate.cfg.Configuration secondPassCompile
[hibernatetool] 情報: processing association property references
[hibernatetool] 2006/02/16 4:00:36 org.hibernate.cfg.Configuration secondPassCompile
[hibernatetool] 情報: processing foreign key constraints
[hibernatetool] 2006/02/16 4:00:36 org.hibernate.cfg.Configuration secondPassCompile
[hibernatetool] 情報: processing extends queue
[hibernatetool] 2006/02/16 4:00:36 org.hibernate.cfg.Configuration secondPassCompile
[hibernatetool] 情報: processing collection mappings
[hibernatetool] 2006/02/16 4:00:36 org.hibernate.cfg.Configuration secondPassCompile
[hibernatetool] 情報: processing association property references
[hibernatetool] 2006/02/16 4:00:36 org.hibernate.cfg.Configuration secondPassCompile
[hibernatetool] 情報: processing foreign key constraints
[hibernatetool] 2006/02/16 4:00:36 org.hibernate.tool.hbm2ddl.SchemaExport execute
[hibernatetool] 情報: Running hbm2ddl schema export
[hibernatetool] 2006/02/16 4:00:36 org.hibernate.tool.hbm2ddl.SchemaExport execute
[hibernatetool] 情報: exporting generated schema to database
[hibernatetool] 2006/02/16 4:00:36 org.hibernate.connection.DriverManagerConnectionProvider configure
[hibernatetool] 情報: Using Hibernate built-in connection pool (not for production use!)
[hibernatetool] 2006/02/16 4:00:36 org.hibernate.connection.DriverManagerConnectionProvider configure
[hibernatetool] 情報: Hibernate connection pool size: 20
[hibernatetool] 2006/02/16 4:00:36 org.hibernate.connection.DriverManagerConnectionProvider configure
[hibernatetool] 情報: autocommit mode: false
[hibernatetool] 2006/02/16 4:00:36 org.hibernate.connection.DriverManagerConnectionProvider configure
[hibernatetool] 情報: using driver: com.mysql.jdbc.Driver at URL: jdbc:mysql://localhost/practice?useUnicode=true&characterEncoding=utf8
[hibernatetool] 2006/02/16 4:00:36 org.hibernate.connection.DriverManagerConnectionProvider configure
[hibernatetool] 情報: connection properties: {user=crm, password=****}
[hibernatetool] alter table kvbase drop foreign key FKBD4366BC7FEF979B;
[hibernatetool] alter table kvi drop foreign key FK1A05E28485590;
[hibernatetool] alter table kvlist drop foreign key FKBD48108928485590;
[hibernatetool] alter table kvs drop foreign key FK1A06828485590;
[hibernatetool] drop table if exists kvbase;
[hibernatetool] drop table if exists kvi;
[hibernatetool] drop table if exists kvlist;
[hibernatetool] drop table if exists kvs;
[hibernatetool] create table kvbase (id integer not null, parent integer, kvkey integer, lpos integer, primary key (id));
[hibernatetool] create table kvi (id integer not null, value integer, primary key (id));
[hibernatetool] create table kvlist (id integer not null, primary key (id));
[hibernatetool] create table kvs (id integer not null, value varchar(255), primary key (id));
[hibernatetool] alter table kvbase add index FKBD4366BC7FEF979B (parent), add constraint FKBD4366BC7FEF979B foreign key (parent) references kvlist (id);
[hibernatetool] alter table kvi add index FK1A05E28485590 (id), add constraint FK1A05E28485590 foreign key (id) references kvbase (id);
[hibernatetool] alter table kvlist add index FKBD48108928485590 (id), add constraint FKBD48108928485590 foreign key (id) references kvbase (id);
[hibernatetool] alter table kvs add index FK1A06828485590 (id), add constraint FK1A06828485590 foreign key (id) references kvbase (id);
[hibernatetool] 2006/02/16 4:00:37 org.hibernate.tool.hbm2ddl.SchemaExport execute
[hibernatetool] 情報: schema export complete
[hibernatetool] 2006/02/16 4:00:37 org.hibernate.connection.DriverManagerConnectionProvider close
[hibernatetool] 情報: cleaning up connection pool: jdbc:mysql://localhost/practice?useUnicode=true&characterEncoding=utf8
[hibernatetool] 4 errors occurred while performing <hbm2ddl>.
[hibernatetool] Error #1: java.sql.SQLException: Table 'practice.kvbase' doesn't exist
[hibernatetool] Error #1: java.sql.SQLException: Table 'practice.kvi' doesn't exist
[hibernatetool] Error #1: java.sql.SQLException: Table 'practice.kvlist' doesn't exist
[hibernatetool] Error #1: java.sql.SQLException: Table 'practice.kvs' doesn't exist
BUILD SUCCESSFUL
Total time: 3 seconds

最後に Error が出ているが、これは最初は drop すべきテーブルがないためで、 必ず出るものだから無視していい。 二度目からは出なくなる。

余談: この種のツールを使うときに、 クラス内の変数名に DB の予約語を使わないように気をつけること。 今回は key という名前の property が出てくるが、 わざわざテーブルの名称を kvkey と明示的に指定している。 Hibernate はテーブルの名称を特に指定しなくても、 自動的にプロパティと同じ名称にして作成してくれるのだが、 key という名前のテーブルを作ろうとすると、 例えば、次のようなエラーが出る。

    ~(略)~
[schemaexport] 05:00:54,642 ERROR SchemaExport:154
 - Unsuccessful: create table KeyValuePair
  (id bigint not null, key integer, value varchar(255), siteid bigint, primary key (id))
[schemaexport] 05:00:54,642 ERROR SchemaExport:155
 - You have an error in your SQL syntax.
   Check the manual that corresponds to your MySQL server version
    for the right syntax to use near 'integer, value varchar(255), siteid bigint, primary key (id))'

これは、key という文字列が予約語なので、 key という名前のカラムが作れないと言っているのだ。

さて、これに対して何かしたいのだが、 まだアクセスするためのクラスもメソッドも何もない。 とりあえず、 add と get だけを定義した interface を用意しておく。

package com.phinloda.orm;

public interface KeyValuePairFacade {
    
    int add(KeyValuePair kvp);
    
    KeyValuePair get(int id);

}

そして、これに対して Hibernate を使って実装する。 Eclipse でこの interface を implement したクラスを生成するのだが、 今回は Spring Framework を使うことにするので、 HibernateDaoSupport を extends するように指定する。 このような雛形ができる。

package com.phinloda.orm;

import org.springframework.orm.hibernate3.support.HibernateDaoSupport;

public class KeyValuePairHibernateImpl extends HibernateDaoSupport implements
        KeyValuePairFacade {

    public int add(KeyValuePair kvp) {
        // TODO Auto-generated method stub
        return 0;
    }

    public KeyValuePair get(int id) {
        // TODO Auto-generated method stub
        return null;
    }

}

試しに実装すると、こんな感じになる。

package com.phinloda.orm;

import java.util.List;

import org.springframework.orm.hibernate3.HibernateTemplate;
import org.springframework.orm.hibernate3.support.HibernateDaoSupport;

import com.phinloda.util.Logger;

public class KeyValuePairHibernateImpl extends HibernateDaoSupport implements
        KeyValuePairFacade {

    public int add(KeyValuePair kvp) {
        HibernateTemplate ht = getHibernateTemplate();
        ht.save(kvp);
        return kvp.getId();
    }

    public KeyValuePair get(int id) {
        return (KeyValuePair) getHibernateTemplate().get("siteinfo", Integer.valueOf(id));
    }

}

これには穴があるので後で埋めなければならないのだが、 とりあえず前に進むことにすると、 テストケースはこんな感じになる。 このテストケースも大きな穴があるのだが、 後で直す。

package com.phinloda.orm;

import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import junit.framework.TestCase;

public class KeyValuePairTest extends TestCase {

    public static void main(String[] args) {
        junit.textui.TestRunner.run(KeyValuePairTest.class);
    }

    protected void setUp() throws Exception {
        super.setUp();
        
        BeanFactory factory = new ClassPathXmlApplicationContext("/applicationContext.xml");
        setFacade((KeyValuePairFacade) factory.getBean("myFacade"));
    }

    protected void tearDown() throws Exception {
        super.tearDown();
    }

    private KeyValuePairFacade facade;

    public KeyValuePairFacade getFacade() {
        return facade;
    }

    public void setFacade(KeyValuePairFacade facade) {
        this.facade = facade;
    }
    
    public final void test1() {
        KeyValuePair kvp = new KVString();
        String testString = "hoge";
        kvp.setValue(testString);
        
        int id = getFacade().add(kvp);
        
        KeyValuePair compare = getFacade().get(id);
        assertEquals(testString, compare.getValue());
    }

}

これを実行する前に、Spring を使うための設定が必要になる。 申し訳ないが、このページは Hibernate の話を書くところなので、 Spring に関してはあまり説明しない予定である。 applicationContext.xml に、次のような記述を行う。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">

<beans>

  <bean id="MySqlDataSource"
    class="org.apache.commons.dbcp.BasicDataSource">
    <property name="driverClassName">
      <value>com.mysql.jdbc.Driver</value>
    </property>
    <property name="url">
      <value>jdbc:mysql://localhost/practice?useUnicode=true&amp;characterEncoding=utf8</value>
    </property>
    <property name="username"><value>********</value></property>
    <property name="password"><value>********</value></property>
  </bean>

  <bean id="sessionFactory"
    class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
    <property name="dataSource"><ref local="MySqlDataSource" /></property>

    <property name="mappingResources">
      <list>
        <value>com/phinloda/orm/KeyValuePair.hbm.xml</value>
      </list>
    </property>
    <property name="hibernateProperties">
      <props>
        <prop key="hibernate.dialect">org.hibernate.dialect.MySQLDialect</prop>
        <prop key="hibernate.show_sql">true</prop>
        <prop key="hibernate.connecton.charSet">UTF-8</prop>
      </props>
    </property>
  </bean>

  <bean name="myFacade" id="myFacade" 
    class="com.phinloda.orm.KeyValuePairFacadeHibernateImpl">
    <property name="sessionFactory"><ref local="sessionFactory"/></property>
  </bean>


</beans>

これで実際にテストケースを実行したら、 JUnit はグリーンバーを表示したので、 値を設定して、引き出すことができた、ということがわかる。 もっとも、このテストケースだと、 本当に永続化されているかどうかはよく分からないはずだ。 Hibernate がどこかメモリ上にキャッシュしているのかもしれないからだ。 本当にデータベースに書き込んだのかどうか、 mysql を直接操作して確かめてみよう。

mysql> select * from kvbase;
+----+--------+-------+------+
| id | parent | kvkey | lpos |
+----+--------+-------+------+
|  1 |      0 |     0 | NULL |
+----+--------+-------+------+
1 row in set (0.00 sec)

mysql> select * from kvs;
+------+-------+
| s_id | value |
+------+-------+
|    1 | hoge  |
+------+-------+
1 row in set (0.00 sec)

確かにデータが入っていることが確認できた。

もう少し複雑なデータをテストしてみよう。 KVList である。 これは、今回のデータ構造上、非常に重要な役目を持っているので、 とにかく重点的に確認しておきたいところだ。 このようなテストケースを書いてみる。

    public final void test2() {
        KeyValuePair kvp = new KVString();
        String testString1 = "hoge";
        kvp.setValue(testString1);

        KVList kvList = new KVList();
        List list = (List) kvList.getValue();
        list.add(kvp);

        int id = getFacade().add(kvList);

        KeyValuePair compare = getFacade().get(id);

        assertTrue(compare instanceof KVList);

        list = (List) compare.getValue();

        assertEquals(testString1, ((KeyValuePair) list.get(0)).getValue());
    }

これを実行すると、 次のようなエラーが発生してしまう。 (適当に改行を挿入している)

04:04:16,343 ERROR LazyInitializationException:19
 - failed to lazily initialize a collection of role:
  com.phinloda.orm.KVList.value, no session or session was closed
org.hibernate.LazyInitializationException:
 failed to lazily initialize a collection of role:
  com.phinloda.orm.KVList.value, no session or session was closed
	at org.hibernate.collection.AbstractPersistentCollection
	.throwLazyInitializationException
	(AbstractPersistentCollection.java:358)
...

一体何が起こったのかというと、 Hibernate の特徴の一つ、lazy fetch が原因だ。 デバッグ用のメッセージを表示すると、 次のような内容がある。

04:04:16,343 DEBUG Printer:83 - listing entities:
04:04:16,343 DEBUG Printer:90 - com.phinloda.orm.KVList{key=0, value=<uninitialized>, parent=0, id=3}
04:04:16,343 DEBUG AbstractFlushingEventListener:289 - executing flush
04:04:16,343 DEBUG AbstractFlushingEventListener:316 - post flush
04:04:16,343 DEBUG SessionFactoryUtils:785 - Closing Hibernate Session
04:04:16,343 DEBUG SessionImpl:291 - closing session

KVList の中身に注目。

com.phinloda.orm.KVList{key=0, value=<uninitialized>, parent=0, id=3}

value の中身が uninitialized となっている。 これが Hibernate の 遅延ロードという機能である。 hibernate.cfg.xml 中では、 次の箇所で lazy を true としている所に注目。

		<list name="value" lazy="true" cascade="all">

試しにこの lazy を false に変更してみれば、 テストケースは成功する。

Note

このように、 Java のコードにも、DB のテーブル構造にも影響しないような変更の後は、 ant を起動して codegen や schema を実行する必要はない。 hibernate.cfg.xml を変更するだけでよい。

しかし、lazy を true にする理由は、 必要なデータを使うときまで読み込まないことで、 効率を上げようという狙いがあるわけだから、 特に理由がない限りは、 false に指定すること自体があまり面白くない。 そこで、 このような場合はには HQL の join fetch という指定を使う。 これを使って get を書き直してみると、こんな感じになる。

    public KeyValuePair get(int id) {
        List list = getHibernateTemplate().find(
                "select si from siteinfo as si inner join fetch si.value where si.id=?"
                , Integer.valueOf(id));
        
        if (list == null || list.size() == 0) {
            return null;
        }

        return (KeyValuePair) list.get(0)
    }

そして、 hibernate.cfg.xml の lazy を true に戻してテストケースを実行すれば、 今度は正しく処理できて、 JUnit はグリーンバーを表示している。 一体どんなデータが生成されているのだろうか? schema を実行してテーブルをクリアした後でこのテストケースだけを実行すると (さっきの test1 はコメントアウトして実行しないようにする)、 このような状態になっている。

mysql> select * from kvbase;
+----+--------+-------+------+
| id | parent | kvkey | lpos |
+----+--------+-------+------+
|  1 |      0 |     0 | NULL |
|  2 |      1 |     0 |    0 |
+----+--------+-------+------+
2 rows in set (0.00 sec)

mysql> select * from kvlist;
+----+
| id |
+----+
|  1 |
+----+
1 row in set (0.00 sec)

mysql> select * from kvs;
+----+-------+
| id | value |
+----+-------+
|  2 | hoge  |
+----+-------+
1 row in set (0.00 sec)

mysql>

これもうまくいっているようだ。 ところで、parent というのは一体何だろう? これは、実は KVList に入っている要素から、 自分が一体どの KVList に入っているのか知るために使われるものだ。 つまり、リストを親、その要素を子だとすると、 子要素から親要素へ逆にたどるためのキーとなるのである。

さて、調子に乗ってこれはどうだ?

    public final void test3() {
        KVList kvList2 = new KVList();
        List list2 = (List) kvList2.getValue();

        KeyValuePair kvp = new KVString();
        String testString = "hoge";
        kvp.setValue(testString);

        list2.add(kvp);

        KVList kvList = new KVList();
        List list = (List) kvList.getValue();
        list.add(kvList2);

        int id = getFacade().add(kvList);

        KeyValuePair compare = getFacade().get(id);

        assertTrue(compare instanceof KVList);

        list = (List) compare.getValue();
        assertTrue(list.get(0) instanceof KVList);
        
        KVList childList = (KVList) list.get(0);
        List childValue = (List) childList.getValue();

        KeyValuePair childKvp = (KeyValuePair) childValue.get(0);
        assertEquals(testString, childKvp.getValue());
    }

つまり、2段のリストである。 実際はこうやってコピペしてテストケースを増やすのはよくないのだが、 それはそうとして、 このテストは通らず、エラーになってしまう。 DBの中身を確認しておこう。

mysql> select * from kvbase;
+----+--------+-------+------+
| id | parent | kvkey | lpos |
+----+--------+-------+------+
|  1 |      0 |     0 | NULL |
|  2 |      1 |     0 |    0 |
|  3 |      2 |     0 |    0 |
+----+--------+-------+------+
3 rows in set (0.00 sec)

mysql> select * from kvlist;
+----+
| id |
+----+
|  1 |
|  2 |
+----+
2 rows in set (0.00 sec)

mysql> select * from kvs;
+----+-------+
| id | value |
+----+-------+
|  3 | hoge  |
+----+-------+
1 row in set (0.00 sec)

mysql>

データは期待した通りに入っていることが分かる。 Exception はさっきと同じ現象だ。 つまり、Hibernate は、 ネストした KVList をうまく処理していないようだ。 テストケースを少し修正してみよう。

        KVList childList = (KVList) list.get(0);
        int childId = childList.getId();
        childList = (KVList) getFacade().get(childList.getId());
        List childValue = (List) childList.getValue();

間の2行を追加した。 childList をもう一度読み直しているのだ。 1段先が読み込めるのであれば、 ここで childList を get することで、 遅延ロードになってしまう KVString を実際に読み込もうという作戦だ。 これは実際うまくいって、 グリーンバーが表示される。

しかし、この処理には二つ問題がある。 アプリケーション側で、 いちいちこのように階層構造を処理するのは面倒だし、 うっかり忘れてしまうと Exception が発生するというのは面白くない。

Note

facade 側で再帰的に処理しておく手はある。 もちろん、いちいち処理したくなければ、 lazy を false にしてしまえばいいのだ。
    public KeyValuePair get(int id) {
        KeyValuePair kvp = getFetch(id);
        if (kvp instanceof KVList) {
            loadNestedList((KVList) kvp);
        }

        return kvp;
    }
    
    /**
     * この KVList の要素に KVList があれば、
     * その KVList の内容を getする。
     * 
     * @param kvList
     */
    protected void loadNestedList(KVList kvList) {
        if (kvList == null)
            return;
 
        List list = (List) kvList.getValue();
        if (list == null)
            return;
        
        for (int i = 0; i < list.size(); i++) {
            KeyValuePair kvp = (KeyValuePair) list.get(i);
            if (kvp instanceof KVList) {
                list.set(i, get(kvp.getId()));
            }            
        }
	}

なお、 この get には、 KVList が自分自身を要素に含んだり、 あるいはどこかでループしてネットワーク状になった場合に スタックを使い果たすというバグがある。 本来は処理したオブジェクトを Set に保持しておいて、 二度処理しないような工夫を行う必要がある。 実際にこのコードを流用しようという方は、気をつけて欲しい。

Note

もっとも、List で途中でループするような構造を作ってしまったら、 単純な ArrayList の toString でさえ破綻する。 そのような構造は想定していない、と見るべきかもしれない。

そして、もう一つの問題は、 もっと本質的なところで深刻である。 LazyInitializationException が出る理由は、 Session が close されているのに、 lazy fetch を実行しようとしたからだ。 つまり、 本来は Session を open したままで次の処理をしなければならない場面で Session が close されているのが問題なのである。 Session を open するというのは、 おそらくコストの高い処理の一つだ。

Hibernate を使う理由の一つは、 lazy fetch を使った処理の高速化にある。 必要なオブジェクトのロードを必要になるまで遅らせることで、 不要な処理をできるだけ省こうという戦略なのだ。

ということで、 この問題を解決するために、 Spring framework のいわゆる 宣言的トランザクション を使う。

Note

宣言的トランザクション (Declarative transaction management)というのは、 要するに、 トランザクションの設定を XML ファイルで宣言する方法のことである。 これに対して、 プログラム中にトランザクションの扱い方をコーディングして設定することもできる (Programmatic transaction management)。

applicationContext.xml は次のように変更する。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN"
 "http://www.springframework.org/dtd/spring-beans.dtd">

<beans>

  <bean id="MySqlDataSource"
    class="org.apache.commons.dbcp.BasicDataSource">
    <property name="driverClassName">
      <value>com.mysql.jdbc.Driver</value>
    </property>
    <property name="url">
      <value>jdbc:mysql://localhost/practice?useUnicode=true&amp;characterEncoding=utf8</value>
    </property>
    <property name="username"><value>********</value></property>
    <property name="password"><value>********</value></property>
  </bean>

  <bean id="sessionFactory"
    class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
    <property name="dataSource"><ref local="MySqlDataSource" /></property>
    <property name="mappingResources">
      <list>
        <value>com/phinloda/orm/KeyValuePair.hbm.xml</value>
      </list>
    </property>
    <property name="hibernateProperties">
      <props>
        <prop key="hibernate.dialect">org.hibernate.dialect.MySQLDialect</prop>
        <prop key="hibernate.show_sql">true</prop>
        <prop key="hibernate.connecton.charSet">UTF-8</prop>
      </props>
    </property>
  </bean>

  <bean id="txManager"
    class="org.springframework.orm.hibernate3.HibernateTransactionManager">
    <property name="sessionFactory">
      <ref local="sessionFactory" />
    </property>
  </bean>
  
  <bean id="myService"
    class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
    <property name="transactionManager"><ref local="txManager"/></property>
    <property name="target"><ref local="myFacade"/></property>
    <property name="transactionAttributes">
      <props>
        <prop key="add*">PROPAGATION_REQUIRED</prop>
        <prop key="save*">PROPAGATION_REQUIRED</prop>
        <prop key="*">PROPAGATION_REQUIRED, readOnly</prop>
      </props>
    </property>
  </bean>

  <bean name="myFacade" id="myFacade" 
    class="com.phinloda.orm.KeyValuePairFacadeHibernateImpl" singleton="false">
    <property name="sessionFactory">
    <ref local="sessionFactory"/>
    </property>
  </bean>

</beans>

myService を定義しているところで、 transactionAttributes の props 中、 add*、save* を PROPAGATION_REQUIRED、 その他を PROPAGATION_REQUIRED, readOnly と指定している。 これは、 com.phinloda.orm.KeyValuePairFacadeHibernateImpl (com.phinloda.orm.KeyValuePairFacade も同じ) の中で、 add と save というメソッドが書き込みを行うものとして定義されているので、 それ以外は readOnly に指定したのである。

これだけでは、 JUnit でテストするときにうまく動作しない。 テストケースも修正して、次のようにする。

package com.phinloda.orm;

import java.util.List;

import org.hibernate.FlushMode;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.orm.hibernate3.SessionFactoryUtils;
import org.springframework.orm.hibernate3.SessionHolder;
import org.springframework.transaction.support.TransactionSynchronizationManager;

import junit.framework.TestCase;

public class KeyValuePairTest extends TestCase {

    public static void main(String[] args) {
        junit.textui.TestRunner.run(KeyValuePairTest.class);
    }

    protected static final String SESSION_FACTORY = "sessionFactory";
    protected static final String FACADE = "myFacade";

    protected SessionFactory sessionFactory = null;

    protected void setUp() throws Exception {
        super.setUp();

        BeanFactory factory = new ClassPathXmlApplicationContext(
                "/applicationContext.xml");
        setFacade((KeyValuePairFacade) factory.getBean(FACADE));

        sessionFactory = (SessionFactory) factory.getBean(SESSION_FACTORY);
        Session session = SessionFactoryUtils.getSession(sessionFactory, true);
        TransactionSynchronizationManager.bindResource(sessionFactory,
                new SessionHolder(session));

    }

    protected void tearDown() throws Exception {
        SessionHolder sessionHolder = (SessionHolder) TransactionSynchronizationManager
                .unbindResource(sessionFactory);
        SessionFactoryUtils.releaseSession(sessionHolder.getSession(),
                sessionFactory);

        super.tearDown();
    }

    private KeyValuePairFacade facade;

    public KeyValuePairFacade getFacade() {
        return facade;
    }

    public void setFacade(KeyValuePairFacade facade) {
        this.facade = facade;
    }

    public final void test1() {
        KeyValuePair kvp = new KVString();
        String testString = "hoge";
        kvp.setValue(testString);

        int id = getFacade().add(kvp);

        KeyValuePair compare = getFacade().get(id);
        assertEquals(testString, compare.getValue());
    }

    public final void test2() {
        KeyValuePair kvp = new KVString();
        String testString1 = "hoge";
        kvp.setValue(testString1);

        KVList kvList = new KVList();
        List list = (List) kvList.getValue();
        list.add(kvp);

        int id = getFacade().add(kvList);

        KeyValuePair compare = getFacade().get(id);

        assertTrue(compare instanceof KVList);

        list = (List) compare.getValue();

        assertEquals(testString1, ((KeyValuePair) list.get(0)).getValue());
    }

    public final void test3() {
        KVList kvList2 = new KVList();
        List list2 = (List) kvList2.getValue();

        KeyValuePair kvp = new KVString();
        String testString = "hoge";
        kvp.setValue(testString);

        list2.add(kvp);

        KVList kvList = new KVList();
        List list = (List) kvList.getValue();
        list.add(kvList2);

        int id = getFacade().add(kvList);

        KeyValuePair compare = getFacade().get(id);

        assertTrue(compare instanceof KVList);

        list = (List) compare.getValue();
        assertTrue(list.get(0) instanceof KVList);

        KVList childList = (KVList) list.get(0);
        List childValue = (List) childList.getValue();

        KeyValuePair childKvp = (KeyValuePair) childValue.get(0);
        assertEquals(testString, childKvp.getValue());
    }

}

setUp と tearDown の所に注目。 これは Web アプリケーションでは OpenSessionInViewInterceptor が行うべき動作なのだが、 JUnit のテストケースを実行するときは、 servlet などが行うその種の処理をシミュレートしなければならない。

Note

この話題は、 Hibernate Forums - View topic - Open Session in View and testing: や、 Unit tests and LazyInitializationException - Spring Framework Support Forums: に出てくる。 これらの情報は、Hibernate 2.x の頃に書かれているのだが、 今回は、 Hibernate 3.x を使っているため、 それ用に一部を修正してある。

サイト情報を入れる

この永続化可能なクラスを使って、 実際にサイトの情報を入れてみるとどうなるのか。 最初に想定したデータ構造は、次のようなものだった。

-----------------------
     class SiteInfo
-----------------------
- id : int
- siteid : int
- uri : String
- title : String
- pageDate : Date
- checkDate : Date
- status : Status
- closed : boolean
- lost : boolean
-----------------------
-----------------------

これを KepValuePair に入れるには、 対応しなければならないクラスが少し不足している。 そこで、Date と boolean に対応するクラスを追加する。 いろいろ追加した結果がこんな感じになった。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-mapping SYSTEM
          "file:///d:/java/dtd/hibernate-mapping-3.0.dtd">

<hibernate-mapping>
	<class name="com.phinloda.orm.KeyValuePair" table="kvbase"
		entity-name="siteinfo">
		<id name="id" type="int" column="id">
			<generator class="increment" />
		</id>

		<property name="parent" type="int" />
		<property name="key" column="kvkey" type="int"></property>

	</class>

	<joined-subclass name="com.phinloda.orm.KVBoolean" table="kvb"
		extends="siteinfo">
		<key column="id" />
		<property name="value" type="boolean" column="value" />
	</joined-subclass>

	<joined-subclass name="com.phinloda.orm.KVByte" table="kvby"
		extends="siteinfo">
		<key column="id" />
		<property name="value" type="byte" column="value" />
	</joined-subclass>

	<joined-subclass name="com.phinloda.orm.KVShort" table="kvsh"
		extends="siteinfo">
		<key column="id" />
		<property name="value" type="short" column="value" />
	</joined-subclass>

	<joined-subclass name="com.phinloda.orm.KVInteger" table="kvi"
		extends="siteinfo">
		<key column="id" />
		<property name="value" type="int" column="value" />
	</joined-subclass>

	<joined-subclass name="com.phinloda.orm.KVLong" table="kvl"
		extends="siteinfo">
		<key column="id" />
		<property name="value" type="long" column="value" />
	</joined-subclass>

	<joined-subclass name="com.phinloda.orm.KVDouble" table="kvd"
		extends="siteinfo">
		<key column="id" />
		<property name="value" type="java.lang.Double" column="value" />
	</joined-subclass>

	<joined-subclass name="com.phinloda.orm.KVTimestamp" table="kvt"
		extends="siteinfo">
		<key column="id" />
		<property name="value" type="java.sql.Timestamp" column="value" />
	</joined-subclass>

	<joined-subclass name="com.phinloda.orm.KVString" table="kvs"
		extends="siteinfo">
		<key column="id" />
		<property name="value" type="string" column="value" />
	</joined-subclass>

	<joined-subclass name="com.phinloda.orm.KVList" table="kvlist"
		extends="siteinfo">
		<key column="id" />
		<list name="value" lazy="true" cascade="all">
			<key column="parent" />
			<list-index column="lpos" />
			<one-to-many class="siteinfo" />
		</list>
	</joined-subclass>

</hibernate-mapping>

少し説明が必要である。 KeyValuePair の定義のところで、 entity-name という属性を指定している。 これは何かというと、 永続化するときのターゲットとなるオブジェクトを指定しているのだ。 言葉にすると分かりにくいが、 あるデータをファイルに保存するときのファイル名とか、 DB に保存するときのテーブル名称というか、その種のものである。 データベースを使うときには、 全て同じテーブルに入れようとはしない。 データの種類によって、違うテーブルを使うのが普通である。 例えば、ちょっと例が下手だが、 本社社員のデータと、 支社の社員のデータを考えてみると、 これらは同じ形式であっても、別のテーブルに入れておきたい、 というようなことが考えられるという、まあそういう話だ。

しかし、Hibernate を使う場合、 基本的に、プログラムのソースコードからは、 データベースの存在は隠蔽されている。 どのテーブルに保存しろ、というような指定はできない。 HQL も、検索対象はクラスで指定するわけで、 テーブルを指定するのではない。

では、同じクラスのデータであっても別テーブルに分けたいときに、 どうするか。 そこで出てくるのが entity なのである。 テーブルではなく、entity という概念を持ち込むことで、 保存先を指定することが可能になるのだ。

ということで、先のマッピングファイルは、 entity-name を siteinfo と指定した。 これは、サイト情報の保存先を指定したことになる。 ここだけを修正すると、 codegen が次のようなエラーを吐く。

BUILD FAILED
D:\java\eclipse\workspace\NetUtil\src\com\phinloda\orm\build.xml:137:
 org.hibernate.MappingException:
  Following superclasses referenced in extends not found:
  com.phinloda.orm.KeyValuePair

Total time: 3 seconds

マッピングファイルをもう少し修正しなければならない。 extends の値も、 クラスでなく、基底クラスの entity に変更する。 codegen でコードを生成すると、 この entity は、基底クラスのクラスに、自動的に変換してくれる。 もう一つ注意が必要なのは、 HQL のクエリの中身だ。 クラスを書くところに、entity を指定しなければならない。 例えば、こう書くとエラーになってしまう。

select * from KeyValuePair

次のように書けばよい。

select * from siteinfo

さて、サイトのいろいろな情報を入れるためには、 それらを識別するための情報が必要になる。 KVString の値が仮に http://www.phinloda.com という文字列であったとしても、 それが URL なのか、それともサイト名なのか、 どうやって区別するのか、ということだ。

もちろん、それを識別するためのものが key である。 つまり、例えば key が1なら URL、 2ならサイト名、とか決めておけばいいのだ。 しかし、どうやってそれを決めればいいのだろうか?

それは意外と面倒な問題なので、 当分は決めうちにすることにして、 ちょっと後回しにしておこう。 とりあえず、何か定数で決めることにすれば、 データはこんな感じで扱うことになる。

        KVInteger siteid = new KVInteger();
        siteid.setKey(KEY_SITEINFO_SITEID);
        siteid.setValue(new Integer(10));
        
        KVString uri = new KVString();
        uri.setKey(KEY_SITEINFO_URI);
        uri.setValue("http://www.phinloda.com/");
        
        KVString title = new KVString();
        title.setKey(KEY_SITEINFO_TITLE);
        title.setValue("phinloda's site");
        
        KVTimeStamp pageDate = new KVTimeStamp();
        pageDate.setKey(KEY_SITEINFO_PAGEDATE);
        pageDate.setValue(null);
        
        KVTimeStamp checkDate = new KVTimeStamp();
        checkDate.setKey(KEY_SITEINFO_CHECKDATE);
        checkDate.setValue(null);
        
        KVInteger status = new KVInteger();
        status.setKey(KEY_SITEINFO_STATUS);
        status.setValue(0);
        
        KVBoolean closed = new KVBoolean();
        closed.setKey(KEY_SITEINFO_CLOSED);
        closed.setValue(false);
        
        KVBoolean lost = new KVBoolean();
        lost.setKey(KEY_SITEINFO_LOST);
        lost.setValue(false);
        
        KVList info = new KVList();
        info.setKey(KEY_SITEINFO_SITEINFO);
        List list = (List) info.getValue();
        list.add(siteid);
        list.add(uri);
        list.add(title);
        list.add(pageDate);
        list.add(checkDate);
        list.add(status);
        list.add(closed);
        list.add(lost);
        
        int id = getFacade().add(info);

これに対して検索をかけるにはどうするか。 極端な場合として、 2通りの方法がある。 HQL で検索して結果を出す方法と、 オブジェクトを load しておいて、 Java 側で検索ロジックを書く方法だ。

後者の方がロジックとしては分かりやすいので、 先に紹介しておく。 仮に、前述のような形式で保存されたサイト情報が、 List sites; に入っているとする。 ベタな検索方法だが、 もし SiteInfo というクラスに getId() というメソッドがあれば、 id が someId である SiteInfo を探すには、 次のようにすればいい。

        for (int i = 0; i < sites.size(); i++) {
            SiteInfo info = sites.get(i);
            if (info.getId() == id)
                return info;            
        }

問題は、SiteInfo をどうするか、getId をどう実装するかだ。 もう一方の HQL で探す方だが、 仮に findBySiteId(int) のようなメソッドがあるとすれば、 多分、こんな感じでできそうだ。

    public List findBySiteId(int siteId) {
        List list = getHibernateTemplate()
                .find(
                        "from KVList as kvl, KVInteger as kvi where kvi.key=1 and kvi.value=? and kvl.id=kvi.parent",
                        Integer.valueOf(siteId));
        return list;
    }

siteId に対応する key が 1 であると決めうちしている。 Eclipse のデフォルトのまま format かけたらこんな所で改行するのだが、 気にしない。

(この項、執筆中)