Top > MyPage
 

Hibernateのお勉強、やりなおし(Hibernate Rebirth)

動機

最初にHibernateを勉強して、早数年が経ち、その後Perlなどの仕事が多くて、あまりさわっていない間にバージョンも3へと進化し、記憶もだいぶ曖昧になってきて、もともとわかっていなかった部分などが絡み合って、こりゃ駄目だあ!と諦め、再度勉強し直すことを、ここに宣言します。

とは言え、1から10まで復習する時間は毛頭ありません。したがって、次の観点を中心に調べたり、実験したりしたいと思います。

  • テーブルの関係、「1:1」、「1:多」、「多:多」の復習と、そのhbmの定義
  • 前項に絡むことだけど、cascadeの挙動(特に「多:多」などの場合)
  • inverseの意味をしっかりつかむ

はじめに

開発の方向性

最近使っていたときの開発の方向は

  1. DBをモデリングソフトなどで設計
  2. 前項を元にスキマーを作成し、DB・テーブルを構築
  3. DTOを作成
  4. 前項にXDOCLET用のタグを書いておき、hbmファイルを作成してもらう
  5. hibernate.cfg.xmlに増えた分のhbmを追加
  6. DAOを作成

というような流れで作成していましたが、視点を変えるためにも、方向を変えてみようと思います。つまり、

  1. hbmファイルを作成
  2. hibernate-toolsを使って、DTOを自動作成
  3. schemaexportでスキーマを作成、DB・テーブルを構築
  4. hibernate.cfg.xmlに増えた分のhbmを追加
  5. DAOを作成

という感じです。まずはHibernate用のマッピングファイルから作成しようというわけです。ただそのためには、多少環境を整える必要があります。

まずは、環境設定

  • J2EEのインストール(単にHibernateのtoolsで使うので・・・)

    http://java.sun.com/javaee/downloads/index.jspからダウンロード。

    [<li></li> is illegal in <li>][<li></li> is illegal in <li>]
    • hbm2java---hbmからDTOを作成
    • schemaexport---hbmからスキマーを作成

    を作成します。

      <target name="schemaexport" description="make tables on Database">
          <taskdef
            name="hibernatetool" 
            classname="org.hibernate.tool.ant.HibernateToolTask" 
            classpathref="build.classpath"/> 
          <hibernatetool destdir="./">
              <classpath refid="build.classpath"/>
          <annotationconfiguration
            configurationfile="WEB-INF/resources/hibernate.cfg.new.xml"/>
            <hbm2ddl drop="false"
                     create="false"
                     export="true"
                     outputfilename="schemaexport.sql"
                     delimiter=";"
                     format="true" />
          </hibernatetool>
      </target>
    
        
      <target name="hbm2java" description="hbmファイルからjava Beanを作成">
        <taskdef name="hibernatetool"
           classname="org.hibernate.tool.ant.HibernateToolTask"
           classpathref="build.classpath"
          >
        </taskdef>
        <hibernatetool destdir="src2">
          <configuration 
               configurationfile="WEB-INF/resources/hibernate.cfg.new.xml"> 
            <fileset dir="src/main/java">
              <include name="**/*.hbm.xml"/> 
            </fileset> 
          </configuration> 
          <hbm2java/>
        </hibernatetool>
      </target>
    

ここで重要なのは

  • DTOの書き出しディレクトリーを destdir="src2"として、間違って上書きされないようにしておくこと(何度泣いたことか!!)
  • configurationfileの中身から、ご丁寧にも、作って欲しくないものまで作成するので、実際の hibernate.cfg.xmlじゃなく、hibernate.cfg.new.xmlにしていた方が良いかも。
  • DTO作成時に、先走って(これに気づくのに30分以上時間を無駄にした)

        <mapping resource="com/chikkun/sample/database/Track.hbm.xml"/>
        <mapping resource="com/chikkun/sample/database/Artist.hbm.xml"/>
    

    を書き込んでおくと、

    org.hibernate.DuplicateMappingException: Duplicate collection \
        role mapping
    

    などと怒られる。DTO作成後に上記を書き込み、そしてschemaを作成すること。

    つまり、

    <?xml version="1.0"?>
    <!DOCTYPE hibernate-configuration PUBLIC
      "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
      "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
    
    <hibernate-configuration>
      <session-factory>
    <property name="dialect">org.hibernate.dialect.PostgreSQLDialect</property>
        <property name="show_sql">false</property>
    <property name="connection.driver_class">org.postgresql.Driver</property>
    <property \
        name="connection.url">jdbc:postgresql://localhost:5432/mpn</property>
        <property name="connection.username">chikkun</property>
        <property name="connection.password">kazukun</property>
      </session-factory>
    </hibernate-configuration>
    

    という、hbmが全く書かれていない状態でOK。

hbmの作成から、DTO、スキマー作成まで

DEVELOPER'S NOTEBOOKから

まずは楽曲を表すTrackテーブルとその音楽を演奏しているArtistテーブルを想定します。これは題名の「DEVELOPER'S NOTEBOOK」のmany-to-manyで使われている例をそのまま借用しています。イメージ的には次のようなER図を想定してます。

ER図

上記の図のように、今回作成しようと考えているテーブルは4つ。

  1. TRACKテーブル
    • TRACK_ID

      このフィールドでTRACK_ARTISTとリンクし、それを通じてARTISTテーブルと多:多(many-to-many)の関係を付けている

    • TITLE
    • filePath
    • playTime
    • added
    • volume
  2. TRACK_COMMENTSテーブル
    • TRACK_ID

      このフィールドでTRACKテーブルとリンク

    • COMMENT
  3. TRACK_ARTISTテーブル(関連テーブル)
    • TRACK_ID

      このフィールドでTRACKテーブルとリンク

    • ARTIST_ID

      このフィールドでARTISTテーブルとリンク

  4. ARTISTテーブル
    • ARTIST_ID

      このフィールドでTRACK_ARTISTとリンクし、それを通じてTRACKテーブルと多:多(many-to-many)の関係を付けている

    • NAME

Track.hbm.xml

というわけで、2つのhbmファイルを見ていきましょう。

<?xml version="1.0" encoding="Windows-31J"?>
<!DOCTYPE hibernate-mapping PUBLIC
    "-//Hibernate/Hibernate Mapping DTD 3.0//EN" 
    "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">

<hibernate-mapping>
  <class name="com.chikkun.sample.database.Track" table="TRACK">
    <meta attribute="class-description">
      Represents a single playable track in the music database.
      @author chikkun
    </meta>

    <id name="id" type="int" column="TRACK_ID">
      <meta attribute="scope-set">protected</meta>
      <generator class="native"/>
    </id>

    <property name="title" type="string">
      <meta attribute="use-in-tostring">true</meta>
      <column name="TITLE" not-null="true" index="TRACK_TITLE"/>
    </property>

    <property name="filePath" type="string" not-null="true"/>
    <property name="playTime" type="time">
      <meta attribute="field-description">Playing time</meta>
    </property>

    <set name="artists" table="TRACK_ARTISTS">
      <key column="TRACK_ID"/>
      <many-to-many class="com.chikkun.sample.database.Artist"
                    column="ARTIST_ID"/>
    </set>

    <set name="comments" table="TRACK_COMMENTS">
      <key column="TRACK_ID"/>
      <element column="COMMENT" type="string"/>
    </set>

    <property name="added" type="date">
      <meta attribute="field-description">
        When the track was created
      </meta>
    </property>

    <property name="volume" type="short" not-null="true">
      <meta attribute="field-description">
          How loud to play the track
      </meta>
    </property>

  </class>


</hibernate-mapping>

      

ここでのポイントは

  1. TRACKテーブルにはTRACK_IDというフィールドがあって、これをPKにする。また、クラス側のフィールド名(変数名)はidで、スコープはprotectedにする。さらに、シーケンスの作成方法はnative---DBに任せる。nativeだと、マニュアルにあるので、今回はPostgreSQLを利用するので、sequenceになるはず。
    <Class name="com.chikkun.sample.database.Track" table="TRACK">
    
    	  
    
  2. <id name="id" type="int" column="TRACK_ID">
          <meta attribute="scope-set">protected</meta>
          <generator class="native"/>
        </id>
    
    	  
    
    [<li></li> is illegal in <li>]
    1. identity supports identity columns in DB2, MySQL, MS SQL Server, Sybase and HypersonicSQL. The returned identifier is of type long, short or int.
    2. sequence

      uses a sequence in DB2, PostgreSQL, Oracle, SAP DB, McKoi or a generator in Interbase. The returned identifier is of type long, short or int

  3.     <property name="title" type="string">
          <meta attribute="use-in-tostring">true</meta>
          <column name="TITLE" not-null="true" index="TRACK_TITLE"/>
        </property>
    
    	  
    

    テーブルには文字列のTITLE、DTOにはtitleがあり、このTITLEでTRACK_TITLEというindexを作成する。

    また、DTOを作成するとtoStringにこれを入れよ、と指示。

    [<li></li> is illegal in <li>][<li></li> is illegal in <li>][<li></li> is illegal in <li>][<li></li> is illegal in <li>]
    1. one-to-manyの関係にある。
    2. 単方向の関係しかないので、双方向関連を表すone-to-many等は書き入れていない。
          <set name="comments" table="TRACK_COMMENTS">
            <key column="TRACK_ID"/>
            <element column="COMMENT" type="string"/>
          </set>
      
      		
      
    [<li></li> is illegal in <li>]
        <property name="added" type="date">
          <meta attribute="field-description">
            When the track was created
          </meta>
        </property>
    
        <property name="volume" type="short" not-null="true">
          <meta attribute="field-description">
              How loud to play the track
          </meta>
        </property>
    
    	    
    

Artist.hbm.xml

次は、ARTISTテーブル。

<?xml version="1.0" encoding="Windows-31J"?>
<!DOCTYPE hibernate-mapping PUBLIC
    "-//Hibernate/Hibernate Mapping DTD 3.0//EN" 
    "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">

<hibernate-mapping>
  <class name="com.chikkun.sample.database.Artist" table="ARTIST">
    <meta attribute="class-description">
       Represents an artist who is associated
        with a track or album.
        @author chikkun
    </meta>

    <id name="id" type="int" column="ARTIST_ID">
      <meta attribute="scope-set">protected</meta>
      <generator class="native"/>
    </id>

    <property name="name" type="string">
      <meta attribute="use-in-tostring">true</meta>
<column name="NAME" not-null="true" unique="true" index="ARTIST_NAME"/>
    </property>

    <set name="tracks" table="TRACK_ARTISTS" inverse="true">
      <meta attribute="field-description">Tracks by this artist</meta>
      <key column="ARTIST_ID"/>
<many-to-many class="com.chikkun.sample.database.Track" column="TRACK_ID"
      />
    </set>
  </class>
</hibernate-mapping>

    

これから、とりあえずDTO作成

さっきのbuild.xmlの中のtarget、「hbm2java」を実行します。再度注意点を書くと

 <target name="hbm2java" description="hbmファイルからjava Beanを作成">
    <taskdef name="hibernatetool"
       classname="org.hibernate.tool.ant.HibernateToolTask"
       classpathref="build.classpath"
      >
    </taskdef>
    <hibernatetool destdir="src2">
<configuration configurationfile="WEB-INF/resources/hibernate.cfg.new.xml">
        <fileset dir="src/main/java">
          <include name="**/*.hbm.xml"/> 
        </fileset> 
      </configuration> 
      <hbm2java/>
    </hibernatetool>
  </target>

    
  1. hibernate.cfg.xmlは、すでにある場合は別途用意すべし。
    <configuration configurationfile="WEB-INF/resources/hibernate.cfg.new.xml">
    
    	
    
  2. DTOの書き出し先をかえとかないと、もともとあるDTOが上書きされてしまうので、下記のように変えて、そこから実際のソースディレクトリーにコピーしよう。
        <hibernatetool destdir="src2">
    
      
    
      ant hbm2java  
    

と打ち込めば、2つのソースファイルがsrc2ディレクトリー以下にできているはずなので、それをソースディレクトリーにコピーしましょう。

Track.java

package com.chikkun.sample.database;

// Generated 2007/12/02 13:39:22 by Hibernate Tools 3.2.0.b9

import java.util.Date;
import java.util.HashSet;
import java.util.Set;

/**
 * Represents a single playable track in the music database.
 * 
 * @author chikkun
 */
public class Track implements java.io.Serializable {

    private int id;

    private String title;

    private String filePath;

    /**
     * Playing time
     */
    private Date playTime;

    private Set artists = new HashSet(0);

    private Set comments = new HashSet(0);

    /**
     * When the track was created
     */
    private Date added;

    /**
     * How loud to play the track
     */
    private short volume;

    public Track() {
    }

    public Track(String title, String filePath, short volume) {
        this.title = title;
        this.filePath = filePath;
        this.volume = volume;
    }

    public Track(String title, String filePath, Date playTime, Set artists,
            Set comments, Date added, short volume) {
        this.title = title;
        this.filePath = filePath;
        this.playTime = playTime;
        this.artists = artists;
        this.comments = comments;
        this.added = added;
        this.volume = volume;
    }

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

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

    public String getTitle() {
        return this.title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getFilePath() {
        return this.filePath;
    }

    public void setFilePath(String filePath) {
        this.filePath = filePath;
    }

    /**
     * * Playing time
     */
    public Date getPlayTime() {
        return this.playTime;
    }

    public void setPlayTime(Date playTime) {
        this.playTime = playTime;
    }

    public Set getArtists() {
        return this.artists;
    }

    public void setArtists(Set artists) {
        this.artists = artists;
    }

    public Set getComments() {
        return this.comments;
    }

    public void setComments(Set comments) {
        this.comments = comments;
    }

    /**
     * * When the track was created
     */
    public Date getAdded() {
        return this.added;
    }

    public void setAdded(Date added) {
        this.added = added;
    }

    /**
     * * How loud to play the track
     */
    public short getVolume() {
        return this.volume;
    }

    public void setVolume(short volume) {
        this.volume = volume;
    }

    /**
     * toString
     * 
     * @return String
     */
    public String toString() {
        StringBuffer buffer = new StringBuffer();

        buffer.append(getClass().getName()).append("@").append(
                Integer.toHexString(hashCode())).append(" [");
buffer.append("title").append("='").append(getTitle()).append("' ");
        buffer.append("]");

        return buffer.toString();
    }

}

    

Artist.java

package com.chikkun.sample.database;

// Generated 2007/12/02 13:39:22 by Hibernate Tools 3.2.0.b9

import java.util.HashSet;
import java.util.Set;

/**
 * Represents an artist who is associated with a track or album.
 * 
 * @author chikkun
 * 
 */
public class Artist implements java.io.Serializable {

    private int id;

    private String name;

    /**
     * Tracks by this artist
     */
    private Set tracks = new HashSet(0);

    public Artist() {
    }

    public Artist(String name) {
        this.name = name;
    }

    public Artist(String name, Set tracks) {
        this.name = name;
        this.tracks = tracks;
    }

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

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

    public String getName() {
        return this.name;
    }

    public void setName(String name) {
        this.name = name;
    }

    /**
     * * Tracks by this artist
     */
    public Set getTracks() {
        return this.tracks;
    }

    public void setTracks(Set tracks) {
        this.tracks = tracks;
    }

    /**
     * toString
     * 
     * @return String
     */
    public String toString() {
        StringBuffer buffer = new StringBuffer();

        buffer.append(getClass().getName()).append("@").append(
                Integer.toHexString(hashCode())).append(" [");
        buffer.append("name").append("='").append(getName()).append("' ");
        buffer.append("]");

        return buffer.toString();
    }

}

    

次はスキマーの作成

<?xml version="1.0"?>
<!DOCTYPE hibernate-configuration PUBLIC
  "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
  "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">

<hibernate-configuration>
  <session-factory>
<property name="dialect">org.hibernate.dialect.PostgreSQLDialect</property>
    <property name="show_sql">false</property>
<property name="connection.driver_class">org.postgresql.Driver</property>
<property \
    name="connection.url">jdbc:postgresql://localhost:5432/mpn</property>
    <property name="connection.username">chikkun</property>
    <property name="connection.password">kazukun</property>
  </session-factory>
</hibernate-configuration>

    

に2行書き加えて、

<?xml version="1.0"?>
<!DOCTYPE hibernate-configuration PUBLIC
  "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
  "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">

<hibernate-configuration>
  <session-factory>
<property name="dialect">org.hibernate.dialect.PostgreSQLDialect</property>
    <property name="show_sql">false</property>
<property name="connection.driver_class">org.postgresql.Driver</property>
<property \
    name="connection.url">jdbc:postgresql://localhost:5432/mpn</property>
    <property name="connection.username">chikkun</property>
    <property name="connection.password">kazukun</property>
    <mapping resource="com/chikkun/sample/database/Track.hbm.xml"/>
    <mapping resource="com/chikkun/sample/database/Artist.hbm.xml"/>
  </session-factory>
</hibernate-configuration>

    

として、

      ant schemaexport  
    

とすると、build.xmlのある場所に

schemaexport.sql

    alter table TRACK_ARTISTS 
        drop constraint FK72EFDAD8D430600D;

    alter table TRACK_ARTISTS 
        drop constraint FK72EFDAD88831A887;

    alter table TRACK_COMMENTS 
        drop constraint FK105B2688D430600D;

    drop table ARTIST;

    drop table TRACK;

    drop table TRACK_ARTISTS;

    drop table TRACK_COMMENTS;

    drop sequence hibernate_sequence;

    create table ARTIST (
        ARTIST_ID int4 not null,
        NAME varchar(255) not null unique,
        primary key (ARTIST_ID)
    );

    create table TRACK (
        TRACK_ID int4 not null,
        TITLE varchar(255) not null,
        filePath varchar(255) not null,
        playTime time,
        added date,
        volume int2 not null,
        primary key (TRACK_ID)
    );

    create table TRACK_ARTISTS (
        TRACK_ID int4 not null,
        ARTIST_ID int4 not null,
        primary key (TRACK_ID, ARTIST_ID)
    );

    create table TRACK_COMMENTS (
        TRACK_ID int4 not null,
        COMMENT varchar(255)
    );

    create index ARTIST_NAME on ARTIST (NAME);

    create index TRACK_TITLE on TRACK (TITLE);

    alter table TRACK_ARTISTS 
        add constraint FK72EFDAD8D430600D 
        foreign key (TRACK_ID) 
        references TRACK;

    alter table TRACK_ARTISTS 
        add constraint FK72EFDAD88831A887 
        foreign key (ARTIST_ID) 
        references ARTIST;

    alter table TRACK_COMMENTS 
        add constraint FK105B2688D430600D 
        foreign key (TRACK_ID) 
        references TRACK;

    create sequence hibernate_sequence;

    

ができています。これを使って(drop部分は初めてだといらないので削除してから)テーブルを作成します。

暫定的なDAOを作成

とりあえず、2つのソースを書きます。SampleBaseDAO.javaとSampleDAO.javaです。

実験用のメソッド等は後で、追加するとして、とりあえず、データを保存したりするような感棚なものだけを作成します。

SampleBaseDAO.java

package com.chikkun.sample.database;

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

/**
 * @author Administrator
 * 単にsessionをもらうメソッドを実装。
 */

/**
 * DAOベースクラス
 */
public class SampleBaseDAO {

    /** セッションファクトリ */
    private static SessionFactory sessionFactory = null;

    /**
     * セッションファクトリを取得します。 最初に呼び出されたときに、初期化処理を行います。
     */
    static synchronized SessionFactory getSessionFactory()
            throws HibernateException {
        if (sessionFactory == null) {
            Configuration cfg = new Configuration();
            cfg.configure("hibernate.cfg.xml");
            sessionFactory = cfg.buildSessionFactory();
        }
        return sessionFactory;
    }

    /**
     * セッションを取得します。
     */
    public Session getSession() throws HibernateException {
        SessionFactory sf = getSessionFactory();
        return sf.openSession();
    }

}

    

hibernate.cfg.xmlの置き場所ですが、とりあえずテストで利用するのでテストのclassesディレクトリーに置く必要があります(上記の設定では)

SampleDAO.java

package com.chikkun.sample.database;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import org.apache.log4j.Logger;
import org.hibernate.HibernateException;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.Transaction;

public class SampleDAO extends SampleBaseDAO{
    private final Logger logger = Logger.getLogger(this.getClass());
    
    public void initializeSeq(int num){
        Session session = null;
        Transaction transaction = null;
        try {
            session = getSession();
            transaction = session.beginTransaction();
Query sqlQuery = session.createSQLQuery("SELECT \
    setval('hibernate_sequence'," + num + ", false)");
            sqlQuery.list();
            transaction.commit();
        } catch (HibernateException ex) {
            if (transaction != null) {
                try {
                    transaction.rollback();
                } catch (HibernateException ignore) {
                    logger.error("trackのデータを登録できませんでした");
                }
            }
            throw new RuntimeException(ex.getMessage());
        } finally {
            if (session != null) {
                try {
                    session.close();
                } catch (HibernateException ignore) {
                }
            }
        }

    }
    
    public List findArtistByName(String name){
        Session session = null;
        List list = null;
        try {
            session = getSession();
            Query query = session
.createQuery("FROM Artist as artist where artist.name like :name");
            query.setString("name", "%" + name + "%");
            list = query.list();
        } catch (HibernateException ex) {
            throw new RuntimeException(ex.getMessage());
        } finally {
            if (session != null) {
                try {
                    session.close();
                } catch (HibernateException ignore) {
                }
            }
        }
        return list;
    }

    public List findTrackByTitle(String name){
        Session session = null;
        List list = null;
        try {
            session = getSession();
            Query query = session
.createQuery("FROM Track as track where track.title like :name");
            query.setString("name", "%" + name + "%");
            list = query.list();
        } catch (HibernateException ex) {
            throw new RuntimeException(ex.getMessage());
        } finally {
            if (session != null) {
                try {
                    session.close();
                } catch (HibernateException ignore) {
                }
            }
        }
        return list;
    }

    public Set findTrackCommentsByTitle(String name){
        Session session = null;
        List list = null;
        Set set = new HashSet();
        try {
            session = getSession();
            Query query = session
.createQuery("FROM Track as track where track.title like :name");
            query.setString("name", "%" + name + "%");
            list = query.list();
            if(list == null || list.size() == 0){
                return set;
            }
            Track track = (Track) list.get(0);
            set = track.getComments();
            set.size();//for lazy
        } catch (HibernateException ex) {
            throw new RuntimeException(ex.getMessage());
        } finally {
            if (session != null) {
                try {
                    session.close();
                } catch (HibernateException ignore) {
                }
            }
        }
        return set;
    }

    public List findTracksByArtistName(String name){
        Session session = null;
        List list = null;
        List tracks = new ArrayList();
        try {
            session = getSession();
            Query query = session
.createQuery("FROM Artist as artist where artist.name like :name");
            query.setString("name", "%" + name + "%");
            list = query.list();
            if(list == null || list.size() == 0){
                return tracks;
            }
            
            Artist art = (Artist) list.get(0);
            Set tracksSet = art.getTracks();

            for(Iterator it = tracksSet.iterator();it.hasNext();){
                Track track = (Track) it.next();
                tracks.add(track);
            }
            
        } catch (HibernateException ex) {
            throw new RuntimeException(ex.getMessage());
        } finally {
            if (session != null) {
                try {
                    session.close();
                } catch (HibernateException ignore) {
                }
            }
        }
        return tracks;
    }

    
    public void saveTrackWithArtists(Track track){
        Session session = null;
        Transaction transaction = null;
        try {
            session = getSession();
            transaction = session.beginTransaction();
            session.save(track);
            transaction.commit();
        } catch (HibernateException ex) {
            if (transaction != null) {
                try {
                    transaction.rollback();
                } catch (HibernateException ignore) {
                    logger.error("trackのデータを登録できませんでした。");
                }
            }
            throw new RuntimeException(ex.getMessage());
        } finally {
            if (session != null) {
                try {
                    session.close();
                } catch (HibernateException ignore) {
                }
            }
        }
    }
}

    

DBUnit用のエクセルファイルを作成

サンプルデータがないとテストができないので、今回はエクセルでデータを作成。

  1. sheet名がテーブル名
  2. sheetの1行目にフィールド名
  3. 一応、「DatabaseSequenceFilter」なるものを使うと、順番を考慮してくれるらしいが、通常はsheetの左側のシートから読み込んでいるらしいので、念のため親テーブルを より左側 に書いておく方が無難でしょう。

というような方法で、次のようなエクセルファイルを作成。

作成したエクセルファイル

テストプログラムを作成

まずは動作か確認を含めて、簡単なテストを作成。しっかり、エクセルからファイルが登録できているかなどのテストもかねて、テスト終了後。データをクリアしていないでいます(sqlで確認するため)。

DatabaseTestTrack

package com.chikkun.sample.database;

import java.io.File;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.Time;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.ResourceBundle;
import java.util.Set;

import junit.framework.TestCase;

import org.apache.log4j.xml.DOMConfigurator;
import org.dbunit.database.DatabaseConnection;
import org.dbunit.database.DatabaseSequenceFilter;
import org.dbunit.database.IDatabaseConnection;
import org.dbunit.dataset.FilteredDataSet;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.excel.XlsDataSet;
import org.dbunit.dataset.filter.ITableFilter;
import org.dbunit.operation.DatabaseOperation;

public class DatabaseTestTrack extends TestCase {

    private ResourceBundle resourceBundle;

    private IDatabaseConnection conn;

    public void testSeq() {
        SampleDAO dao = new SampleDAO();
        dao.initializeSeq(18);
        List list = dao.findArtistByName("John");
        Artist artist = (Artist) list.get(0);
        System.out.println("John=" + artist.getName());
        Track track = new Track();
        track.setAdded(new Date());
        track.setFilePath("/john/mother.mp3");
        track.setPlayTime(Time.valueOf("00:10:12"));
        track.setTitle("Mother");
        track.setVolume((short) 0);
        Set artists = new HashSet();
        artists.add(artist);
        track.setArtists(artists);
        // Set tracks = artist.getTracks();
        // tracks.add(track);
        // artist.setTracks(tracks);
        dao.saveTrackWithArtists(track);

        List tracks = dao.findTracksByArtistName("John");
        assertEquals("John's songs number", 4, tracks.size());

        Set comments = dao.findTrackCommentsByTitle("White");
        assertEquals("White Room's comments number", 2, comments.size());
    }

    protected void setUp() throws Exception {
        DOMConfigurator.configure("WEB-INF/log4j.xml");
        super.setUp();
        // ResourceBundleの初期化
// test用のclasses/resources/ApplicationResource.propertiesがないとエラーになる。
        // mvnを一度実行しておくとコピーされるが、注意を要する。
        resourceBundle = ResourceBundle
                .getBundle("resources.ApplicationResource");
        conn = getDBConnection();
        ITableFilter filter = new DatabaseSequenceFilter(conn);
        IDataSet dataset = new XlsDataSet(new File("test/artist.xls"));
        IDataSet fdataset = new FilteredDataSet(filter, dataset);

        DatabaseOperation.CLEAN_INSERT.execute(conn, dataset);
    }

    public final IDatabaseConnection getDBConnection() throws Exception {

        ResourceBundle resourcebundle = ResourceBundle
                .getBundle("resources.ApplicationResource");
        String url = resourcebundle.getString("system.db.url");
        String user = resourcebundle.getString("system.db.user");
        String password = resourcebundle.getString("system.db.pass");
        String driver = resourcebundle.getString("system.db.driver");

        // コネクションの取得
        Class.forName(driver);
        Connection conn = DriverManager.getConnection(url, user, password);
        IDatabaseConnection dbConn = new DatabaseConnection(conn);
        return dbConn;
    }

    private Connection getConnection() throws Exception {
        Class.forName("org.postgresql.Driver");
        String url = resourceBundle.getString("system.db.url");
        String user = resourceBundle.getString("system.db.user");
        String pass = resourceBundle.getString("system.db.pass");
        Connection con = DriverManager.getConnection(url, user, pass);
        return con;
    }

}

    

実験を、ようやく、開始

とりあえず、一通り実験できる状況になったので、すこしずつ実験を開始しようと思います。現状の設定のポイントは

  1. TrackとArtistは TRACK_ARTISTという関連テーブルによって many-to-mayで関係づけられている。
  2. TrackとTRACK_COMMENTSがString型のCOMMENTSというフィールドの値を複数持つと設定されている。

    ※他のone-to-manyの場合とどこが違うかというと、いちいちDTOのクラスを介しておらず、バリュー型(というのかな)で関係づけており、単に値を参照したいような場合は、これで十分のよう。

lazyの確認

エクセルのデータから、Trackを取得し、それからArtistを参照してみましょう。

まずはDAOは以下を使用する。

SampleDAO

    public List findTrackByTitle(String name){
        Session session = null;
        List list = null;
        try {
            session = getSession();
            Query query = session
.createQuery("FROM Track as track where track.title like :name");
            query.setString("name", "%" + name + "%");
            list = query.list();
        } catch (HibernateException ex) {
            throw new RuntimeException(ex.getMessage());
        } finally {
            if (session != null) {
                try {
                    session.close();
                } catch (HibernateException ignore) {
                }
            }
        }
        return list;
    }

    

次に、テストでは

DatabaseTestTrack

    public void testLazy(){
        List searched = dao.findTrackByTitle("White Room");
        assertEquals("White Roomの数", 1,searched.size());
        //lazy指定していないけど
        Track track = (Track) searched.get(0);
        Set artists = track.getArtists();
        assertEquals("White Roomを演奏しているアーティスト", 3,artists.size());
    }

    

何も指定しないと、lazyはfalseになっているはずなんだが、なぜか、次のような例外が起こって、テストは途中で止まってしまう。

org.hibernate.LazyInitializationException: 
failed to lazily initialize a collection of role:
com.chikkun.sample.database.Track.artists, no session or session was closed

    

なので、これをTrack.hbm.xmlに

Track.hbm.xml

    <set name="artists" table="TRACK_ARTISTS" lazy="false">
      <key column="TRACK_ID"/>
<many-to-many class="com.chikkun.sample.database.Artist" \
    column="ARTIST_ID"/>
    </set>

    

とlazy="true"を書き入れると、すんなりテストが通ります。じゃあ、ということで反対から行くと・・・DAOのメソッドは

SampleDAO

    public List findArtistByName(String name){
        Session session = null;
        List list = null;
        try {
            session = getSession();
            Query query = session
.createQuery("FROM Artist as artist where artist.name like :name");
            query.setString("name", "%" + name + "%");
            list = query.list();
        } catch (HibernateException ex) {
            throw new RuntimeException(ex.getMessage());
        } finally {
            if (session != null) {
                try {
                    session.close();
                } catch (HibernateException ignore) {
                }
            }
        }
        return list;
    }

    

で、テストは

    public void testLazy2(){
        List searched = dao.findArtistByName("Jack");
        assertEquals("Jack Bruceの数", 1,searched.size());
        //lazy指定していないけど
        Artist artist = (Artist) searched.get(0);
        Set tracks = artist.getTracks();
        assertEquals("Jack Bruceが演奏しているTrackの数", 1,tracks.size());
    }

    

です。これも最後のアサートで、例外が起こります。例外は

org.hibernate.LazyInitializationException: 
failed to lazily initialize a collection of role:
com.chikkun.sample.database.Track.artists, no session or session was closed

    

で、前回と同じ。つまり、これも

Artist.hbm.xml

    <set name="tracks" table="TRACK_ARTISTS" inverse="true" lazy="false">
      <meta attribute="field-description">Tracks by this artist</meta>
      <key column="ARTIST_ID"/>
<many-to-many class="com.chikkun.sample.database.Track" column="TRACK_ID"
      />
    </set>

    

とやると、やはりすんなり、テストが通ります。

つまり、少なくとも「many-to-may」では、デフォルトはlazy="true"になっているようなので、要注意ですな。

cascade

次は、cascadeの指定による振る舞いを研究。まずはデフォルトのままArtistを削除してみよう。DAOに削除するメソッドを追加して、

SampleDAO

    public final void deleteArtist(final Artist artist) {
        Session session = null;
        Transaction transaction = null;
        try {
            session = getSession();
            transaction = session.beginTransaction();
            session.delete(artist);
            transaction.commit();
        } catch (HibernateException ex) {
            if (transaction != null) {
                try {
                    transaction.rollback();
                } catch (HibernateException ignore) {
                    logger.error("artistの削除がきませんでした。");
                }
            }
                throw new RuntimeException(ex.getMessage());
        } finally {
            if (session != null) {
                try {
                    session.close();
                } catch (HibernateException ignore) {
                }
            }
        }
    }

    

一応、削除後のArtistの数を数えたいので、これも追加。

SampleDAO

    public final List findAllArtists(){
        Session session = null;
        List list = null;
        try {
            session = getSession();
            Query query = session
            .createQuery("FROM Artist as artist order by order.name");
            list = query.list();
        } catch (HibernateException ex) {
            throw new RuntimeException(ex.getMessage());
        } finally {
            if (session != null) {
                try {
                    session.close();
                } catch (HibernateException ignore) {
                }
            }
        }
        return list;

    }

    

そんで、テストでは

DatabaseTestTrack

    public void testCascade1(){
        List searched = dao.findArtistByName("Jack");
        assertEquals("Jack Bruceの数", 1,searched.size());
        //lazy指定していないけど
        Artist artist = (Artist) searched.get(0);
        dao.deleteArtist(artist);
        List artists = dao.findAllArtists();
        assertEquals("1人削除した後の数", 12,artists.size());
    }

    

このテストを実行すると、次のような例外が発生する。

2007-12-05 10:23:37,758,JDBCExceptionReporter.java,ERROR: update or delete on table "artist" violates foreign key constraint "fk72efdad88831a887" on table "track_artists" Detail: Key (artist_id)=(10) is still referenced from table "track_artists".

これは、hbmが

Arttist.hbm.xml