Hibernate on JPA(Java Persistence API) によるサクサク開発

HibernateJPAを使ったサンプル。利用する主なプロダクトは以下

ライブラリの入手

Hibernate

https://www.hibernate.org/ から Hibernate Core と Hibernate Annotations を入手。hibernate-distribution-3.3.2.GA の 以下をクラスパスに追加

  • hibernate3.jar
  • antlr-2.7.6.jar (lib.required内に存在)
  • dom4j-1.6.1.jar (lib.required内に存在)
  • javassist-3.9.0.GA.jar (lib.required内に存在)
  • jta-1.1.jar (lib.required内に存在)
  • commons-collections-3.1.jar (lib.required内に存在)

hibernate-annotations-3.4.0.GA の 以下をクラスパスに追加

  • hibernate-annotations.jar
  • ejb3-persistence.jar (lib内に存在)
  • hibernate-commons-annotations.jar (lib内に存在)
H2

http://www.h2database.com/html/main_ja.html から All Platforms 向けの h2-2009-08-09.zip を入手。以下をクラスパスに追加

  • h2-1.1.117.jar (bin内に存在)
TestNG

http://testng.org/doc/index.html から testng-5.10.zip を入手。以下をクラスパスに追加

Eclipse側には事前に http://beust.com/eclipse などからプラグインを組み込んでおく

slf4j

http://www.slf4j.org/から slf4j-1.5.8 を入手。以下をクラスパスに追加

  • slf4j-api-1.5.8.jar
  • slf4j-jdk14-1.5.8.jar

プロジェクトの作成

Eclipseで新規Javaプロジェクトを作成。上記で入手したライブラリにクラスパスを通す。

まずはスモールスタートで、Entityのソース作成(パッケージはetc9.domainとした)

package etc9.domain;

import java.io.Serializable;

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name="item")
public class Item implements Serializable {

    @Id
    private Integer id;
    private String name;

    public Integer getId() { return id; }
    public void setId(Integer id) { this.id = id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
}

ポイントは以下

  • @Entity にてこのクラスが Entity であることを指示
  • @Table(name="item") にてマッピング対象のテーブル名を指定
  • @Id にてキーの指定

hibernate.cfg.xmlの作成

hibernate.cfg.xmlをソースフォルダのルートに作成

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

<hibernate-configuration>
    <session-factory>
        <property name="connection.driver_class">org.h2.Driver</property>
        <property name="connection.url">jdbc:h2:tcp://localhost:9092/hibernate</property>
        <property name="connection.username">sa</property>
        <property name="connection.password"></property>
        <property name="dialect">org.hibernate.dialect.H2Dialect</property>
        
        <property name="hibernate.hbm2ddl.auto">create</property>
        <property name="hibernate.show_sql">true</property>
        
        <mapping package="etc9.domain"/>
        <mapping class="etc9.domain.Item"/>
    </session-factory>

</hibernate-configuration>

ポイントは以下

  • mapping class としてクラスを指定
  • hibernate.hbm2ddl.autoにてスキーマの自動生成を指定(後で説明)

実行用のテストケース作成

package etc9.domain;

import org.h2.tools.Server;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.cfg.AnnotationConfiguration;
import org.hibernate.cfg.Configuration;
import org.testng.annotations.AfterSuite;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.BeforeSuite;
import org.testng.annotations.Test;

import static org.testng.Assert.assertEquals;

public class ItemTest {
    
    private static final String BASE_DIR = "db\\h2";
    private static Server tcpServer;
    
    private SessionFactory sessionFactory;
    
    @BeforeSuite
    public void beforeSuite() throws Exception {
        tcpServer = org.h2.tools.Server.createTcpServer(
                new String[] { "-baseDir", BASE_DIR, "-tcpPort", "9092" }).start();
        
        Configuration config = new AnnotationConfiguration().configure();
        sessionFactory = config.buildSessionFactory();
    }

    @AfterSuite
    public void afterSuite() {
        tcpServer.shutdown();
    }
    
    @BeforeMethod
    public void beforeMethod() {
        Item item = new Item();
        item.setId(1);
        item.setName("Hibernate");
        
        Session session = sessionFactory.openSession();
        Transaction transaction = session.beginTransaction();
        session.save(item);
        transaction.commit();
        session.close();
    }
    
    @Test
    public void testItem() {
        Session session = sessionFactory.openSession();        
        Item item = (Item) session.get(Item.class, 1);
        assertEquals(item.getName(), "Hibernate");
    }
}

ポイントは以下

  • @BeforeSuite内にて H2 の起動
  • @BeforeSuite内にて Hibernate の SessionFactory をインスタンス化(この時点でスキーマ自動生成)
  • @BeforeMethod内にて DB に対してレコード挿入

実行

ItemTest をTestNGにて実行すると、BeforeSuite メソッド内にて H2 のプロセスを起動。その後、SessionFactory のインスタンスを作成
このタイミングで、hibernate.hbm2ddl.auto の create 設定により、Entity として登録してある Item に対応するテーブルを Hibernate が自動作成する。hibernate.hbm2ddl.auto に指定できる値は以下

  • validate 存在するスキーマとの検証のみ実施
  • create SessionFactoryのインスタンス作成時にdropして再作成
  • create-drop createの動作に加え、SessionFactory.close時にdrop
  • update スキーマの変更時のみ再作成

beforeMethod()にて作成されたItemテーブルにレコードを追加。testItem()ではItemテーブルからselecctしている。
DDLDMLを一切書かず、Entityクラスの修正も即座に反映され、またデータベースの起動も不要になり、サクサク開発が実施できる。

以降では、この環境を元に機能を追加していく。

IDの自動採番

Item の id は外部より設定する必要があった。これを自動採番するようにする。
Item の id フィールドにGeneratedValueを指定する

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

これで、テストケースで指定していた setId(1) が削除でき、IDは自動採番される

    @BeforeMethod
    public void beforeMethod() {
        Item item = new Item();
        // item.setId(1);
        item.setName("Hibernate");

1対1の関連

上記に続いて注文明細(OrderDetail)と商品(Item)の1対1の関連を作る。
Itemに価格のフィールドを追加。また、商品名を非nullと宣言。

package etc9.domain;

import java.io.Serializable;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name="item")
public class Item implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(nullable = false)
    private String name;
    private Integer price;
    
    public Long getId() { return id; }
    public void setId(Long id) {    this.id = id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public Integer getPrice() { return price; }
    public void setPrice(Integer price) { this.price = price; }
}


続いて注文明細

package etc9.domain;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToOne;
import javax.persistence.Table;

@Entity
@Table(name="order_detail")
public class OrderDetail {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @OneToOne
    private Item item;
    
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public Item getItem() { return item; }
    public void setItem(Item item) { this.item = item; }
}

@OneToOneアノテーションにて商品と1対1の関連を定義


hibernate.cfg.xmlにはOrderDetailのマッピング指定を追加

    <mapping class="etc9.domain.Item"/>
    <mapping class="etc9.domain.OrderDetail"/>


これで、注文明細と商品の1対1の関連が定義できた。さっそくテストケースを修正。beforeMethod()を以下のようにする。

    @BeforeMethod
    public void beforeMethod() throws Exception {
        Session session = sessionFactory.openSession();
        Transaction transaction = session.beginTransaction();

        Item item = new Item();
        item.setName("Hibernate");
        item.setPrice(1000);
        session.save(item);
        
        OrderDetail ditail = new OrderDetail();
        detail.setItem(item);
        session.save(detail);
        
        transaction.commit();
        session.close();
    }

テストメソッドは以下。

    @Test
    public void testOrderDitailItem() {
        Session session = sessionFactory.openSession();
        OrderDetail ditail = (OrderDetail)session.get(OrderDetail.class, 1L);
        assertEquals(detail.getItem().getName(), "Hibernate");
        assertEquals(detail.getItem().getPrice(), new Integer(1000));
    }

これで、OrderDetailを取得して、そこからItemを取得できる。Hibernate は OrderDetailの取得時に reft outer join のSQLを発行してItemのレコードも同時に取得している。

1対多の関連

先ほど注文明細を作成し、商品との1対1の関係を作成したので、次は注文(Order)と注文明細(OrderDetail)の1対多の関連を作る。(同値性の考慮はここではしない)

Orderは以下。

@Entity
@Table(name="order_t")
public class Order implements Serializable {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    private Date orderDate;
    
    @OneToMany(mappedBy="order")
    private Collection<OrderDetail> details = new HashSet<OrderDetail>();

    public Integer getId() { return id; }
    public void setId(Integer id) { this.id = id; }
    public Date getOrderDate() { return orderDate; }
    public void setOrderDate(Date orderDate) { this.orderDate = orderDate; }
    public Collection <OrderDetail> getDetails() { return details; }
    public void setDetails(Collection <OrderDetail> details) { this.details = details; }
}

orderというテーブルはH2で作成できないので、@Table(name="order_t")としてある。@OneToMany(mappedBy="id")

続いて、子テーブルとなるOrderDetail

@Entity
@Table(name="order_detail")
public class OrderDetail {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @OneToOne
    private Item item;
    
    @ManyToOne
    private Order order;
    
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public Item getItem() { return item; }
    public void setItem(Item item) { this.item = item; }
    public Order getOrder() { return order; }
    public void setOrder(Order order) { this.order = order; }
}

これで、order_detailテーブルに、order_idカラムが追加され、order_tテーブルの子となる。

hibernate.cfg.xmlにはOrderのマッピング指定を追加

    <mapping class="etc9.domain.Item"/>
    <mapping class="etc9.domain.OrderDetail"/>
    <mapping class="etc9.domain.Order"/>

Order側のmappedByを以下のようにすると、「ORDER_DETAIL FOREIGN KEY(ID) REFERENCES ORDER_T(ID)」のような参照制約が付与される。

    @OneToMany(mappedBy="id")
    private Collection<OrderDetail> details = new HashSet<OrderDetail>();

また、mappedByを以下のように指定しなかった場合は、ORDER_T_ORDER_DETAIL というORDER_TテーブルとORDER_DETAILテーブルの関連テーブルが作成される。

    @OneToMany
    private Collection<OrderDetail> details = new HashSet<OrderDetail>();

テストメソッドは省略。