Java Tutorial 第四堂(3)Hibernate 與 JPA



現在來想想一個需求,如果設計變更,要為每個影片的導演增加更多資訊,因而 Dvd 類別中 Stringdirector,必須成為一個 Director 型態:
public class Dvd {
    private String title;
    private Integer year;
    private Integer duration;

    private Director director;
    ...
}

Director
中將會包括名稱等資訊:
public class Director {
    private String name;
    ...
}

那麼你的 DvdDaoJdbcImpl 將得因為這個需求變化而修改程式了。如果隨著後續的程式開發,這類需求不斷增加,可能會導致這類修改不斷發生,而且你會逐漸感受到物件導向與關聯式資料庫因為模型不匹配導致的種種問題,像是物件導向的繼承如何在關聯式資料庫中對應,多型查詢如何實現等問題。

Java 的世界中對這類物件關聯對應(Object-Relational Mapping, ORM)當然不缺解決方案,最有名的方案之一就是 Hibernate,2001年末 Hibernate 第一個版本發表,2003 年 6  月 8 日 Hibernate 2 發表,並於年末獲得 Jolt 2004 大獎,由於 Hibernate 廣為流行,設計方式後續影響了 EJB3 中 JPA(Java Persistence API) 規格之製定。

使用 Hibernate 這類的 ORM 方案,基本上需要宣告物件與關聯式資料庫的對應關係,後續操作就是從物件的觀點來進行操作,Hibernate 會自動為你產生對應的 SQL 語句。

hibernate.cfg.xml

對應關係宣告的第一步,就是宣告資料庫組態資訊,這是在 hibernate.cfg.xml 中設定:
<?xml version="1.0" encoding="UTF-8"?>
<!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="hibernate.connection.driver_class">org.hsqldb.jdbc.JDBCDriver</property>
    <property name="hibernate.connection.url">jdbc:hsqldb:file:src/main/resources/db/dvd_library</property>
    <property name="hibernate.connection.username">codedata</property>
    <property name="hibernate.connection.password">123456</property>
    <property name="hibernate.dialect">org.hibernate.dialect.HSQLDialect</property>
    <property name="hibernate.hbm2ddl.auto">create</property>
    <property name="show_sql">true</property>
    <property name="format_sql">true</property>

    <mapping class="tw.codedata.Dvd" />
    <mapping class="tw.codedata.Director" />
    </session-factory>
</hibernate-configuration>

hibernate.dialect
宣告了想使用的資料庫 SQL 方言,hibernate.hbm2ddl.auto 設定為 create,表示每次都重建資料庫,這在開發測試時有用,讓你不用手動進行這項工作。show_sql 表示執行時顯示 Hibernate 自動產生的 SQL 語句,format_sql 表示是否將這些產生的 SQL 語句排版一下,以利觀看。

Entity 宣告

在 hibernate.cfg.xml 中可看到 mapping class 宣告了 DvdDirector,這表示物件關係對應資訊,將會在這兩個類別中定義,這樣的類別稱為 Entity 類別。Dvd 的宣告如下:
package tw.codedata;

import javax.persistence.*;

@Entity
@Table(name="dvds")
public class Dvd {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    private String title;
    private Integer year;
    private Integer duration;

    @ManyToOne(cascade=CascadeType.ALL)
    @JoinColumn(name="director_id")
    private Director director;
    ...
}

作為 Entity 的類別,必須使用 @Entity 標註,如果類別名稱與表格名稱不同,可以使用 @Table 標註表格名稱資訊,每個 Entity 類別必須有獨一無二的識別屬性,並與資料表格的主鍵相對應,使用 @Id 標註,在這邊還標註了 @GeneratedValue(strategy = GenerationType.AUTO),表示利用資料庫本身的主鍵自動產生策略。

一個導演可能主導多個影片,因此 DvdDirector 的關係是多對一,這使用 @ManyToOne 標註,cascade=CascadeType.ALL 表示聯級操作,設定為無論儲存、合併、 更新或移除,一併對被參考物件作出對應動作。@JoinColumn(name="director_id") 設定了資料庫外鍵的欄位名稱為 director_id

類似地,Director 也必須進行相關標註:
package tw.codedata;

import javax.persistence.*;

@Entity
@Table(name="directors")
public class Director {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    private String name;
...
}

那麼那些沒有標註的欄位呢?沒有標註就會採預設值,例如 Directorname 欄位沒有標註,那就是對應至 directors 表格的 name 欄位,如果欄位名稱不同,可以使用 @Column 標註。

注意到 import 時是 javax.persistence,如前所述,Hibernate 影響了 JPA 的規格制定,原本 Hibernate 的相關標註是置放於 org.hibernate.annotations 之中,javax.persistence 有許多標註與之對應,由於 JPA 已是標準,標註時鼓勵你使用 javax.persistence 中的相關標註,實際上你使用最新版的 Hibernate,若使用了 org.hibernate.annotations 中的標註,就會發現它們已不再建議使用(Deprecated)。

SessionFactory

你可以使用 Configuration 讀取 hibernate.cfg.xml、建立 SessionFactory 物件,後者用來建立Session 物件,負責資料庫操作過程的功能。不過這個過程,老實說,很麻煩,因此建立一個 HibernateUtil 類別會比較方便一些:
package tw.codedata;

import org.hibernate.cfg.Configuration;
import org.hibernate.SessionFactory;
import org.hibernate.boot.registry.StandardServiceRegistry;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;

public class HibernateUtil {

    private static final SessionFactory sessionFactory;
    private static final StandardServiceRegistry serviceRegistry;

    static {
        try {
            Configuration configuration = new Configuration();
            configuration.configure();

            serviceRegistry = new StandardServiceRegistryBuilder()
                               .applySettings(configuration.getProperties()).build();
            sessionFactory = configuration.buildSessionFactory(serviceRegistry);
        } catch (Throwable ex) {
            throw new ExceptionInInitializerError(ex);
        }
    }

    public static SessionFactory getSessionFactory() {
        return sessionFactory;
    }

    public static void closeAllResources() {
        sessionFactory.close();
        StandardServiceRegistryBuilder.destroy(serviceRegistry);
    }
}

老實說,這個過程是蠻醜陋的,因而,在你將來有時間深入 Hibernate 之前,故且當這是個魔法好了 ...

儲存與查詢

有了 SessionFactory 實例之後,你就可以來個簡單的儲存過程了:
Session session = sessionFactory.openSession();
session.beginTransaction(); // 開啟交易
session.save(dvd); // 儲存 Dvd 實體
session.getTransaction().commit(); // 確認變更
session.close(); // 關閉此次操作過程

查詢的話,有幾種方式,像是搭配 HQL(Hibernate Query Language):
Session session = sessionFactory.openSession();
session.beginTransaction();
List dvds = session.createQuery("from Dvd").list();

session.getTransaction().commit();
session.close();

注意!"from Dvd" 不是 SQL,而是 HQL,讀起來是「從 Dvd 實體物件中查詢」,而不是「從 Dvd 表格中查詢」,記得嗎?Dvd 中有個 Director,Hibernate 會自動查詢並封裝為 Director 實例。

練習 13:使用 Hibernate


在 Lab 檔案的 exercises/exercise13 中有個 Hibernate 目錄,這是改寫自 Java Tutorial 第三堂(2)使用 spring-jdbc 存取資料庫 的範例,其中 build.gradle 已經幫你寫好了:
apply plugin: 'java'
apply plugin:'application'
mainClassName = 'tw.codedata.Main'

repositories {
    mavenCentral()
}

dependencies {
    compile 'org.hsqldb:hsqldb:2.3.1'
    compile group: 'com.google.guava', name: 'guava', version: '15.0'
    compile 'org.hibernate:hibernate-core:4.3.0.Final'
}

hibernate.cfg.xml、DvdDirector 的標註、HibernateUtil 基本上就是上頭描述過的,也都已經先為你撰寫好了。那你要做什麼?觀察剛描述過的幾個檔案之位置與內容,然後建一個 DirectorDaoHibernateImpl 內容如下:
package tw.codedata;

import com.google.common.base.Optional;
import java.util.List;
import org.hibernate.*;

public class DirectorDaoHibernateImpl implements DirectorDao {
    private SessionFactory sessionFactory;

    public DirectorDaoHibernateImpl(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

    @Override
    public void saveDirector(Director director) {
        Session session = sessionFactory.openSession();
        session.beginTransaction();
        session.save(director);
        session.getTransaction().commit();
        session.close();
    }

    @Override
    public Optional maybeFromName(String name) {
        Session session = sessionFactory.openSession();
        session.beginTransaction();

        List directors = 
                          session
                          .createQuery("from Director as d where d.name = :name")
                          .setString("name", name).list();

        session.getTransaction().commit();
        session.close();

        return directors.isEmpty() ? Optional.absent() :  Optional.of(directors.get(0));

    }
}

這個 DAO 負責 Director 的儲存與查詢,接著建一個 DvdDaoHibernateImpl 如下:
package tw.codedata;

import java.util.List;
import org.hibernate.Session;
import org.hibernate.SessionFactory;

public class DvdDaoHibernateImpl implements DvdDao {
    private SessionFactory sessionFactory;

    public DvdDaoHibernateImpl(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

    @Override
    public void saveDvd(Dvd dvd) {
        Session session = sessionFactory.openSession();
        session.beginTransaction();
        session.save(dvd);
        session.getTransaction().commit();
        session.close();
    }

    @Override
    public List allDvds() {
        Session session = sessionFactory.openSession();
        session.beginTransaction();

         List dvds = session.createQuery("from Dvd").list();

        session.getTransaction().commit();
        session.close();

        return dvds;
    }
}

觀察一下 Main 的內容:
package tw.codedata;

import org.hibernate.*;

public class Main {
    public static void main(String[] args) {

        SessionFactory factory = HibernateUtil.getSessionFactory();

        DirectorDao directorDao = new DirectorDaoHibernateImpl(factory);
        DvdDao dvdDao = new DvdDaoHibernateImpl(factory);

        Director director = new Director("XD");
        directorDao.saveDirector(director);

        dvdDao.saveDvd(new Dvd("XD", 112, 1, director));

        for(Dvd dvd : dvdDao.allDvds()) {
           System.out.println(dvd);
        }

        HibernateUtil.closeAllResources();       
    }
}

接著執行 gradle run,觀察一下,你會看到什麼輸出結果。在這個練習中,你有感覺到哪邊不太方便嗎?下一篇文章,會使用 spring-orm 來改善 ...