現在來想想一個需求,如果設計變更,要為每個影片的導演增加更多資訊,因而
Dvd
類別中 String
的 director
,必須成為一個 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
宣告了 Dvd
與 Director
,這表示物件關係對應資訊,將會在這兩個類別中定義,這樣的類別稱為 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)
,表示利用資料庫本身的主鍵自動產生策略。 一個導演可能主導多個影片,因此
Dvd
與 Director
的關係是多對一,這使用 @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;
...
}
那麼那些沒有標註的欄位呢?沒有標註就會採預設值,例如
Director
的 name
欄位沒有標註,那就是對應至 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、
Dvd
、Director
的標註、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 來改善 ...