powerdee.com
Google
 
このサイト内 Web
 
カウンタ

Hibernateの関連について

インデックスへ戻る
MiddlegenIDEでデータベーススキーマからマッピングファイルとPOJOを自動生成してみます。
当サンプルのソースコードはこちらです。

データベーススキーマの作成

サンプルとして以下のようなテーブルスキーマを作成します。Clayを利用してsqlも自動生成しちゃいます。

Categoryは1対多による自己関連で、CategoryとItemは1対多の関連です。

クラス図で表すと以下のようになります。ちなみに関連は双方向です。

MiddlegenIDEでマッピングファイルを自動生成する

ここでは、Cascadeをall-delete-orphanに設定しています。categoryとitemの関連も同じです。

以下のようにPOJO、マッピングファイル、hibernate設定ファイルが生成されています。

生成されたマッピング定義ファイル Category.hbm.xml のitemとの関連を定義する部分に条件(order-by、where)を追加してみます。

    <set
        name="items"
        lazy="true"
        inverse="true"
        cascade="all-delete-orphan"
        order-by="ITEM_ID asc"        <-- ITEMテーブルをITEM_IDの昇順に並べる
        where="DELETE_DATE IS NULL"   <-- 論理的に削除されているレコードは対象外
    >
        <key>
            <column name="CATEGORY_ID" />
        </key>
        
        <one-to-many 
            class="com.powerdee.model.Item"
        />
    </set>

Categoryに足場コードを追加する

生成されたPOJOのCategory.javaに1対多関連の足場コードを追加します。これは関係の管理を容易にします。
setItemsをprivateに変更することで、addメソッドが強制され、関連の端との整合性が保たれる。
ちなみに、Hibernateはプロパティアクセッサメソッドの可視性はなんであろうと問いません。

    public void addItem(Item item) {
        if (item == null)
            throw new IllegalArgumentException("Can't add a null Category as item.");
        if (item.getCategory() != null)
        	item.getCategory().getItems().remove(item);
        
        item.setCategory(this);
        
        this.getItems().add(item);
    }

    private void setItems(Set items) {    <-- 可視性をpublicからprivateへ変更
        this.items = items;
    }

    public void addChildCategory(Category category) {
        if (category == null)
            throw new IllegalArgumentException("Can't add a null Category.");
        if (category.getCategory() != null)
        	category.getCategory().getCategories().remove(category);
        
        category.setCategory(this);
        
        this.getCategories().add(category);
    }

    private void setCategories(Set categories) {
        this.categories = categories;
    }

意外とはまりやすいのが、多側のプロパティを生成しておくこと。わすれると足場コードでNullPointerExceptionです。
実装はLinkedHashSetです。addメソッドで追加した順に永続化するのと、先ほどマッピングファイルで指定したorder-byでソートする為です。

    /** persistent field */
    private Set items = new LinkedHashSet();

    /** persistent field */
    private Set categories = new LinkedHashSet();

カスケードをテストしてみる。

まず、testInsertCategory()で親カテゴリを永続化しています。(★1)
すると、Category.hbm.xmlのcascade="all-delete-orphan"の指定により、親カテゴリに関連する子カテゴリ、 さらには子カテゴリに関連するアイテムその1、アイテムその2、アイテムその3も連鎖保存(cascade save)されます。 (推移的永続化と呼ばれる)
testSelectDeleteCategory()により、関連するオブジェクトが永続化されているのを確認できます。 また、親カテゴリを削除する(★2)ことにより、関連するオブジェクトも削除されます。

package com.powerdee.model;

import java.util.Iterator;
import java.util.List;

import junit.framework.TestCase;

import org.hibernate.HibernateException;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.cfg.Configuration;

public class Hibernate2test extends TestCase {

    private static final SessionFactory sessionFactory;

    static {
        try {
            sessionFactory 
                = new Configuration().configure().buildSessionFactory();
        } catch (Throwable ex) {
            throw new ExceptionInInitializerError(ex);
        }
    }
    
    public static Session getSession() throws HibernateException {
        return sessionFactory.openSession();
    }

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

    public void testInsertCategory() throws Exception {
        Session session = getSession();

        Item item1 = new Item();
        item1.setName("アイテムその1");
        Item item2 = new Item();
        item2.setName("アイテムその2");
        Item item3 = new Item();
        item3.setName("アイテムその3");
        
        Category childCategory = new Category();
        childCategory.setName("子カテゴリ");
        childCategory.addItem(item1);    <-- 子カテゴリにアイテムその1を追加
        childCategory.addItem(item2);    <-- 子カテゴリにアイテムその2を追加
        childCategory.addItem(item3);    <-- 子カテゴリにアイテムその3を追加
        
        Category parentCategory = new Category();
        parentCategory.setName("親カテゴリ");
        parentCategory.addChildCategory(childCategory);    <-- 親カテゴリに子カテゴリを追加

        Transaction transaction = session.beginTransaction();
        session.save(parentCategory);    <-- ★1親カテゴリを永続化!
        transaction.commit();
        session.close();
    }
    
    public void testSelectDeleteCategory() throws Exception {
        Session session = getSession();
        Transaction transaction = session.beginTransaction();
        
        String HQL 
            = "from Category cat where cat.category.categoryId is null";
        Query query = session.createQuery(HQL);
        List categoryList = query.list();
        Category parentCategory = (Category) categoryList.get(0);
        assertEquals("親カテゴリ", parentCategory.getName());
        
        Iterator categories = parentCategory.getCategories().iterator();
        Category childCategory = (Category) categories.next();
        assertEquals("子カテゴリ", childCategory.getName());
        
        Iterator items = childCategory.getItems().iterator();
        assertEquals("アイテムその1", ((Item) items.next()).getName());
        assertEquals("アイテムその2", ((Item) items.next()).getName());
        assertEquals("アイテムその3", ((Item) items.next()).getName());
        
        session.delete(parentCategory);    <-- ★2これで全て削除される!
        transaction.commit();
        
        session.close();
    }
}

論理削除済のアイテムその4を追加してみます。

package com.powerdee.model;

import java.util.Date;
import java.util.Iterator;
import java.util.List;

import junit.framework.TestCase;

import org.hibernate.HibernateException;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.cfg.Configuration;

public class Hibernate2test extends TestCase {

    private static final SessionFactory sessionFactory;

    static {
        try {
            sessionFactory 
                = new Configuration().configure().buildSessionFactory();
        } catch (Throwable ex) {
            throw new ExceptionInInitializerError(ex);
        }
    }
    
    public static Session getSession() throws HibernateException {
        return sessionFactory.openSession();
    }

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

    public void testInsertCategory() throws Exception {
        Session session = getSession();

        Item item1 = new Item();
        item1.setName("アイテムその1");
        Item item2 = new Item();
        item2.setName("アイテムその2");
        Item item3 = new Item();
        item3.setName("アイテムその3");
        Item item4 = new Item();
        item4.setName("アイテムその4");
        item4.setDeleteDate(new Date());   <-- 削除日を入力
        
        Category childCategory = new Category();
        childCategory.setName("子カテゴリ");
        childCategory.addItem(item1);
        childCategory.addItem(item2);
        childCategory.addItem(item3);
        childCategory.addItem(item4);
        
        Category parentCategory = new Category();
        parentCategory.setName("親カテゴリ");
        parentCategory.addChildCategory(childCategory);

        Transaction transaction = session.beginTransaction();
        session.save(parentCategory);
        transaction.commit();
        session.close();
    }
    
    public void testSelectDeleteCategory() throws Exception {
        Session session = getSession();
        Transaction transaction = session.beginTransaction();
        
        String HQL 
            = "from Category cat where cat.category.categoryId is null";
        Query query = session.createQuery(HQL);
        List categoryList = query.list();
        Category parentCategory = (Category) categoryList.get(0);
        assertEquals("親カテゴリ", parentCategory.getName());
        
        Iterator categories = parentCategory.getCategories().iterator();
        Category childCategory = (Category) categories.next();
        assertEquals("子カテゴリ", childCategory.getName());
        
        Iterator items = childCategory.getItems().iterator();
        assertEquals("アイテムその1", ((Item) items.next()).getName());
        assertEquals("アイテムその2", ((Item) items.next()).getName());
        assertEquals("アイテムその3", ((Item) items.next()).getName());
        
        session.delete(parentCategory);
        transaction.commit();
        
        session.close();
    }
}

すると、deleteで失敗してしまいます。ログを見ると・・・参照整合性違反ですね。 アイテムその4を取得せずに削除を行った為、子カテゴリが削除できないわけです。
cascade="all-delete-orphan"とwhere=の指定が混在すると混乱しますね。。

71 logExceptions WARN  org.hibernate.util.JDBCExceptionReporter  
    - SQL Error: 1217, SQLState: 23000
72 logExceptions ERROR org.hibernate.util.JDBCExceptionReporter  
    - Cannot delete or update a parent row: a foreign key constraint fails

遅延フェッチと外部結合(イーガーフェッチ)

先ほどのtestSelectDeleteCategory()のログを見ると以下のように個別にSELECT文が発行されているのがわかります。
マッピングファイルの関連指定で遅延フェッチ(lazy="true")と指定しているために、?.iterator()を呼び出すことで 関連にアクセスしたときにSQL SELECTによって遅延ロードしています。

select 
  category0_.CATEGORY_ID as CATEGORY1_0_, category0_.NAME as NAME0_, 
  category0_.INSERT_DATE as INSERT3_0_, category0_.UPDATE_DATE as UPDATE4_0_,
  category0_.DELETE_DATE as DELETE5_0_, category0_.PARENT_CATEGORY_ID as PARENT6_0_ 
from 
  category category0_ 
where 
  category0_.PARENT_CATEGORY_ID is null

select 
  categories0_.PARENT_CATEGORY_ID as PARENT6_1_,
  categories0_.CATEGORY_ID as CATEGORY1_1_, 
  categories0_.CATEGORY_ID as CATEGORY1_0_0_, 
  categories0_.NAME as NAME0_0_, categories0_.INSERT_DATE as INSERT3_0_0_, 
  categories0_.UPDATE_DATE as UPDATE4_0_0_, 
  categories0_.DELETE_DATE as DELETE5_0_0_, 
  categories0_.PARENT_CATEGORY_ID as PARENT6_0_0_ 
from 
  category categories0_ 
where 
  categories0_.PARENT_CATEGORY_ID=?

select 
  items0_.CATEGORY_ID as CATEGORY8_1_, items0_.ITEM_ID as ITEM1_1_, 
  items0_.ITEM_ID as ITEM1_1_0_, items0_.NAME as NAME1_0_, 
  items0_.DESCRIPTION as DESCRIPT3_1_0_, items0_.PRICE as PRICE1_0_, 
  items0_.INSERT_DATE as INSERT5_1_0_, items0_.UPDATE_DATE as UPDATE6_1_0_, 
  items0_.DELETE_DATE as DELETE7_1_0_, items0_.CATEGORY_ID as CATEGORY8_1_0_ 
from 
  item items0_ 
where  
  items0_.DELETE_DATE IS NULL and items0_.CATEGORY_ID=? order by items0_.ITEM_ID asc

select 
  items0_.CATEGORY_ID as CATEGORY8_1_, items0_.ITEM_ID as ITEM1_1_, 
  items0_.ITEM_ID as ITEM1_1_0_, items0_.NAME as NAME1_0_, 
  items0_.DESCRIPTION as DESCRIPT3_1_0_, items0_.PRICE as PRICE1_0_, 
  items0_.INSERT_DATE as INSERT5_1_0_, items0_.UPDATE_DATE as UPDATE6_1_0_, 
  items0_.DELETE_DATE as DELETE7_1_0_, items0_.CATEGORY_ID as CATEGORY8_1_0_ 
from 
  item items0_ 
where  
  items0_.DELETE_DATE IS NULL and items0_.CATEGORY_ID=? order by items0_.ITEM_ID asc

select 
  categories0_.PARENT_CATEGORY_ID as PARENT6_1_, 
  categories0_.CATEGORY_ID as CATEGORY1_1_, 
  categories0_.CATEGORY_ID as CATEGORY1_0_0_, 
  categories0_.NAME as NAME0_0_, categories0_.INSERT_DATE as INSERT3_0_0_, 
  categories0_.UPDATE_DATE as UPDATE4_0_0_, 
  categories0_.DELETE_DATE as DELETE5_0_0_, 
  categories0_.PARENT_CATEGORY_ID as PARENT6_0_0_ 
from 
  category categories0_ 
where 
  categories0_.PARENT_CATEGORY_ID=?


以下のように外部結合フェッチを行うと1回のSELECTで親カテゴリ、子カテゴリ、アイテムのオブジェクト構造を取得することが可能です。

    public void testEagerFetch() throws Exception {
        Session session = getSession();
        
        String HQL 
        = "from Category as c " +
                "left join fetch c.categories " +
                "left join fetch c.items " +
                "where c.category.categoryId is null";

        Query query = session.createQuery(HQL);
        List categoryList = query.list();
        assertEquals(1, categoryList.size());
        Category parentCategory = (Category) categoryList.get(0);
        assertEquals("親カテゴリ", parentCategory.getName());
        
        Iterator categories = parentCategory.getCategories().iterator();
        Category childCategory = (Category) categories.next();
        assertEquals("子カテゴリ", childCategory.getName());
        
        Iterator items = childCategory.getItems().iterator();
        assertEquals("アイテムその1", ((Item) items.next()).getName());
        assertEquals("アイテムその2", ((Item) items.next()).getName());
        assertEquals("アイテムその3", ((Item) items.next()).getName());
        
        session.close();
    }

ログを見ると以下のようなSQLが発行されたようです。
むむ?これだと親カテゴリと子カテゴリは外部結合されましたが、子カテゴリからアイテムまでは拾ってきてくれていないですね。。あたりまえ><

select 
  category0_.CATEGORY_ID as CATEGORY1_0_0_, 
  categories1_.CATEGORY_ID as CATEGORY1_0_1_, 
  items2_.ITEM_ID as ITEM1_1_2_, category0_.NAME as NAME0_0_, 
  category0_.INSERT_DATE as INSERT3_0_0_, category0_.UPDATE_DATE as UPDATE4_0_0_, 
  category0_.DELETE_DATE as DELETE5_0_0_, category0_.PARENT_CATEGORY_ID as PARENT6_0_0_, 
  categories1_.NAME as NAME0_1_, categories1_.INSERT_DATE as INSERT3_0_1_, 
  categories1_.UPDATE_DATE as UPDATE4_0_1_, categories1_.DELETE_DATE as DELETE5_0_1_, 
  categories1_.PARENT_CATEGORY_ID as PARENT6_0_1_, categories1_.PARENT_CATEGORY_ID as PARENT6_0__, 
  categories1_.CATEGORY_ID as CATEGORY1_0__, 
  items2_.NAME as NAME1_2_, items2_.DESCRIPTION as DESCRIPT3_1_2_, 
  items2_.PRICE as PRICE1_2_, items2_.INSERT_DATE as INSERT5_1_2_, 
  items2_.UPDATE_DATE as UPDATE6_1_2_, items2_.DELETE_DATE as DELETE7_1_2_, 
  items2_.CATEGORY_ID as CATEGORY8_1_2_, items2_.CATEGORY_ID as CATEGORY8_1__, 
  items2_.ITEM_ID as ITEM1_1__ 
from 
  category category0_ 
    left outer join category categories1_ 
    on category0_.CATEGORY_ID=categories1_.PARENT_CATEGORY_ID 
    left outer join item items2_ 
    on category0_.CATEGORY_ID=items2_.CATEGORY_ID and items2_.DELETE_DATE IS NULL 
where 
  category0_.PARENT_CATEGORY_ID is null order by items2_.ITEM_ID asc

select 
  items0_.CATEGORY_ID as CATEGORY8_1_, items0_.ITEM_ID as ITEM1_1_, 
  items0_.ITEM_ID as ITEM1_1_0_, items0_.NAME as NAME1_0_, 
  items0_.DESCRIPTION as DESCRIPT3_1_0_, items0_.PRICE as PRICE1_0_, 
  items0_.INSERT_DATE as INSERT5_1_0_, items0_.UPDATE_DATE as UPDATE6_1_0_, 
  items0_.DELETE_DATE as DELETE7_1_0_, items0_.CATEGORY_ID as CATEGORY8_1_0_ 
from 
  item items0_ 
where  
  items0_.DELETE_DATE IS NULL and 
  items0_.CATEGORY_ID=? 
order by 
  items0_.ITEM_ID asc

遅延ロードで複数回SELECTしてくるか、HQLで結合クエリを明示的に記述するかですかね
マッピングはLAZYにしておいて、HQLでfetch Join するのがベストのようです。
ちなみに以下のように条件を入れないと、1回のSELECTのみ発行されるんですが、1対多の関連なんで返却されるlistのサイズが4になっていますね。。。

    public void testEagerFetch() throws Exception {
        Session session = getSession();
        
        String HQL 
        = "from Category as c " +
                "left join fetch c.categories " +
                "left join fetch c.items";  <-- 条件を入れない

        Query query = session.createQuery(HQL);
        List categoryList = query.list();
        assertEquals(4, categoryList.size());  <-- 親も関連する子の数だけ返されている。。
        Category parentCategory = (Category) categoryList.get(0);
        assertEquals("親カテゴリ", parentCategory.getName());
        
        Iterator categories = parentCategory.getCategories().iterator();
        Category childCategory = (Category) categories.next();
        assertEquals("子カテゴリ", childCategory.getName());
        
        Iterator items = childCategory.getItems().iterator();
        assertEquals("アイテムその1", ((Item) items.next()).getName());
        assertEquals("アイテムその2", ((Item) items.next()).getName());
        assertEquals("アイテムその3", ((Item) items.next()).getName());
        
        session.close();
    }
select 
  category0_.CATEGORY_ID as CATEGORY1_0_0_, 
  categories1_.CATEGORY_ID as CATEGORY1_0_1_, 
  items2_.ITEM_ID as ITEM1_1_2_, 
  category0_.NAME as NAME0_0_, 
  category0_.INSERT_DATE as INSERT3_0_0_, 
  category0_.UPDATE_DATE as UPDATE4_0_0_, 
  category0_.DELETE_DATE as DELETE5_0_0_, 
  category0_.PARENT_CATEGORY_ID as PARENT6_0_0_, 
  categories1_.NAME as NAME0_1_, 
  categories1_.INSERT_DATE as INSERT3_0_1_, 
  categories1_.UPDATE_DATE as UPDATE4_0_1_, 
  categories1_.DELETE_DATE as DELETE5_0_1_, 
  categories1_.PARENT_CATEGORY_ID as PARENT6_0_1_, 
  categories1_.PARENT_CATEGORY_ID as PARENT6_0__, 
  categories1_.CATEGORY_ID as CATEGORY1_0__, 
  items2_.NAME as NAME1_2_, 
  items2_.DESCRIPTION as DESCRIPT3_1_2_, 
  items2_.PRICE as PRICE1_2_, 
  items2_.INSERT_DATE as INSERT5_1_2_, 
  items2_.UPDATE_DATE as UPDATE6_1_2_, 
  items2_.DELETE_DATE as DELETE7_1_2_, 
  items2_.CATEGORY_ID as CATEGORY8_1_2_, 
  items2_.CATEGORY_ID as CATEGORY8_1__, 
  items2_.ITEM_ID as ITEM1_1__ 
from 
  category category0_ 
    left outer join category categories1_ 
      on category0_.CATEGORY_ID=categories1_.PARENT_CATEGORY_ID 
    left outer join item items2_ 
      on category0_.CATEGORY_ID=items2_.CATEGORY_ID 
      and items2_.DELETE_DATE IS NULL 
order 
  by items2_.ITEM_ID asc

インデックスへ戻る


おすすめ書籍


HIBERNATE イン アクション

著者:Christain Bauer、Gavin Ki
出版社:ソフトバンク クリエイティブ(2005-12-28)
価格:¥4,410(税込)
Light Weight Java―JSF/Hibernate/SpringによるフレームワークでWebアプリケーションの開発効率向上

著者:岡本 隆史、金子 崇之、吉田 英嗣、権藤 夏男
出版社:毎日コミュニケーションズ(2005-04)
価格:¥3,360(税込)


ページTopへ / ▲Homeへ