ITコンサルの日常

ITコンサル会社に勤務する普通のITエンジニアの日常です。

HibernateでJOIN&複合キー

Hibernate(ver3.0.5)でありがちなサンプルを作ってみるのに、すげえ大変だったっていう話です。というか、SQL + JDBCでやってきた僕にとっては、結構どうでも良いことではまってストレスが溜まるものだなあという感想を持ちました。Hibernateのサンプルを示す時は、

をひとまとめにしてやる必要があると感じました。そういう意味だと、Chapter 8. Association Mappingsって分かりづらいですね。

ありがちなサンプルってのは、顧客・商品・注文の三つの概念に対して、誰がどの商品をいくつ注文したかを一覧にして表示するというアプリケーションのことです。データベース設計としては、以下のようにしました。

  • 顧客(Customer) (customer-id char(5), customer-name char(50))
  • 商品(Item) (item-id char(5), item-name char(50))
  • 注文(Purchase) (customer-id char(5), item-id char(5), quantity int)

これで、customer-idとitem-idでそれぞれJOINすれば、望みの結果が得られるというわけです。SQLで表現すれば、以下のようになるでしょう。

SELECT c.customer_name, i.item_name, p.quantity FROM Customer c, Item i, Purchase p WHERE p.customer_id = c.customer_id AND p.item_id = i.item_id

それで、Hibernateで同じことをやろうとしたら、これが見事にハマりました。

■はまりポイントその1
まずはJOINを行わず、Purchase表のみを読み取ろうと試みました。つまり、SQLで表現すれば、

SELECT p.customer_id, p.item_id, p.quantity FROM Purchase p

というところです。この段階では、顧客テーブル、商品テーブルはJOINしていないことから、顧客名や商品名などの翻訳は表示することができません。
主キーとしては、customer_id, item_idの複合キーとなることから、

<class name="exam.Purchase" table="Purchase">
    <composite-id>
        <key-property name = "customer_id"/>
        <key-property name = "item_id"/>
    </composite-id>
    <property name = "quantity"/>
</class>

と定義したのですが、

java.lang.ClassCastException
    at org.hibernate.loader.Loader.getKeyFromResultSet(Loader.java:759)
    at org.hibernate.loader.Loader.getRowFromResultSet(Loader.java:292)
    at org.hibernate.loader.Loader.doQuery(Loader.java:412)

と、ClassCastExceptionが発生。どうやら、Idクラスを内部クラスで別途定義してやる必要があるようです。

・マッピング
<class name="exam.Purchase" table="Purchase">
    <composite-id name = "id">
        <key-property name = "customer_id"/>
        <key-property name = "item_id"/>
    </composite-id>
    <property name = "quantity"/>
</class>

・Javaソース
import java.io.Serializable;

public class Purchase
{
    // フィールド定義
    private int quantity;
    private Purchase.Id id;

    // quantity, idのgetter, setter

    static class Id implements Serializable
    {
        private String customer_id;
        private String item_id;

        // customer_id, item_idそれぞれのgetter, setter
    }
}

・クライアントクラスのソース
package exam;

import java.util.List;

import org.hibernate.Query;
import org.hibernate.Session;

public class TestHibernate
{
    public static void main(String[] args)
    {
        Session session = HibernateUtil.currentSession();

        Query q = session.createQuery("from exam.Purchase");
        List l = q.list();

        for(int i=0; i<l.size(); i++)
        {
            Purchase p = (Purchase)l.get(i);
            System.out.println(p.getId().getCustomer_id() + "さんが" + p.getId().getItem_id() + "を" + p.getQuantity() + "個買いました。");
        }

        HibernateUtil.closeSession();
    }
}

・実行結果例
1さんが1を1個買いました。
1さんが2を3個買いました。
2さんが3を10個買いました。

■はまりポイントその2
次に、Purchaseクラスにmany-to-oneのrelationを追加して、顧客・商品・注文の3テーブルを結合し、顧客名と商品名も表示できるようにしたいと考えました。SQLで表現した場合は、冒頭に示した

SELECT c.customer_name, i.item_name, p.quantity FROM Customer c, Item i, Purchase p WHERE p.customer_id = c.customer_id AND p.item_id = i.item_id

となります。注文テーブルから見た場合、顧客テーブル、商品テーブルは、それぞれmany-to-oneの関係となることから、以下のように定義しました。

<class name="exam.Purchase" table="Purchase">
    <composite-id name = "id">
        <key-property name = "customer_id"/>
        <key-property name = "item_id"/>
    </composite-id>
    <property name = "quantity"/>
    <many-to-one name="customer" 
        column="customer_id"
        not-null="true"/>
    <many-to-one name="item" 
        column="item_id"
        not-null="true"/>
</class>

ところが、以下のようなエラーが発生してしまいました。

java.lang.ExceptionInInitializerError
    at exam.HibernateUtil.<clinit>(HibernateUtil.java:22)
    at exam.TestHibernate.main(TestHibernate.java:12)
Caused by: org.hibernate.MappingException: Repeated column in mapping for entity: exam.Purchase column: item_id (should be mapped with insert="false" update="false")

many-to-oneタグに「insert="false" update="false"」の属性を付与する必要があるようです。(JOINした表は更新できないからか?)
結局insert="false" update="false"属性を追加して、こんな感じに書き換えました。

・マッピング
<class name="exam.Purchase" table="Purchase">
    <composite-id name = "id">
        <key-property name = "customer_id"/>
        <key-property name = "item_id"/>
    </composite-id>
    <property name = "quantity"/>
    <many-to-one name="customer" 
        column="customer_id"
        not-null="true"
        insert="false" update="false"/>
    <many-to-one name="item" 
        column="item_id"
        not-null="true"
        insert="false" update="false"/>
</class>

・Javaソース
import java.io.Serializable;

public class Purchase
{
    // フィールド定義
    private int quantity;
    private Customer customer;
    private Item item;
    private Purchase.Id id;

    // 各フィールドのgetter, setter

    static class Id implements Serializable
    {
        private String customer_id;
        private String item_id;

        // customer_id, item_idそれぞれのgetter, setter
    }
}

・クライアントクラスのソース
package exam;

import java.util.List;

import org.hibernate.Query;
import org.hibernate.Session;

public class TestHibernate
{
    public static void main(String[] args)
    {
        Session session = HibernateUtil.currentSession();

        Query q = session.createQuery("from exam.Purchase");
        List l = q.list();

        for(int i=0; i<l.size(); i++)
        {
            Purchase p = (Purchase)l.get(i);
            System.out.println(p.getCustomer().getCustomer_name() + "さんが" + p.getItem().getItem_name() + "を" + p.getQuantity() + "個買いました。");
        }

        HibernateUtil.closeSession();
    }
}

・実行結果例
やまだ たろうさんがぱそこんを1個買いました。
やまだ たろうさんがてれびを3個買いました。
やまだ はなこさんがびでおを10個買いました。