面向 Java 開發人員的 db4o 指南

 第一部分 簡介和概覽

    早就聽說數據庫間的大戰以關係型數據庫的勝利告終。然而,這之後,編程界真的就風平浪靜、一片繁榮了嗎?持肯定觀點的人恐怕最近都沒有試過使用關係數據庫來支持 Java™ 對象吧。著名作家和講師 Ted Neward 爲我們帶來了這個由多個部分組成的 系列 ,深入介紹了 db4o,它是當前關係型數據庫的一種面向對象的可選方案。

在我出道成爲程 序員的時候,數據庫之戰似乎已完全平息。Oracle 和其他幾個數據庫供應商都非常支持和看好關係模型及其標準查詢語言 SQL。實際上,坦率地講,我從未將任何關係數據庫的直接祖先,比如 IMS 或無處不在的平面文件,用於長期存儲。客戶機/服務器看起來似乎長久不衰。

之 後,忽然有一天,我發現了 C++。正像許多在這個特別的時刻發現了這個特別的語言的其他人一樣,它改變了我的整個編程 “世界觀”。我的編程模型從基於函數和數據的變成了基於對象的。一時間,再也聽不到開發人員大談構建優雅的數據結構和 “信息隱藏” 了,現在,我們更熱衷於多態封裝繼承 —— 一整套新的熱門字眼。

關於本系列
信息存儲和檢索作爲同義語伴隨 RDBMS 已經 10 來年了,但現在情況有所改變。Java 開發人員尤其厭倦於所謂的對象關係型阻抗失配,也對試圖解決這個問題失去了耐心。再加上可行的替代方案的出現,就導致了人們對對象持久性和檢索的興趣的復甦。 本系列 是對開放源碼數據庫 db4o 的詳盡介紹,db4o 可以充分利用當前的面向對象的語言、系統和理念。要下載 db4o,可以參考 db4o 主頁;爲了實踐本系列的示例,需要下載 db4o。

與此同時,關係數據庫已風光不再,一種新的數據庫 —— 對象數據庫 —— 成爲了人們的新寵。若能再結合一種面向對象的語言,例如 C++ (或與之類似的編程新貴,Java 編程),OODBMS 真是可以堪稱編程的理想王國。

但 是,事情的發展並非如此。OODBMS 在 90 年代晚期達到了頂峯,隨後就一直在走下坡路。原來的輝煌早已退去,剩下的只有晦澀和侷限。在第二輪的數據庫之戰結束之時,關係數據庫又成了贏家。(雖然大 多數 RDBMS 供應商都或多或少地採用了對象,但這不影響大局。)

上述情況中存在的惟一問題是,開發人員對 OODBMS 的熱衷一直沒有衰退,db4o 的出現就很好地說明了這一點。

對象和關係

對象關係型阻抗失配 這個話題完全可以拿出來進行學術討論,但簡單說來,其本質是:對象系統與關係系統在如何處理實體之間的互動方面所採取的方式是截然不同的。表面上看,對象系統和關係系統彼此非常合適,但若深入研究,就會發現二者存在本質差異。

首先,對象具有身份的隱式性質(其表徵是隱藏/隱式的 this 指針或引用,它實際上是內存的一個位置),而關係則具有身份的顯式性質(其表徵是組成關係屬性的主鍵)。其次,關係數據庫通過隱藏數據庫範圍內的數據查詢 和其他操作的實現進行封裝,而對象則在每個對象上實現新的行爲(當然,模塊所實現的繼承都在類定義中進行指定)。另外,可能也是最有趣的是,關係模型是個 封閉的模型,其中任何操作的結果都將產生一個元組集,適合作爲另一個操作的輸入。這就使嵌套的 SELECT 以及很多其他功能成爲可能。而對象模型則無此能力,尤其是向調用程序返回 “部分對象” 這一點。對象是要麼全有要麼全無的,其結果就是:與 RDBMS 不同,OODBMS 不能從表或一組表返回任一、全部或部分列。

簡言之,對象(用像 Java 代碼、C++ 和 C# 這類語言實現)和關係(由像 SQLServer、Oracle 和 DB/2 這樣的現代 RDBMS 實現)操作的方式有極大的差異。對於減少這種差異,程序員責無旁貸。






映射的作用

過 去,開發人員曾試圖減少對象和關係間的這種差距,嘗試過的方式之一是手動映射,比如通過 JDBC 編寫 SQL 語句並將結果收集進字段。對這種方式的一個合理的質疑是:是否還有更簡化的方法來進行處理。開發人員大都用自動的對象關係映射實用工具或庫(比如 Hibernate)來解決這個問題。

即使是通過 Hibernate(或 JPA、JDO、Castor JDO、Toplink 或任何可用的其他 ORM 工具),映射問題也無法徹底解決,它們只會轉移到配置文件。而且,這種方式與要解決的問題頗有些風馬牛不相及。比方說,如果您想要創建一個分層良好的繼承 模型,將它映射到表或一組表無疑是失敗之舉。若用對常規形式的違背來換取查詢的性能,就會將 DBA 與開發人員在某種程度上對立起來。

可問題是很難構建一個富域模型(參見 Martin Fowler 和 Eric Evans 各自所著的書),不管是您以後想要調整它來匹配現有的數據庫模式,還是想要調整數據庫執行其操作的功能來支持對象模型(甚或這兩者)。

但如果能不調整,豈不是更好?






進入 db4o:OODBMS 的迴歸

db4o 庫是最近纔出現在 OODBMS 領域的,它使 “純對象存儲” 的概念在新一代對象開發人員中重獲新生。(他們笑稱,現在不是很流行懷舊麼。)爲了讓您對如何使用 db4o 有一個概念,特給出如下代表單個人的一個基本類:

注意:如果還尚未下載,請現在就 下載 db4o。爲了更好地進行討論(或至少編譯代碼),db4o 是必需的,本系列的後續文章也會用到它。


清單 1. Person 類
                package com.tedneward.model;

public class Person
{
public Person()
{ }
public Person(String firstName, String lastName, int age)
{
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}

public String getFirstName() { return firstName; }
public void setFirstName(String value) { firstName = value; }

public String getLastName() { return lastName; }
public void setLastName(String value) { lastName = value; }

public int getAge() { return age; }
public void setAge(int value) { age = value; }

public String toString()
{
return
"[Person: " +
"firstName = " + firstName + " " +
"lastName = " + lastName + " " +
"age = " + age +
"]";
}

public boolean equals(Object rhs)
{
if (rhs == this)
return true;

if (!(rhs instanceof Person))
return false;

Person other = (Person)rhs;
return (this.firstName.equals(other.firstName) &&
this.lastName.equals(other.lastName) &&
this.age == other.age);
}

private String firstName;
private String lastName;
private int age;
}

在衆多的類中,Person 類顯得極爲尋常;還很簡單。但若深入探究,就不難看出這個類會呈現出非常類似於對象的有趣屬性和功能,例如它可以有配偶類,也可以有子類,等等。(我在後續的專欄中會歷數這些屬性和功能;現在,我只側重於進行概括介紹。)

在基於 Hibernate 的系統中,將這個 Person 類的一個實例放入數據庫,需要如下幾個步驟:

  1. 需要創建關係模式,向數據庫描述類型。
  2. 需要創建映射文件,用這些文件將列和數據庫的表映射到域模型的類和字段。
  3. 在代碼中,需要通過 Hibernate 打開到數據庫的連接(用 Hibernate 術語來說,就是會話),並與 Hibernate API 進行交互來存儲對象和將對象取回。

上述操作在 db4o 中出奇地簡單,如清單 2 所示:


清單 2. 在 db4o 內運行 INSERT
                import com.db4o.*;

import com.tedneward.model.*;

public class Hellodb4o
{
public static void main(String[] args)
throws Exception
{
ObjectContainer db = null;
try
{
db = Db4o.openFile("persons.data");

Person brian = new Person("Brian", "Goetz", 39);

db.set(brian);
db.commit();
}
finally
{
if (db != null)
db.close();
}
}
}

這樣就行了。無需生成模式文件,無需創建映射配置,需要做的只是運行客戶機程序,當運行結束時,爲存儲在 persons.data 中的新 “數據庫” 檢查本地目錄。

檢索所存儲的 Person 在某些方面非常類似於某些對象關係型映射庫的操作方式,原因是對象檢索最簡單的形式就是按例查詢(query-by-example)。只需爲 db4o 提供相同類型的一個原型對象,該對象的字段設置爲想要按其查詢的值,這樣一來,就會返回匹配該條件的一組對象,如清單 3 所示:


清單 3. 在 db4o 內運行 INSERT(版本 1)
                import com.db4o.*;

import com.tedneward.model.*;

public class Hellodb4o
{
public static void main(String[] args)
throws Exception
{
ObjectContainer db = null;
try
{
db = Db4o.openFile("persons.data");

Person brian = new Person("Brian", "Goetz", 39);
Person jason = new Person("Jason", "Hunter", 35);
Person clinton = new Person("Brian", "Sletten", 38);
Person david = new Person("David", "Geary", 55);
Person glenn = new Person("Glenn", "Vanderberg", 40);
Person neal = new Person("Neal", "Ford", 39);

db.set(brian);
db.set(jason);
db.set(clinton);
db.set(david);
db.set(glenn);
db.set(neal);

db.commit();

// Find all the Brians
ObjectSet brians = db.get(new Person("Brian", null, 0));
while (brians.hasNext())
System.out.println(brians.next());
}
finally
{
if (db != null)
db.close();
}
}
}

運行上述代碼,會看到檢索出兩個對象。






但是...

在您將我定義爲狂熱的推崇者之前,請允許我先列舉幾條反對 db4o 的言論。

db4o 幾乎無法對應於現有的 Oracle、SQLServer 或 DB2!
完 全正確。相比之下,db4o 更適合於 MySQL 或 HSQL,這使其對於大量項目來說已經足夠。更爲重要的是,開銷一直是 db4o 開發人員特別關注的事情,這又讓它特別適於小型的嵌入式環境。(我這裏給出的示例只是一個簡單的演示,和其他任何小型的演示一樣,請務必清楚一點,即姑且 不論它相比其他工具具有多少潛力,db4o 都會比您在這裏所看到的要強大許多。)

我不能用 JDBC 在 db4o 上進行查詢!
確 實如此,儘管 db4o 團隊曾想過創建 JDBC 驅動程序以讓對象數據庫能接受 SQL 語法,即一種所謂的 “關係對象映射”。(開發團隊之所以沒有這麼做,據說是因爲沒有這個必要,而且性能會因此大受影響。)問題的關鍵是,您在實現中使用的是對象 (POJO),除此之外別無他物。若不存儲關係,爲何還要使用 SQL 呢?

但是我的其他程序如何獲得數據呢?
這要看具體情況。如果您這裏所謂的 “其他程序” 指的是其他 Java 代碼,那麼只需在這些程序內使用 Person 類的定義並將其傳遞進 ObjectContainer,正如我這裏所做的一樣。在 OODBMS 內,類定義本身就充當模式,所以無需其他的工具來獲取 Person 對象。但是如果您這裏所謂的 “其他程序” 指的是其他語言的代碼,那麼問題就會複雜一些。

對於 db4o 不支持的語言,比如 C++ 或 Python,數據基本上是訪問不到的,除非是您能從 Java 代碼構建程序。db4o 適用於 C# 和其他的 .NET 語言,而其數據格式在二者之間是兼容的,這就使得 Java 對象對同樣定義的 .NET 類也可用。如果您這裏所謂的 “其他程序” 指的是使用 SQL 和標準調用級接口(比如 ODBC 或 JDBC)來與數據庫進行交互的報告工具,那麼 db4o(或與此相關的任何 OODBMS)可能未必是很好的選擇。機警的讀者會發現報告功能現在對 OODBMS 還不可用,但好消息是:已針對此問題發起很多產品和項目,而且 db4o 還支持 “複製(Replication)”,它允許 db4o 實例將數據從其自身的存儲格式複製進 RDBMS。

但它是個文件!
在 這種特殊情況下,確實如此;但正如前面所講,db4o 在何處和如何存儲數據方面十分靈活,而且還提供了一個輕量級的客戶機/服務器選項。如果您所期待的是功能完善的 RDBMS 所能提供的冗餘性,那麼 db4o 並不能如您所願(但其他的 OODBMS 能提供這類特性)。

但當我再次運行示例時,我會得到副本!(實際上,我每次運行該示例都會得到副本。)
這裏,實際上回到了我們所討論的第一個有趣的問題:身份,正是這一點將對象數據庫和關係數據庫區分開來。正如我前面所言,對象系統中的身份是通過隱式的 “this” 引用賦予的,Java 對象使用這個引用在內存中標識其自身。在對象數據庫中,它被稱爲 OID對象標識符),該 OID 在 OODBMS 中充當主鍵。

當創建新對象並將其 “放” 進數據庫中時,新對象並不具有與其相關的 OID,因而會收到其自身惟一的 OID 值。它會複製,就如同 RDBMS 在執行每個 INSERT 操作時生成主鍵所做的那樣(比如 順序計數器或自動增量字段)。換言之,就主鍵而言,OODBMS 與 RDBMS 相當接近,但主鍵本身並非傳統 RDBMS(或過去習慣使用 RDBMS 的程序員)認爲的那種主鍵。

換言之,db4o 旨在解決某些方面的問題,而不是成爲解決全部持久性問題的一站式通用解決方案。實際上,這讓 db4o 首輪就戰勝了 OODBMS:db4o 無意向那些生產 IT 人員宣稱自己是多麼好的一種理念,以至於完全可以放棄他們在關係數據庫上的投資。





結束語

傳 統的集中關係型數據庫作爲數據存儲和操縱的首選工具的地位在短期內無法撼動。以這種數據庫爲基礎發展起來的工具非常之多,歷史也很久遠,而且許多程序員也 都陷在 “我們總需要數據庫” 的思維模式之中,這些無疑加固了其地位。實際上,db4o 在技術上的設計和定位並不是爲了挑戰 RDBMS 的這一地位。

但 “面向服務” 社區迫切要求我們構建鬆散耦合的多層世界,當您開始將 OODBMS 放到這種環境中去審視的時候,有趣的現象就出現了:如果要實現組件(服務、層或諸如此類的東西)間真正的鬆散耦合,那麼結果常常是某種程度的耦合只存在於 服務的調用程序和該服務的公開 API(或 XML 類型,不管您如何看待它)之間。無需數據類型,無需公開對象模型,無需共享數據庫 —— 本質上講,持久性方法僅僅是一個實現細節。因此,在大量場景中可用的持久性方法的範圍會顯著擴大。


第二部分 查詢,更新和一致性

    儘管 RDBMS 使用 SQL 作爲其查詢和檢索數據的主要機制,但是 OODBMS 可以使用一些不同的機制。在本系列的第二期文章中,Ted Neward 將介紹一些新方法,包括 Query by Example 以及定製只有 OODBMS 才具有的機制。正如他解釋的一樣,有些替代方法比 SQL 本身更易於使用。

本系列的第一篇文章 中,我討論了 RDBMS 作爲 Java™ 對象存儲解決方案的不足之處。正如我所說的,在當今的面向對象世界裏,與關係數據庫相比,db4o 這樣的對象數據庫可以爲面向對象開發人員提供更多的功能。

在本文及以後的文章中,我將繼續介紹對象數據庫。我將使用示例來演示這種存儲系統的強大之處,它儘可能實現與面向對象編程語言中(本例中爲 Java 編程語言)使用的實體形式相同。特別是,我將介紹用於檢索、修改並將對象重新存儲到 db4o 的各種可用機制。正如您將瞭解的一樣,當您從 SQL 的限制解脫出來後,會對自己能夠完成這麼多的事情而感到喫驚。

如果您還沒有下載 db4o,可能希望 立即下載。您需要使用它來編譯示例。

Query by Example

Query by Example(QBE)是一種數據庫查詢語言,它允許您通過設計模板(對其進行比較)來創建查詢,而不是通過使用謂詞條件的語言(如 SQL)。上一次我使用了 db4o 的 QBE 引擎演示了數據檢索,這裏將快速回顧一下。首先看一下這個絕對簡單的數據庫。它由一種類型組成,清單 1 列出了其定義:


清單 1. Person 類
                package com.tedneward.model;

public class Person
{
public Person()
{ }
public Person(String firstName, String lastName, int age)
{
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}

public String getFirstName() { return firstName; }
public void setFirstName(String value) { firstName = value; }

public String getLastName() { return lastName; }
public void setLastName(String value) { lastName = value; }

public int getAge() { return age; }
public void setAge(int value) { age = value; }

public String toString()
{
return
"[Person: " +
"firstName = " + firstName + " " +
"lastName = " + lastName + " " +
"age = " + age +
"]";
}

public boolean equals(Object rhs)
{
if (rhs == this)
return true;

if (!(rhs instanceof Person))
return false;

Person other = (Person)rhs;
return (this.firstName.equals(other.firstName) &&
this.lastName.equals(other.lastName) &&
this.age == other.age);
}

private String firstName;
private String lastName;
private int age;
}

與 POJO 類似,Person 並不是一個複雜的類。它由三個字段和一些基本的支持類似 POJO 行爲的方法組成,即 toString()equals()。(閱讀過 Joshua Bloch 的 Effective Java 的讀者將注意到我忽略了 hashCode() 實現,很明顯這違背了 Rule 8。作者經常使用的典型說法就是,我將 hashCode() 留給 “讀者進行練習”,這通常意味着作者不想解釋或認爲沒有必要提供手頭的示例。我同樣將它留給讀者作爲練習,請自行判斷我們這裏的練習屬於哪種情況。

在清單 2 中,我創建了 6 個對象,將它們放入了一個文件中,然後使用 QBE 調用名字匹配 “Brian” 模式的兩個對象。這種查詢使用原型對象(被傳入到 get() 調用的對象)來確定對象是否匹配數據庫查詢,並返回匹配條件的對象的 ObjectSet(實際上是一個集合)。


清單 2. Query by Example
                import com.db4o.*;

import com.tedneward.model.*;

public class Hellodb4o
{
public static void main(String[] args)
throws Exception
{
ObjectContainer db = null;
try
{
db = Db4o.openFile("persons.data");

Person brian = new Person("Brian", "Goetz", 39);
Person jason = new Person("Jason", "Hunter", 35);
Person clinton = new Person("Brian", "Sletten", 38);
Person david = new Person("David", "Geary", 55);
Person glenn = new Person("Glenn", "Vanderberg", 40);
Person neal = new Person("Neal", "Ford", 39);

db.set(brian);
db.set(jason);
db.set(clinton);
db.set(david);
db.set(glenn);
db.set(neal);

db.commit();

// Find all the Brians
ObjectSet brians = db.get(new Person("Brian", null, 0));
while (brians.hasNext())
System.out.println(brians.next());
}
finally
{
if (db != null)
db.close();
}
}
}

查詢規則

由於 QBE 使用原型對象作爲其模板來搜索數據,關於其用法有一些簡單的規則。當 db4o 針對給定的目標(概念正確但實際實現進行了簡化)搜索所有 Person 類型的對象時,要確定數據存儲中的某個對象是否滿足條件,需要逐個比較字段值。如果原型中的字段值爲 “null”,則該值匹配數據存儲中的任何值;否則的話,必須精確地匹配值。對於原語類型,由於它不能真正具有 “null” 值,所以使用 0 作爲通配符值。(這同樣指出了 QBE 方法的一個缺點 —— 不能夠有效地使用 0 作爲搜索值)。應該指定多個字段值,所有字段的值都應該被數據庫中的對象滿足,從而使候選對象滿足查詢條件;實際上,這意味着將字段使用 “AND” 連接起來形成查詢謂詞。

在前面的示例中,查詢所有 firstName 字段等於 “Brian” 的 Person 類型,並且有效地忽略 lastNameage 字段。在表中,這個調用基本上相當於 SQL 查詢的 SELECT * FROM Person WHERE firstName = "Brian"。(雖然如此,在嘗試將 OODBMS 查詢映射到 SQL 時還是要謹慎一些:這種類比並不完善,並且會對特定查詢的性質和性能產生誤解)。

查詢返回的對象是一個 ObjectSet,它類似於一個 JDBC ResultSet(一個簡單的對象容器)。使用由 ObjectSetIterator 接口遍歷結果非常簡單。使用 Person 的特定方法需要對 next() 返回的對象進行向下轉換。 實現的





更新和一致性

雖然簡單的顯示數據只和數據本身有關,大多數對象需要進行修改並重新存入數據庫中。這可能是使用 OODBMS 最棘手的部分,因爲對象數據庫使用與關係數據庫不同的一致性概念。實際上,這意味着在使用對象數據庫時,必須更加謹慎地比較內存中的對象和存儲中的對象。

清單 3 所示的簡單示例演示了這種不同的一致性概念:


清單 3. 三個 Brian
                import com.db4o.*;

import com.tedneward.model.*;

public class Hellodb4o
{
public static void main(String[] args)
throws Exception
{
ObjectContainer db = null;
try
{
db = Db4o.openFile("persons.data");

Person brian = new Person("Brian", "Goetz", 39);
Person jason = new Person("Jason", "Hunter", 35);
Person clinton = new Person("Brian", "Sletten", 38);
Person david = new Person("David", "Geary", 55);
Person glenn = new Person("Glenn", "Vanderberg", 40);
Person neal = new Person("Neal", "Ford", 39);

db.set(brian);
db.set(jason);
db.set(clinton);
db.set(david);
db.set(glenn);
db.set(neal);

db.commit();

// Find all the Brians
ObjectSet brians = db.get(new Person("Brian", null, 0));
while (brians.hasNext())
System.out.println(brians.next());

Person brian2 = new Person("Brian", "Goetz", 39);
db.set(brian2);
db.commit();

// Find all the Brians
ObjectSet brians = db.get(new Person("Brian", null, 0));
while (brians.hasNext())
System.out.println(brians.next());
}
finally
{
if (db != null)
db.close();
}
}
}

當運行清單 3 中的查詢時,數據庫將報告三個 Brian,其中兩個是 Brian Goetz。(如果 persons.data 文件已經存在於當前的目錄,則會出現類似的結果 —— 創建的所有 Person 將被存儲到 persons.data 文件,而查詢將返回存儲在其中的所有 Brian)。

擴展接口
db4o 開發團隊偶然情況下發現某些 API 被使用得較少,或指出對在團隊還不確定的 API 上進行的 “實驗” 應該成爲核心 ObjectContainer API 的一部分。在這種情況下,ext() 方法返回的 ExtObjectContainer 實例提供了方法。各版本中這個類的可用方法不盡相同,因爲它們被引入、刪除或移入了核心 ObjectContainer 類本身。這個列表包括了測試內存中對象的方法,以查看對象是否和 db4o 容器實例相關聯,列出了容器可識別的所有類,或者設置/釋放併發的信號量。並且始終查看 db4o 文檔中有關 ExtObjectContainer 類的完整信息。

很明顯,這裏並不強制使用關於主鍵的舊規則,那麼對象數據庫如何處理惟一性概念?

採納 OID

當對象被存儲到對象數據庫中,將創建一個惟一鍵,稱爲 Object identifierOID(其發音類似於 avoid 的最後一個音節),它惟一地標識對象。OID,和 C# 和 Java 編程中的 this 指針/引用類似,除非顯式指定,否則則是隱式的。在 db4o 中,可以通過調用 db.ext().getID() 查找給定對象的 OID。(還可以使用 db.ext().getByID() 方法按照 OID 檢索對象。調用該方法具有一些非常複雜的含義,不便在這裏討論,但是它仍然是一種方法)。

在實踐中,所有這些意味着由開發人員判斷是否一個對象曾經存在於系統中,通常在插入對象前通過查詢該對象的容器實現,如清單 4 所示:


清單 4. 插入前進行查詢
                // ... as before
ObjectContainer db = null;
try
{
db = Db4o.openFile("persons.data");

...

// We want to add Brian Goetz to the database; is he already there?
if (db.get(new Person("Brian", "Goetz", 0).hasNext() == false)
{
// Nope, no Brian Goetz here, go ahead and add him
db.set(new Person("Brian", "Goetz", 39));
db.commit();
}
}

在這個特定例子中,假設系統中 Person 的惟一性是其姓名的組合。因此,當在數據庫中搜索 Brian 時,只需要對 Person 實例查找這些屬性。(或許幾年前已經添加過 Brain —— 當時他還不到 39 歲。)

如果希望修改數據庫中的對象,那麼從容器中檢索對象,使用某種方式進行修改,然後將其存儲回數據庫即可,如圖 5 所示:


清單 5. 更新對象
                // ... as before
ObjectContainer db = null;
try
{
db = Db4o.openFile("persons.data");

...

// Happy Birthday, David Geary!
if ((ObjectSet set = db.get(new Person("David", "Geary", 0))).hasNext())
{
Person davidG = (Person)set.next();
davidG.setAge(davidG.getAge() + 1);
db.set(davidG);
db.commit();
}
else
throw new MissingPersonsException(
"David Geary doesn't seem to be in the database");
}

db4o 容器在這裏並沒有出現一致性問題,這是因爲有問題的對象已經被標識爲來自數據庫的對象,即它的 OID 已經被存儲在 db4o bookkeeping 基礎設施中。相應地,當調用 set 時,db4o 將會更新現有的對象而不是插入新對象。





一種搜索實用方法

特 定於應用程序主鍵的概念值得經常注意,即使它沒有繼承 QBE 的概念。您所需要的是使用一種實用方法簡化基於標識的搜索。這一節將展示基於 Reflection API 用法的解決方案,我們會將正確的值放在正確的字段,此外,還介紹了針對不同選擇和外觀對解決方案進行調優的方法。

讓我們從一個基本前提開始:我具有一個數據庫,其中包含我希望根據一組具有特定值的字段查詢的類型(Person)。在這種方法中,我對 Class 使用了 Reflection API,創建了該類型的新實例(調用其默認構造方法)。然後遍歷具有這些字段的 String 數組,取回 Class 中的每個 Field 對象。隨後,遍歷對應於每個字段值的對象數組,然後調用 Field.set() 將該值放入我的模板對象中。

完成這些操作後,對 db4o 數據庫調用 get() 並查看返回的 ObjectSet 是否包含任何對象。這給出了一個基本方法的大概輪廓,如清單 6 所示:


清單 6. 執行 QBE 一致性搜索的實用方法
                import java.lang.reflect.*;
import com.db4o.*;

public class Util
{
public static boolean identitySearch(ObjectContainer db, Class type,
String[] fields, Object[] values)
throws InstantiationException, IllegalAccessException,
NoSuchFieldException
{
// Create an instance of our type
Object template = type.newInstance();

// Populate its fields with the passed-in template values
for (int i=0; i<fields.length; i++)
{
Field f = type.getDeclaredField(fields[i]);
if (f == null)
throw new IllegalArgumentException("Field " + fields[i] +
" not found on type " + type);
if (Modifier.isStatic(f.getModifiers()))
throw new IllegalArgumentException("Field " + fields[i] +
" is a static field and cannot be used in a QBE query");
f.setAccessible(true);
f.set(template, values[i]);
}

// Do the query
ObjectSet set = db.get(template);
if (set.hasNext())
return true;
else
return false;
}
}

很明顯,要對這種方法進行大量的調優以進行嘗試,例如捕獲所有的異常類型並將它們作爲運行時異常重新拋出,或者返回 ObjectSet 本身(而非 true/false),甚至返回包含 ObjectSet 內容的數組對象(ObjectSet 的內容使得查看返回數組的長度非常簡單)。然而,可以從清單 7 中很明顯地看到,這種用法並沒有比基本的 QBE 版本簡單多少。


清單 7. 可以工作的實用方法
                // Is Brian already in the database?
if (Util.identitySearch(
db, Person.class, {"firstName", "lastName"}, {"Brian", "Goetz"}) == false)
{
db.set(new Person("Brian", "Goetz", 39));
db.commit();
}

事實上,對於存儲的類本身,這種實用方法的實用性 開始變得明顯,如清單 8 所示:


清單 8. 在 Person 內使用實用方法
                public class Person
{
// ... as before

public static boolean exists(ObjectContainer db, Person instance)
{
return (Util.identitySearch(db, Person.class,
{"firstName", "lastName"},
{instance.getFirstName(), instance.getLastName()});
}
}

或者,您可以調整該方法來返回找到的實例,這樣 Person 實例使它的 OID 正確地關聯,等等。關鍵要記住可以在 db4o 基礎架構之上構建方便的方法,從而使 db4o 更加易於使用。

注意:使用 db4o SODA 查詢 API 對存儲在磁盤的底層對象執行這類查詢是一種更有效的方法,但這稍微超出了本文討論的範圍,所以我將在以後討論這些內容。





高級查詢

目前爲止,您已經瞭解瞭如何查詢單個的或者滿足特定條件的對象。儘管這使得查詢非常簡單,但同時也有一些限制:比如,如果需要檢索所有姓氏以 G 開頭的 Person,或者所有年齡大於 21 的 Person,該怎麼辦?QBE 方法對於這類查詢無能爲力,因爲 QBE 只能執行相等匹配,而無法進行比較查詢。

通常,即使是中等複雜程度的比較對於 OODBMS 也是一種弱點,而這正是關係模型和 SQL 的長處。在 SQL 中執行比較查詢非常簡單,但是在 OODBMS 中執行同樣的查詢卻需要一些不是很吸引人的方法:

  • 獲取所有對象並自行執行關係比較。
  • 擴展 QBE API 以包含謂詞。
  • 創建一種能夠被轉換爲查詢您的對象模型的查詢語言。

薄弱的比較查詢

很 明顯,上面所述的第一種方法只能用於最普通的數據庫,因爲它對能夠在實際中使用的數據庫的規模有很明顯的上限。取回一百萬個對象不成問題,甚至可以很輕鬆 地處理最困難的硬件,尤其是當跨越網絡連接時。(這不是對 OODBMS 的控告,順便提一下,通過網絡連接獲取一百萬行可能仍在 RDBMS 服務器能力之內,但是這將摧毀它所在的網絡。)

第二種方法破壞了 QBE 方法的簡單性,並且導致瞭如清單 9 所示的糟糕代碼:


清單 9. 使用了謂詞的 QBE 調用
                Query q = new Query();
q.setClass(Person.class);
q.setPredicate(new Predicate(
new And(
new Equals(new Field("firstName"), "David"),
new GreaterThan(new Field("age"), 21)
)));
q.Execute();

很容易看出,使用這種技術使得中等複雜的查詢很快就變得不能工作,尤其是與 SQL 這類查詢語言的簡單性相比。

第三種方法是創建能夠用來查詢數據庫對象模型的查詢語言。過去,OODBMS 開發人員創建了一種標準的查詢語言,對象查詢語言(Object Query Language),或 OQL,這種語言類似於清單 10 顯示的內容:


清單 10. OQL 片段
                SELECT p FROM Person
WHERE p.firstName = "David" AND p.age > 21

表面上看,OQL 非常類似於 SQL,因此它應該和 SQL 一樣強大並且易於使用。OQL 的缺點就是它要求返回……什麼?類似於 SQL 的語言要求返回列集(元組),與 SQL 相同,但是對象數據庫不會以這種方式工作 —— 它希望返回對象,而不是隨機集。尤其是在強類型語言中,如 C# 或 Java 編程語言,這些對象類型必須是先驗 的,而與 SQL 那種基於集合的概念不同。





db4o 中的原生查詢

db4o 沒有強制開發人員使用複雜的查詢 API,也沒有引入新的 “-QL” 之類的東西,它提供了一個名爲原生查詢可以 使用 SODA 形式,這種形式主要用於細粒度查詢控制。然而,正如在第二篇看到的一樣,SODA 通常只用於手動優化查詢。 的工具,該工具功能強大且易用,如清單 11 所示。(db4o 的查詢 API


清單 11. db4o 原生查詢
                // ... as before
ObjectContainer db = null;
try
{
db = Db4o.openFile("persons.data");

...

// Who wants to get a beer?
List<Person> drinkers = db.query(new Predicate<Person>() {
public boolean match(Person candidate) {
return person.getAge() > 21;
}
}
for (Person drinker : drinkers)
System.out.println("Here's your beer, " + person.getFirstName());
}

之所以說查詢是 “原生” 的,是因爲它是使用編程語言本身編寫的(本例爲 Java 語言),而不是必須要轉換爲其他內容的任意語言。(Predicate API 的非通用版本可用於 Java 5 之前的版本,儘管它使用起來不是很簡便。)

考慮一下這一點,您很可能想知道如何精確地實現這種特殊的方法。必須使用源預處理程序將源文件及其包含的查詢轉換爲數據庫引擎能夠理解的內容(a la SQL/J 或其他嵌入的預處理程序),或者數據庫將所有的 Person 對象發回給對全部集合執行謂詞的客戶機(換言之,正是早先拒絕使用的方法)

結果證明,db4o 並沒有執行任何這些操作;相反,db4o 的基本原理採用有趣並創新的方法進行原生查詢。db4o 系統將謂詞發送到數據庫,在運行時對 match() 的字節碼執行字節碼分析。如果字節碼簡單到可以理解的話,db4o 將該查詢轉換爲 SODA 查詢以提高效率,這種情況下不需要對傳送到 match() 方法的所有對象進行實例化。使用這種方法,程序員可以繼續使用他們覺得方便的語言編寫查詢,而查詢本身可以被轉換爲數據庫能夠理解並有效執行的內容。(如 果願意的話,可以稱之爲 “JQL”—— Java Query Language,不過請不要向 db4o 開發人員轉述這個名字,這會給我帶來麻煩)。

務必包含 BLOAT!
db4o Java 版包含了一些 jar 文件,包括一個用於 JDK 1.1、JDK 1.2 和 Java 5 版本的核心 db4o 實現。該版本還包括了一個名爲 BLOAT 的 jar 文件。不管其名字如何,這是一個必須具備的 Java 字節碼優化器(由 Purdue University 開發),它在運行時類路徑中結合 db4o-5.0-nqopt.jar 實現原生查詢。沒有包含這些庫並不會生成任何錯誤,但是會使所有原生查詢都無法優化。(開發人員可以找出這個問題,但只能是被動發現,即使用本節所述的監 聽器。)

讓 db4o 告訴您……

原生查詢方法並不是完美的。比如,編寫一個足夠複雜的原生查詢來超越字節碼分析器是完全不可能的,因此需要最壞情況的執行模型。在這種最壞情況的場景中,db4o 必須實例化數據庫中查詢類型的每一個對象,並通過 match() 實現傳送每個對象。可以預料到,這將有損查詢性能,不過可以在需要的位置安裝監聽器來解決這一問題。

預見錯誤並進行優化,直覺並不總是夠用,因爲代碼暗示的原因完全不同。比如,包含一個控制檯打印語句(Java 代碼中的 System.out.println,或者 C# 中的 System.Console.WriteLine)將使 db4o 的 .NET 版本中的優化器發生錯誤,而 Java 版本則能夠對該語句優化。您不能夠真正預見這種類型的變化(儘管可以通過經驗瞭解這種變化),所以,最好讓系統告訴您發生的錯誤,正如在極限編程中一樣。

簡單地對 ObjectContainer 本身註冊一個監聽器(Db4oQueryExecutionListener),如果原生查詢不能進行優化時將通知您,如清單 12 所示:


清單 12. DiagnosticListener
                // ... as before
ObjectContainer db = null;
try
{
db = Db4o.openFile("persons.data");

db.ext().configure().diagnostic().addListener(new DiagnosticListener() {
public void onDiagnostic(Diagnostic d) {
if (d instanceof NativeQueryNotOptimized)
{
// could display information here, but for simplicity
// let's just fail loudly
throw new RuntimeException("Native query failed optimization!");
}
}
});
}

很明顯,只有在開發過程中這樣做纔是理想的 —— 在運行時最好將這個錯誤記錄到 log4j 錯誤流中,或者記錄到不會影響用戶的類似內容中。





結束語

面向 Java 開發人員的 db4o 指南 的第二篇文章中,我使用 OODBMS 的一致性概念作爲起點,解釋了 db4o 如何存儲和檢索對象,並簡單介紹了它的原生查詢工具。

QBE 是進行簡單查詢的首選機制,因爲它是一種更加易於使用的 API,但是它要求您的域對象允許任何或所有包含數據的字段被設置爲 null,這將有悖於一些域規則。比如說,如果能夠對 Person 對象執行姓和名字的查詢將非常好。然而,在 QBE 查詢中使用 Person 查詢名字,要求姓氏可以是 null,這實際上意味着我們必須選擇域約束或者查詢能力,而這兩者都不能被完全接受。

原生查詢爲執行復雜查詢提供了一種功能強大的方法,而且不需要學習新的查詢語言或使用複雜對象結構對謂詞建 模。對於 db4o 原生查詢不能夠滿足需要的情況,SODA API(對於任何對象系統,最初以獨立的查詢系統出現,而且仍然存在於 SourceForge 中)允許對查詢進行最細緻的調優,其代價就是破壞了簡單性。

這種查詢數據庫方法的多面性可能讓您備受挫折,它非常複雜,容易造成混淆,並且與 RDBMS 工作方式完全不同。事實上,這並不是問題:多數大型數據庫將 SQL 文本轉換爲字節碼格式(對這種格式進行分析和優化),執行存儲在磁盤的數據,彙編迴文本,然後返回。db4o 原生查詢方法將編譯放到了字節碼之後,由 Java(或 C#)編譯器來處理,因此保證了類型安全並對錯誤查詢語法進行早期檢測。(很不幸的是,JDBC 的訪問 SQL 的方法丟失了類型安全,因爲這是一個調用級別的接口,因此它限制只有在運行時才能檢查字符串。對於任何 CLI 都是一樣的,而不僅僅是 JDBC;ODBC 和 .NET 的 ADO.NET 也同樣受此限制)。在數據庫內部仍然執行了優化,但是並不是返回文本,而是返回真正的對象,以供使用。這與 SQL/Hibernate 或其他 ORM 方法形成了顯著對比,Esther Dyson 對此做了很好的描述,如下所示:

利用表格存儲對象,就像是將汽車開回家,然後拆成零件放進車庫裏,早晨可以再把汽車裝配起來。但是人們不禁要問,這是不是泊車的最有效的方法呢。

第三部分 db4o 中的數據庫重構

    重構 Java™ 代碼遠遠比重構關係數據庫簡單,但幸運的是,對於對象數據庫卻並非如此。在本期的面向 Java 開發人員的 db4o 指南 中,Ted Neward 介紹他喜歡的對象數據庫的另一個優點:db4o 簡化了重構,使之變得非常容易。

本系列的上一篇文章 中,我談到了查詢 RDBMS 與查詢像 db4o 這樣的對象數據庫的不同之處。正如我所說的那樣,與通常的關係數據庫相比, db4o 可以提供更多的方法來進行查詢,爲您處理不同應用程序場景提供了更多選擇。

這一次,我將繼續這一主題 —— db4o 的衆多選項 —— 看看 db4o 如何處理重構。自 6.1 版開始,db4o 能自動識別和處理三種不同類型的重構:添加字段、刪除字段和添加一個類的新接口。我不會討論所有這三種重構(我將着重介紹添加字段和更改類名),但我將介紹 db4o 中的重構最令人興奮的內容 —— 將向後兼容和向前兼容引入到數據庫變更管理中。

您將看到,db4o 能夠靜默地處理更新,並確保代碼與磁盤的一致性,這大大減輕了重構系統中持久性存儲的壓力。這樣的靈活性也使得 db4o 非常適合於測試驅動開發過程。

現實中的重構

上個月,我談到了使用原生和 QBE 樣式的查詢來查詢 db4o。在上次討論中,我建議運行示例代碼的讀者刪除包含之前運行結果的已有數據庫文件。這是爲了避免由於一致性概念在 OODBMS 中與在關係理論中的不同而導致的 “怪異” 結果。

這 種變通辦法對於我的例子是適用的,但是也提出了現實中存在的一個有趣的問題。當定義其中所存儲的對象的代碼發生改變時,OODBMS 會怎樣?在一個 RDBMS 中,“存儲” 與 “對象” 之間的聯繫很清晰:RDBMS 遵從在使用數據庫之前執行的 DDL 語句所定義的一種關係模式。然後,Java 代碼要麼使用手寫的 JDBC 處理代碼將查詢結果映射到 Java 對象,要麼通過 Hibernate 之類的庫或新的 Java Persistence API (JPA) “自動” 完成映射。不管通過何種方式,這種映射是顯式的,每當發生重構時,都必須作出修改。

從理論上講,理論與實踐之間沒有不同。但也只是從理論上才能這麼講。重構關係數據庫和對象/關係映射文件應該 是簡單的。但在實際中,只有當重構僅僅與 Java 代碼有關時,RDBMS 重構才比較簡單。在這種情況下,只需更改映射就可以完成重構。但是,如果更改發生在數據的關係存儲本身上,那麼就突然進入一個全新的、複雜的領域,這個專 題複雜到足夠寫一本書。(我的一個同事曾經描述到 “500 頁數據庫表、觸發器和視圖” 這樣的一本書。) 可以說,由於現實中的 RDBMS 常常包含需要保留的數據,僅僅刪除模式然後通過 DDL 語句重新構建模式不是 正確的選擇。

現在我們知道,當定義其中的對象的 Java 代碼發生改變時 RDBMS 會發生什麼樣的變化。(或者,至少我們知道 RDBMS 管理器會怎樣,這是一個很頭痛的問題。)現在我們來看看當代碼發生改變時 db4o 數據庫的反應。





設置數據庫

如果您已經閱讀了本系列中的前兩篇文章,那麼應該熟悉我的非常簡單的數據庫。目前,它由一種類型組成,即 Person 類型,該類型的定義包含在清單 1 中:


清單 1. Person
                package com.tedneward.model;

public class Person
{
public Person()
{ }
public Person(String firstName, String lastName, int age)
{
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}

public String getFirstName() { return firstName; }
public void setFirstName(String value) { firstName = value; }

public String getLastName() { return lastName; }
public void setLastName(String value) { lastName = value; }

public int getAge() { return age; }
public void setAge(int value) { age = value; }

public String toString()
{
return
"[Person: " +
"firstName = " + firstName + " " +
"lastName = " + lastName + " " +
"age = " + age +
"]";
}

public boolean equals(Object rhs)
{
if (rhs == this)
return true;

if (!(rhs instanceof Person))
return false;

Person other = (Person)rhs;
return (this.firstName.equals(other.firstName) &&
this.lastName.equals(other.lastName) &&
this.age == other.age);
}

private String firstName;
private String lastName;
private int age;
}

接下來,我填充數據庫,如清單 2 所示:


清單 2. ‘t0’ 時的數據庫
                import java.io.*;
import java.lang.reflect.*;
import com.db4o.*;
import com.tedneward.model.*;

// Version 1
public class BuildV1
{
public static void main(String[] args)
throws Exception
{
new File(".", "persons.data").delete();

ObjectContainer db = null;
try
{
db = Db4o.openFile("persons.data");

Person brianG = new Person("Brian", "Goetz", 39);
Person jason = new Person("Jason", "Hunter", 35);
Person brianS = new Person("Brian", "Sletten", 38);
Person david = new Person("David", "Geary", 55);
Person glenn = new Person("Glenn", "Vanderberg", 40);
Person neal = new Person("Neal", "Ford", 39);
Person clinton = new Person("Clinton", "Begin", 19);

db.set(brianG);
db.set(jason);
db.set(brianS);
db.set(david);
db.set(glenn);
db.set(neal);
db.set(clinton);

db.commit();

// Find all the Brians
ObjectSet brians = db.get(new Person("Brian", null, 0));
while (brians.hasNext())
System.out.println(brians.next());
}
finally
{
if (db != null)
db.close();
}
}
}

注意,在清單 2 中開始的代碼片段中,我顯式地刪除了文件 “persons.data”。這樣做可以保證一開始就有整潔的記錄。在 Build 應用程序將來的版本中,爲了演示重構過程,我將保持 persons.data 文件不動。還需注意,Person 類型將來要發生改變(這將是我的重構的重點),所以請務必熟悉爲每個例子存儲和/或取出的版本。(請在 本文的源代碼 中查看每個版本的 Person 中的註釋,以及源代碼樹中的 Person.java.svn 文件。這些內容有助於理解例子。)





第一次重構

到目前爲止,公司一直運營良好。公司數據庫填滿了 Person,可以隨時查詢、存儲和使用,基本上可以滿足每個人的需求。但公司高層讀了一本最近非常暢銷的叫做 People have feelings too! 的管理書籍之後,決定修改該數據庫,以包括 Person 的情緒(mood)。

在傳統的對象/關係場景中,這意味着兩個主要任務:一是重構代碼(下面我會對此進行討論),二是重構數據庫模式,以包括反映 Person 情緒的新數據。現在,Scott Ambler 已經有了一些 RDBMS 重構方面的很好的資源(見 參考資料),但重構關係數據庫遠比重構 Java 代碼複雜這一事實絲毫沒有改變,而在必須保留已有生產數據的情況下,這一點特別明顯。

然而,在 OODBMS 中,事情就變得簡單多了,因爲重構完全發生在代碼(在這裏就是 Java 代碼)中。需要記住的是,在 OODBMS 中,代碼就是 模式。因此可以說, OODBMS 提供一個 “單一數據源(single source of truth)”,而不是 O/R 世界,即數據被編碼在兩個不同的位置:數據庫模式和對象模型。(兩者發生衝突時哪一方 “勝出”,這是 Java 開發人員中爭論得很多的一個話題。

重構數據庫模式

我的第一步是創建一個新類型,用於定義要跟蹤的所有情緒。使用一個 Java 5 枚舉類型就很容易做到這一點,如清單 3 所示:


清單 3. Howyadoin'?(你好嗎?)
                package com.tedneward.model;

public enum Mood
{
HAPPY, CONTENT, BLAH, CRANKY, DEPRESSED, PSYCHOTIC, WRITING_AN_ARTICLE
}

第二步,我需要更改 Person 代碼,添加一個字段和一些用於跟蹤情緒的屬性方法,如清單 4 所示:


清單 4. No, howYOUdoin'?(不,你好嗎?)
                package com.tedneward.model;

// Person v2
public class Person
{
// ... as before, with appropriate modifications to public constructor and
// toString() method

public Mood getMood() { return mood; }
public void setMood(Mood value) { mood = value; }

private Mood mood;
}

檢查 db4o

在做其它事情之前,我們先來看看 db4o 對查找數據庫中所有 Brian 的查詢如何作出響應。換句話說,當數據庫中沒有存儲 Mood 實例時,如果在數據庫上運行一個基於已有的 Person 的查詢,db4o 將如何作出響應(見清單 5)?


清單 5. 每個人都還好嗎?
                import com.db4o.*;
import com.tedneward.model.*;

// Version 2
public class ReadV2
{
public static void main(String[] args)
throws Exception
{
// Note the absence of the File.delete() call

ObjectContainer db = null;
try
{
db = Db4o.openFile("persons.data");

// Find all the Brians
ObjectSet brians = db.get(new Person("Brian", null, 0, null));
while (brians.hasNext())
System.out.println(brians.next());
}
finally
{
if (db != null)
db.close();
}
}
}

結果有些令人喫驚,如清單 6 所示:


清單 6. db4o 應付自如
                
[Person: firstName = Brian lastName = Sletten age = 38 mood = null]
[Person: firstName = Brian lastName = Goetz age = 39 mood = null]

Person 的兩個定義(一個在磁盤上,另一個在代碼中)並不一致的事實面前,db4o 不但沒有 卡殼,而且更進了一步:它查看磁盤上的數據,確定那裏的 Person 實例沒有 mood 字段,並靜默地用默認值 null 替代。(順便說一句,在這種情況下,Java Object Serialization API 也是這樣做的。)

這 裏最重要的一點是,db4o 靜默地處理它看到的磁盤上的數據與類型定義之間的不匹配。這成爲貫穿 db4o 重構行爲的永恆主題:db4o 儘可能靜默地處理版本失配。它或者擴展磁盤上的元素以包括添加的字段,或者,如果給定 JVM 中使用的類定義中不存在這些字段,則忽略它們。





代碼到磁盤兼容性

db4o 對磁盤上缺失的或不必要的字段進行某種調整,這一思想需要解釋一下,所以讓我們看看當更新磁盤上的數據以包括情緒時會出現什麼情況,如清單 7 所示:


清單 7. 我們很好
                import com.db4o.*;
import com.tedneward.model.*;

// Version 2
public class BuildV2
{
public static void main(String[] args)
throws Exception
{
ObjectContainer db = null;
try
{
db = Db4o.openFile("persons.data");

// Find all the Persons, and give them moods
ObjectSet people = db.get(Person.class);
while (people.hasNext())
{
Person person = (Person)people.next();

System.out.print("Setting " + person.getFirstName() + "'s mood ");
int moodVal = (int)(Math.random() * Mood.values().length);
person.setMood(Mood.values()[moodVal]);
System.out.println("to " + person.getMood());
db.set(person);
}

db.commit();
}
finally
{
if (db != null)
db.close();
}
}
}

在清單 7 中,我發現數據庫中的所有 Person,並隨機地爲他們賦予 Mood。在更現實的應用程序中,我會使用一組基準數據,而不是隨即選擇數據,但對於這個例子而言這樣做也行。運行上述代碼後產生清單 8 所示的輸出:


清單 8. 今天每個人的情緒如何?
                Setting Brian's mood to BLAH
Setting David's mood to WRITING_AN_ARTICLE
Setting Brian's mood to CONTENT
Setting Jason's mood to PSYCHOTIC
Setting Glenn's mood to BLAH
Setting Neal's mood to HAPPY
Setting Clinton's mood to DEPRESSED

可以通過再次運行 ReadV2 驗證該輸出。最好是再運行一下初始的查詢版本 ReadV1(該版本看上去很像 ReadV2,只是它是在 V1 版本的 Person 的基礎上編譯的)。 運行之後,產生如下輸出:


清單 9. 舊版的 ‘今天每個人的情緒如何?’
                
[Person: firstName = Brian lastName = Sletten age = 38]
[Person: firstName = Brian lastName = Goetz age = 39]

對於清單 9 中的輸出,值得注意的是,與我將 Mood 擴展添加到 Person 類(在 清單 6 中)之前 db4o 的輸出相比,這裏的輸出並無不同 —— 這意味着 db4o 是同時向後兼容和向前兼容的。





再度重構!

假設要更改已有類中一個字段的類型,例如將 Person 的 age 從整型改爲短整型。(畢竟,通常沒有人能活過 32,000 歲 —— 而且我相信,即使真的 有那麼長壽的人,仍然可以重構代碼,將該字段改回整型。)假定兩種類型在本質上是類似的,就像 int 與 short,或 float 與 double,db4o 只是靜默地處理更改 —— 同樣是或多或少仿效 Java Object Serialization API。這種操作的缺點是,db4o 可能會意外地將一個值截尾。只有當一個值 “轉換爲範圍更小的類型” 時,即這個值超出了新類型允許的範圍,例如試圖從 long 類型轉換爲 int 類型,纔會發生這樣的情況。貨物出門概不退貨,買主需自行小心 —— 在開發期間或原型開發期間,務必進行徹底的單元測試。

db4o 保存舊的數據嗎?
如果刪除一個字段,然後再添加那個字段,db4o 竟然可以找回那個字段當初存在時原有的值。不,db4o 不會永遠跟蹤所有被刪除的字段的值 —— 當數據庫被請求執行所謂的碎片整理操作時,它就會刪除那些值。本系列將來的文章中會更詳細地談到碎片整理,請繼續關注。

實際上,db4o 向後兼容的妙法值得解釋一下。基本上,當 db4o 看到新類型的字段時,就會在磁盤上創建一個新字段,該字段有相同的名稱,但是具有新的類型,就好像它是添加到類中的另一個新字段一樣。這還意味着,舊的值 仍然保留在舊類型的字段中。因此,通過將字段重構回初始值,總可以 “回調” 舊值,取決於觀察問題的角度,這可以說是一個特性,也可以說是一個 bug。

注意,對類中方法的更改與 db4o 無關,因爲它不將方法或方法實現作爲存儲的對象數據的一部分,對於構造函數的重構也是如此。只有字段和類名本身(接下來會進行討論)對於 db4o 纔是重要的。





第三次重構比較困難

在某些情況下,需要發生的重構可能更劇烈一些,例如整個更改一個類的名稱(可以是類名,也可以是類所在的包的名稱)。像這樣的更改對於 db4o 是比較大的更改,因爲它需要根據 classname 來存儲對象。例如,當 db4o 查找 Person 實例時,它在標有名稱 com.tedneward.model.Person 的塊的特定區域中進行查找。因此,改變名稱會使 db4o 不知所措:它不能魔術般地推斷 com.tedneward.model.Person 現在就是 com.tedneward.persons.model.Individual。幸運的是,有兩種方法可以教會 db4o 如何管理這樣的轉換。

更改磁盤上的名稱

使 db4o 適應這樣劇烈的更改的一種方法是編寫自己的重構工具,使用 db4o Refactoring API 打開已有的數據文件,並更改在磁盤上的名稱。可以通過一組簡單的調用做到這一點,如清單 10 所示:


清單 10. 從 Person 重構爲 Individual
                import com.db4o.*;
import com.db4o.config.*;

// ...

Db4o.configure().objectClass("com.tedneward.model.Person")
.rename("com.tedneward.persons.model.Individual");

注意,清單 10 中的代碼使用 db4o Configuration API 獲得一個配置對象,該配置對象被用作對 db4o 的大多數選項的 “元控制(meta-control)” —— 在運行時,您將使用這個 API 而不是命令行標誌或配置文件來設置特定的設置(雖然您完全可以創建自己的命令行標誌或配置文件來驅動 Configuration API 調用)。然後,使用 Configuration 對象獲得 Person 類的 ObjectClass 實例……或者更確切地說,是表示磁盤上存儲的 Person 實例的 ObjectClass 實例。ObjectClass 還包含很多其它選項,在本系列的後面我會展示其中的一些選項。

使用別名

在某些情況下,磁盤上的數據必須存在,以支持由於技術或策略上的某種原因而不能重新編譯的早期應用程序。在這些情況下,V2 應用程序必須能夠提取 V1 實例,並在內存中將它們轉換成 V2 實例。 幸運的是,在向磁盤存儲並從中檢索對象時,可以依靠 db4o 的別名 特性創建一個 shuffle 步驟。這樣便可以區別內存中使用的類型和存儲的類型。

db4o 支持三種類型的別名,其中一種類型只有當 .NET 和 Java 風格的 db4o 之間共享數據文件時纔有用。 清單 11 中出現的別名是 TypeAlias,它有效地告訴 db4o 用內存中的 “A” 類型(運行時名稱)替換磁盤上的 “B” 類型(存儲的名稱)。啓用這種別名是一種雙線操作。


清單 11. TypeAlias shuffle
                import com.db4o.config.*;

// ...

TypeAlias fromPersonToIndividual =
new TypeAlias("com.tedneward.model.Person", "com.tedneward.persons.model.Individual");
Db4o.configure().addAlias(fromPersonToIndividual);

當運行時,db4o 現在將查詢數據庫中的 Individual 對象的任何調用識別爲一個請求,而不會查找存儲的 Person 實例;這意味着,Individual 類中的名稱和類型應該和 Person 中存儲的名稱和類型類似,db4o 將適當地處理它們之間的映射。然後,Individual 實例將被存儲在 Person 名稱之下。

更多重構方法
我還沒有談到 db4o 支持重構的所有方法,也就是說還有很多要學的東西。即使您發現 db4o 的重構選項不能很好地處理自己的情況,也仍然有舊的後備選項可用,您可以在要求的位置用一個臨時名稱創建新類,編寫一些代碼從舊類創建新類的對象,然後刪 除舊的對象,並將臨時類重新命名爲適當的名稱。如果急於知道這種選項,請參閱 db4o 的 doc/reference directory 的 Advanced Type Handling 小節中的 “Refactoring and meta-information”。

結束語

由 於對象數據庫中的模式就是類定義本身,而不是採用不同語言的單獨的 DDL 定義,因此本文中的每個重構例子都顯得簡單很多。db4o 中的重構是使用代碼完成的,常常可以通過一個配置調用來確定,最壞情況也只不過是編寫和運行一個轉換實用程序,以將已有實例從舊的類型更新爲新的類型。而 且這種類型的轉換對於幾乎所有生產中的 RDBMS 重構都是必需的。

db4o 強大的重構能力使之在開發期間非常有用,因爲在開發期間,正在設計的很多對象仍然是變化無常的,即使不需要每個小時都重構,至少也需要每天都重構。如果將 db4o 用於單元測試和測試驅動開發,則可以節省大量更改數據庫的時間,如果重構只是簡單的字段添加/刪除或類型/名稱更改,這一點就更加明顯了。

這就是本文討論的內容,但是請記住:如果要用對象編寫應用程序,並且持久性存儲實際上 “只是和實現有關”,那麼爲什麼非得把很好的對象限制成規規矩矩、四四方方的樣子呢?

第四部分 超越簡單對象


到目前爲止,我們在 db4o 中創建並操作對象看起來都比較簡單 —— 事實上,甚至有點簡單了。本文中,熱心於 db4o 的 Ted Neward 將超越這些簡單對象,他將展示簡單對象結構化(引用對象的對象)時發生的操作。此外,他還闡述了包括無限遞歸、層疊行爲以及引用一致性在內的一些話題。

一段時間以來,在 面向 Java 開發人員的 db4o 指南 中,我查看了各種使用 db4o 存儲 Java 對象的方法,這些方法都不依賴映射文件。 使用原生對象數據庫的其中一個優點就是可以避免對象關係映射(也許這不是重點),但我曾用於闡述這種優點的對象模型過於簡單,絕大多數企業系統要求創建並操作相當複雜的對象,也稱爲結構化對象,因此本文將討論結構化對象的創建。

結構化對象 基本上可以看成是一個引用其他對象的對象。儘管 db4o 允許對結構化對象執行所有常用的 CRUD 操作,但是用戶卻必須承受一定的複雜性。本文將探究一些主要的複雜情況(比如無限遞歸、層疊行爲和引用一致性),以後的文章還將深入探討更加高級的結構化 對象處理問題。作爲補充,我還將介紹探察測試(exploration test):一種少爲人知的可測試類庫 db4o API 的測試技術。

從簡單到結構化

清單 1 重述了我在介紹 db4o 時一直使用的一個簡單類 Person


清單 1. Person
                
package com.tedneward.model;

public class Person
{
public Person()
{ }
public Person(String firstName, String lastName, int age, Mood mood)
{
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
this.mood = mood;
}

public String getFirstName() { return firstName; }
public void setFirstName(String value) { firstName = value; }

public String getLastName() { return lastName; }
public void setLastName(String value) { lastName = value; }

public int getAge() { return age; }
public void setAge(int value) { age = value; }

public Mood getMood() { return mood; }
public void setMood(Mood value) { mood = value; }

public String toString()
{
return
"[Person: " +
"firstName = " + firstName + " " +
"lastName = " + lastName + " " +
"age = " + age + " " +
"mood = " + mood +
"]";
}

public boolean equals(Object rhs)
{
if (rhs == this)
return true;

if (!(rhs instanceof Person))
return false;

Person other = (Person)rhs;
return (this.firstName.equals(other.firstName) &&
this.lastName.equals(other.lastName) &&
this.age == other.age);
}

private String firstName;
private String lastName;
private int age;
private Mood mood;
}

OODBMS 系統中的 String
您可能還記得,在我此前的文章示例中,Person 類型使用 String 作爲字段。在 Java 和 .NET 裏,String 是一種對象類型,從 Object 繼承而來,這似乎有些矛盾。事實上,包括 db4o 在內的絕大多數 OODBMS 系統在對待 String 上與其他對象都有不同, 尤其針對 String 的不可變(immutable)特性。

這個簡單的 Person 類在用於介紹基本 db4o 存儲、查詢和檢索數據操作時行之有效,但它無法滿足真實世界中企業編程的複雜性。 舉例而言,數據庫中的 Person 有家庭地址是很正常的。有些情況下,還可能需要配偶以及子女。

若要在數據庫里加一個 “Spouse” 字段,這意味着要擴展 Person,使它能夠引用 Spouse 對象。假設按照某些業務規則,還需要添加一個 Gender 枚舉類型及其對應的修改方法,並在構造函數裏添加一個 equals() 方法。在清單 2 中,Person 類型有了配偶字段和對應的 get/set 方法對,此時還附帶了某些業務規則:


清單 2. 這個人到了結婚年齡嗎?
                
package com.tedneward.model;
public class Person {
// . . .

public Person getSpouse() { return spouse; }
public void setSpouse(Person value) {
// A few business rules
if (spouse != null)
throw new IllegalArgumentException("Already married!");

if (value.getSpouse() != null && value.getSpouse() != this)
throw new IllegalArgumentException("Already married!");

spouse = value;

// Highly sexist business rule
if (gender == Gender.FEMALE)
this.setLastName(value.getLastName());

// Make marriage reflexive, if it's not already set that way
if (value.getSpouse() != this)
value.setSpouse(this);
}

private Person spouse;
}

清單 3 中的代碼創建了兩個到達婚齡的 Person,代碼和您預想的很接近:


清單 3. 去禮堂,要結婚了……
                
import java.util.*;
import com.db4o.*;
import com.db4o.query.*;
import com.tedneward.model.*;

public class App
{
public static void main(String[] args)
throws Exception
{
ObjectContainer db = null;
try
{
db = Db4o.openFile("persons.data");

Person ben = new Person("Ben", "Galbraith",
Gender.MALE, 29, Mood.HAPPY);
Person jess = new Person("Jessica", "Smith",
Gender.FEMALE, 29, Mood.HAPPY);

ben.setSpouse(jess);

System.out.println(ben);
System.out.println(jess);

db.set(ben);

db.commit();

List<Person> maleGalbraiths =
db.query(new Predicate<Person>() {
public boolean match(Person candidate) {
return candidate.getLastName().equals("Galbraith") &&
candidate.getGender().equals(Gender.MALE);
}
});
for (Person p : maleGalbraiths)
{
System.out.println("Found " + p);
}
}
finally
{
if (db != null)
db.close();
}
}
}

開始變得複雜了

除了討厭的業務規則之外,有幾個重要的情況出現了。首先,當對象 ben 存儲到數據庫後,OODBMS 除了存儲一個對象外,顯然還做了其他一些事情。 再次檢索 ben 對象時,與之相關的配偶信息不僅已經存儲而且還被自動檢索。


思考一下,這包含了可怕的暗示。儘管可以想見 OODBMS 是如何避免無限遞歸 的場景, 更恐怖的問題在於,設想一個對象有着對其他 幾十個、成百上千個對象的引用,每個引用對象又都有着 其自身對其他對象的引用。不妨考慮一下模型表示子女、雙親等的情景。 僅僅是從數據庫中取出一個 Person 就會導致 追溯到所有人類的源頭。這意味着在網絡上傳輸大量對象!

幸運的是,除了那些最原始的 OODBMS,幾乎所有的 OODBMS 都已解決了這個問題,db4o 也不例外。

db4o 的探察測試

考察 db4o 的這個領域是一項棘手的任務,也給了我一個機會 展示一位好友教給我的策略:探察測試。(感謝 Stu Halloway,據我所知,他是第一個擬定該說法的人。) 探察測試,簡要而言,是一系列單元測試,不僅測試待查的庫,還可探究 API 以確保庫行爲與預期一致。該方法具有一個有用的副作用,未來的庫版本可以放到探察測試代碼中,編譯並且測試。如果代碼不能編譯或者無法通過所有的探察測試,則顯然意味着庫沒有做到向後兼容,您就可以在用於生產系統之前發現這個問題。

對 db4o API 的探察測試使我能夠使用一種 “before” 方法來創建數據庫並使用 Person 填充數據庫,並使用 “after” 方法來刪除數據庫並消除測試過程中發生的誤判(false positive)。若非如此,我將不得不記得每次手工刪除 persons.data 文件。 坦白說,我並不相信自己在探索 API 的時候還能每次都記得住。

我在進行 db4o 探察測試時,在控制檯模式使用 JUnit 4 測試庫。寫任何測試代碼前,StructuredObjectTest 類如清單 4 所示:


清單 4. 影響 db4o API 的測試
                
import java.io.*;
import java.util.*;
import com.db4o.*;
import com.db4o.query.*;
import com.tedneward.model.*;

import org.junit.Before;
import org.junit.After;
import org.junit.Ignore;
import org.junit.Test;
import static org.junit.Assert.*;

public class StructuredObjectsTest
{
ObjectContainer db;

@Before public void prepareDatabase()
{
db = Db4o.openFile("persons.data");

Person ben = new Person("Ben", "Galbraith",
Gender.MALE, 29, Mood.HAPPY);
Person jess = new Person("Jessica", "Smith",
Gender.FEMALE, 29, Mood.HAPPY);

ben.setSpouse(jess);

db.set(ben);

db.commit();
}

@After public void deleteDatabase()
{
db.close();
new File("persons.data").delete();
}


@Test public void testSimpleRetrieval()
{
List<Person> maleGalbraiths =
db.query(new Predicate<Person>() {
public boolean match(Person candidate) {
return candidate.getLastName().equals("Galbraith") &&
candidate.getGender().equals(Gender.MALE);
}
});

// Should only have one in the returned set
assertEquals(maleGalbraiths.size(), 1);

// (Shouldn't display to the console in a unit test, but this is an
// exploration test, not a real unit test)
for (Person p : maleGalbraiths)
{
System.out.println("Found " + p);
}
}
}

自然,針對這套測試運行 JUnit 測試運行器會生成預計輸出:要麼是“.”,要麼是綠條,這與所選擇的測試運行器有關(控制檯或 GUI)。注意,一般不贊成向控制檯寫數據 —— 應該用斷言進行驗證,而不是用眼球 —— 不過在探察測試裏,做斷言之前看看得到的數據是個好辦法。如果有什麼沒通過,我總是可以註釋掉 System.out.println 調用。(可以自由地添加,以測試您想測試的其他 db4o API 特性。)

從這裏開始,假定清單 4 中的測試套件包含了代碼示例和測試方法(由方法簽名中的 @Test 註釋指明。)。





存取結構化對象

存儲結構化對象很大程度上和以前大部分做法一樣:對對象調用 db.set(),OODBMS 負責其餘的工作。對哪個對象調用 set() 並不重要,因爲 OODBMS 通過對象標識符(OID)對對象進行了跟蹤(參閱 “ 面向 Java 開發人員的 db4o 指南:查詢、更新以及一致性”),因此不會對同一對象進行兩次存儲。

Retrieving 結構化對象則令我不寒而慄。如果要檢索的對象(無論是通過 QBE 或原生查詢) 擁有大量對象引用,而每個被引用的對象也有着大量的對象引用,以此類推。這有一點像糟糕的 Ponzi 模式,不是嗎?

避免無限遞歸

不管大多數開發者的最初反應(一般是 “不可能是這樣的吧,是嗎?”)如何, 無限遞歸在某種意義上正是 db4o 處理結構化對象的真正方式。事實上,這種方式是絕大多數程序員希望的,因爲我們都希望在尋找所創建的對象時,它們正好 “就在那裏”。同時,我們也顯然不想通過一根線纜獲得整個世界的信息,至少不要一次就得到。

db4o 對此採用了折衷的辦法,限制所檢索的對象數量,使用稱爲激活深度(activation depth)的方法,它指明在對象圖中進行檢索的最低層。換句話說,激活深度表示從根對象中標識的引用總數,db4o 將在查詢中遍歷根對象並返回結果。在前面的例子中,當檢索 Ben 時,默認的激活深度 5 足夠用於檢索 Jessica,因爲它只需要 僅僅一個引用遍歷。任何距離 Ben 超過 5 個引用的對象將無法 被檢索到, 它們的引用將置爲空。我的工作就是 顯式地從數據庫激活那些對象,在 ObjectContainer 使用 activate() 方法。

如果要改變默認激活深度, 需要以一種精密的方式,在 Configuration 類(從 db.configure() 返回)中使用 db4o 的 activationDepth() 方法修改默認值。 還有一種方式,可以對每個類配置激活深度。 在清單 5 中,使用 ObjectClassPerson 類型配置默認激活深度:


清單 5. 使用 ObjectClass 配置激活深度
                
// See ObjectClass for more info
Configuration config = Db4o.configure();
ObjectClass oc = config.objectClass("com.tedneward.model.Person");
oc.minimumActivationDepth(10);





更新結構化對象

更新所關注的是另外一個問題:如果在對象圖中更新一個對象,但並沒有做顯式設置, 那麼會發生什麼?正如最初調用 set() 時,將存儲引用了其他存儲對象的相關對象,與之相似, 當一個對象傳遞到 ObjectContainer,db4o 遍歷所有引用,將發現的對象存儲到數據庫中,如清單 6 所示:


清單 6. 更新被引用的對象
                
@Test public void testDependentUpdate()
{
List<Person> maleGalbraiths =
db.query(new Predicate<Person>() {
public boolean match(Person candidate) {
return candidate.getLastName().equals("Galbraith") &&
candidate.getGender().equals(Gender.MALE);
}
});

Person ben = maleGalbraiths.get(0);

// Happy birthday, Jessica!
ben.getSpouse().setAge(ben.getSpouse().getAge() + 1);

// We only have a reference to Ben, so store that and commit
db.set(ben);
db.commit();

// Find Jess, make sure she's 30
Person jess = (Person)db.get(
new Person("Jessica", "Galbraith", null, 0, null)).next();
assertTrue(jess.getAge() == 30);
}

儘管已經對 jess 對象做了變動, ben 對象還擁有對 jess 的引用。因此內存中 jess Person 的更新會保存在數據庫中。

其實不是這樣。好的,我剛纔是在撒謊。

測試誤判

事實是,探察測試在某個地方出問題了,產生了一個誤判。 儘管從文檔來看並不明顯, ObjectContainer 保持着已激活對象的緩存, 所以當清單 6 中的測試從容器中檢索 Jessica 對象時,返回的是 包含變動的內存對象,而不是寫到磁盤上真正數據。 這掩蓋了一個事實,某類型的默認更新深度 是 1, 意味着只有原語值(包括 String)纔會在調用 set() 時被存儲。爲了使該行爲生效,我必須稍微修改一下測試,如清單 7 所示:


清單 7. 測試誤判
                
@Test(expected=AssertionError.class)
public void testDependentUpdate()
{
List<Person> maleGalbraiths =
db.query(new Predicate<Person>() {
public boolean match(Person candidate) {
return candidate.getLastName().equals("Galbraith") &&
candidate.getGender().equals(Gender.MALE);
}
});

Person ben = maleGalbraiths.get(0);
assertTrue(ben.getSpouse().getAge() == 29);

// Happy Birthday, Jessica!
ben.getSpouse().setAge(ben.getSpouse().getAge() + 1);

// We only have a reference to Ben, so store that and commit
db.set(ben);
db.commit();

// Close the ObjectContainer, then re-open it
db.close();
db = Db4o.openFile("persons.data");

// Find Jess, make sure she's 30
Person jess = (Person)db.get(
new Person("Jessica", "Galbraith", null, 0, null)).next();
assertTrue(jess.getAge() == 30);
}

測試時,得到 AssertionFailure, 說明此前有關對象圖中層疊展開的對象更新的論斷是錯誤的。(通過將您希望拋出異常的類類型的 @Test 註釋的值設置爲 expected,可以使 JUit 提前預測到這種錯誤。)

設置層疊行爲

Db4o 僅僅返回緩存對象,而不對其更多地進行隱式處理,這是一個有爭議的話題。 很多編程人員認爲 要麼這種行爲是有害的並且違反直覺,要麼 這種行爲正是 OODBMS 應該做的。不要去管這兩種觀點優劣如何, 重要的是理解數據庫的默認行爲並且知道如何修正。在清單 8 中,使用 ObjectClass.setCascadeOnUpdate() 方法爲一特定類型改變 db4o 的 默認更新動作。不過要注意,在打開 ObjectContainer 之前,必須 設定該方法爲 true。清單 8 展示了修改後的正確的層疊測試。


清單 8. 設置層疊行爲爲 true
                
@Test
public void testWorkingDependentUpdate()
{
// the cascadeOnUpdate() call must be done while the ObjectContainer
// isn't open, so close() it, setCascadeOnUpdate, then open() it again
db.close();
Db4o.configure().objectClass(Person.class).cascadeOnUpdate(true);
db = Db4o.openFile("persons.data");

List<Person> maleGalbraiths =
db.query(new Predicate<Person>() {
public boolean match(Person candidate) {
return candidate.getLastName().equals("Galbraith") &&
candidate.getGender().equals(Gender.MALE);
}
});

Person ben = maleGalbraiths.get(0);
assertTrue(ben.getSpouse().getAge() == 29);

// Happy Birthday, Jessica!
ben.getSpouse().setAge(ben.getSpouse().getAge() + 1);

// We only have a reference to Ben, so store that and commit
db.set(ben);
db.commit();

// Close the ObjectContainer, then re-open it
db.close();

db = Db4o.openFile("persons.data");

// Find Jess, make sure she's 30
Person jess = (Person)db.get(
new Person("Jessica", "Galbraith", null, 0, null)).next();
assertTrue(jess.getAge() == 30);
}

不僅可以爲更新設置層疊行爲,也可以對檢索(創建值爲 “unlimited” 的激活深度)和刪除設置層疊行爲 —— 這是我最新琢磨的 Person 對象的最後一個應用 。





刪除結構化對象

從數據庫中刪除對象與檢索和更新對象類似: 默認情況下,刪除一個對象時,不刪除它引用的對象。 一般而言,這也是理想的行爲。如清單 9 所示:


清單 9. 刪除結構化對象
                
@Test
public void simpleDeletion()
{
Person ben = (Person)db.get(new Person("Ben", "Galbraith", null, 0, null)).next();
db.delete(ben);

Person jess = (Person)db.get(new Person("Jessica", "Galbraith", null, 0, null)).next();
assertNotNull(jess);
}

但是,有些時候在刪除對象時,希望強制刪除其引用的對象。 與激活和更新一樣, 可以通過調用 Configuration 類觸發此行爲。如清單 10 所示:


清單 10. Configuration.setCascadeOnDelete()
                
@Test
public void cascadingDeletion()
{
// the cascadeOnUpdate() call must be done while the ObjectContainer
// isn't open, so close() it, setCascadeOnUpdate, then open() it again
db.close();
Db4o.configure().objectClass(Person.class).cascadeOnDelete(true);
db = Db4o.openFile("persons.data");

Person ben =
(Person)db.get(new Person("Ben", "Galbraith", null, 0, null)).next();
db.delete(ben);

ObjectSet<Person> results =
db.get(new Person("Jessica", "Galbraith", null, 0, null));
assertFalse(results.hasNext());
}

執行該操作時要小心,因爲它意味着其他引用了被消除層疊的對象的對象將擁有一個對 null 的引用 —— db4o 對象數據庫在防止刪除被引用對象上使用的引用一致性 在這裏沒有什麼作用。 (引用一致性是 db4o 普遍需要的特性,據說開發團隊正在考慮在未來某個版本中加入這一特性。對於使用 db4o 的開發人員來說,關鍵在於要以一種不違反最少意外原則 的方式實現,甚至某些時候, 即使是在關係數據庫中, 打破一致性規則實際上也是一種理想的實踐。)





結束語

本文是該系列文章的分水嶺:在此之前, 我使用的所有示例都基於非常簡單的對象,從應用角度來講, 那些例子都不現實,其主要作用只是爲了使您理解 OODBMS,而不是被存儲的對象。 理解像 db4o 這樣的 OODBMS 是如何通過引用存儲相關對象,是比較複雜的事情。 幸運的是,一旦您掌握了這些行爲(通過解釋和理解),您所要做的就只是 開始調整代碼來實現這些行爲。

在本文中,您看到了一些基本例子,通過調整複雜代碼來實現 db4o 對象模型。 學習瞭如何對結構化對象執行一些簡單 CRUD 操作,同時,也看到了一些 不可避免的問題和解決方法。

其實,目前的結構化對象例子仍然比較簡單, 對象之間還只是直接引用關係。 許多夫妻都知道,結婚一段時間後,孩子將會出現。 本系列的下一文章中,我將繼續 探索 db4o 中的結構化對象的創建與操作,看看在引入若干子對象後, benjess 對象將發生什麼。


第五部分  數組和集合

    集合和數組爲 面向 Java 開發人員的 db4o 指南: 超越簡單對象 中首次討論的結構化對象引入了新的複雜性。幸運的是,db4o 絲毫沒有因爲處理多樣性關係而出現困難 —— 您應該也不會被它難倒。

在本系列的前一篇文章中,我開始談到了 db4o 如何處理 結構化對象,或者包含非原始類型字段的對象。正如我所展示的那樣,增加對象關係的複雜性對 db4o 持久模型有一些重大的影響。我談到了在刪除期間解決像激活深度(activation depth)、級聯更新與刪除和參照完整性等問題的重要性。我還介紹了一種叫做 探察測試 的開發人員測試策略,附帶給出了使用 db4o API 的第一個練習。

在本文中,我繼續介紹 db4o 中結構化對象的存儲和操作,並首先介紹多樣性關係(multiplicity relationship),在多樣性關係中,對象中含有對象集合形式的字段。(在此,集合 是指像 ArrayList 之類的 Collection 類和標準語言數組。)您將看到,db4o 可以輕鬆處理多樣性。您還將進一步熟悉 db4o 對級聯更新和激活深度的處理。

處理多樣性關係

隨着這個系列深入下去,之前的 Person 類肯定會變得更加複雜。在 關於結構化對象的上一次討論 結束的時候,我在 Person 中添加了一個 spouse 字段和一些相應的業務邏輯。在那篇文章的最後我提到,舒適的家庭生活會導致一個或更多 “小人兒” 降臨到這個家庭。但是,在增加小孩到家庭中之前,我想先確保我的 Person 真正有地方可住。我要給他們一個工作場所,或者還有一個很好的夏日度假屋。一個 Address 類型應該可以解決所有這三個地方。



清單 1. 添加一個 Address 類型到 Person 類中
                
package com.tedneward.model;

public class Address
{
public Address()
{
}

public Address(String street, String city, String state, String zip)
{
this.street = street; this.city = city;
this.state = state; this.zip = zip;
}

public String toString()
{
return "[Address: " +
"street=" + street + " " +
"city=" + city + " " +
"state=" + state + " " +
"zip=" + zip + "]";
}

public int hashCode()
{
return street.hashCode() & city.hashCode() &
state.hashCode() & zip.hashCode();
}

public boolean equals(Object obj)
{
if (obj == this)
return this;

if (obj instanceof Address)
{
Address rhs = (Address)obj;

return (this.street.equals(rhs.street) &&
this.city.equals(rhs.city) &&
this.state.equals(rhs.state) &&
this.zip.equals(rhs.zip));
}
else
return false;
}

public String getStreet() { return this.street; }
public void setStreet(String value) { this.street = value; }

public String getCity() { return this.city; }
public void setCity(String value) { this.city = value; }

public String getState() { return this.state; }
public void setState(String value) { this.state = value; }

public String getZip() { return this.zip; }
public void setZip(String value) { this.zip = value; }

private String street;
private String city;
private String state;
private String zip;
}

可以看到,Address 只是一個簡單的數據對象。將它添加到 Person 類中意味着 Person 將有一個名爲 addressesAddress 數組作爲字段。第一個地址是家庭住址,第二個是工作地址,第三個(如果不爲 null 的話)是度假屋地址。當然,這些都被設置爲 protected,以便將來通過方法來封裝。

完成這些設置後,現在可以增強 Person 類,使之支持小孩,所以我將爲 Person 定義一個新字段:一個 PersonArrayList,它同樣也有一些相關的方法,以便進行適當的封裝。

接下來,由於大多數小孩都有父母,我還將添加兩個字段來表示母親和父親,並增加適當的 accessor/mutator 方法。我將爲 Person 類增加一個新的方法,使之可以創建一個新的 Person,這個方法有一個貼切的名稱,即 haveBaby。此外還增加一些業務規則,以支持生小孩的生物學需求,並將這個新的小 Person 添加到爲母親和父親字段創建的 children ArrayList 中。做完這些之後,再將這個嬰兒返回給調用者。

清單 2 顯示,新定義的 Person 類可以處理這種多樣性關係。


清單 2. 定義爲多樣性關係的家庭生活
                
package com.tedneward.model;

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

public class Person
{
public Person()
{ }
public Person(String firstName, String lastName, Gender gender, int age, Mood mood)
{
this.firstName = firstName;
this.lastName = lastName;
this.gender = gender;
this.age = age;
this.mood = mood;
}

public String getFirstName() { return firstName; }
public void setFirstName(String value) { firstName = value; }

public String getLastName() { return lastName; }
public void setLastName(String value) { lastName = value; }

public Gender getGender() { return gender; }

public int getAge() { return age; }
public void setAge(int value) { age = value; }

public Mood getMood() { return mood; }
public void setMood(Mood value) { mood = value; }

public Person getSpouse() { return spouse; }
public void setSpouse(Person value) {
// A few business rules
if (spouse != null)
throw new IllegalArgumentException("Already married!");

if (value.getSpouse() != null && value.getSpouse() != this)
throw new IllegalArgumentException("Already married!");

spouse = value;

// Highly sexist business rule
if (gender == Gender.FEMALE)
this.setLastName(value.getLastName());

// Make marriage reflexive, if it's not already set that way
if (value.getSpouse() != this)
value.setSpouse(this);
}

public Address getHomeAddress() { return addresses[0]; }
public void setHomeAddress(Address value) { addresses[0] = value; }

public Address getWorkAddress() { return addresses[1]; }
public void setWorkAddress(Address value) { addresses[1] = value; }

public Address getVacationAddress() { return addresses[2]; }
public void setVacationAddress(Address value) { addresses[2] = value; }

public Iterator<Person> getChildren() { return children.iterator(); }
public Person haveBaby(String name, Gender gender) {
// Business rule
if (this.gender.equals(Gender.MALE))
throw new UnsupportedOperationException("Biological impossibility!");

// Another highly objectionable business rule
if (getSpouse() == null)
throw new UnsupportedOperationException("Ethical impossibility!");

// Welcome to the world, little one!
Person child = new Person(name, this.lastName, gender, 0, Mood.CRANKY);
// Well, wouldn't YOU be cranky if you'd just been pushed out of
// a nice warm place?!?

// These are your parents...
child.father = this.getSpouse();
child.mother = this;

// ... and you're their new baby.
// (Everybody say "Awwww....")
children.add(child);
this.getSpouse().children.add(child);

return child;
}

public String toString()
{
return
"[Person: " +
"firstName = " + firstName + " " +
"lastName = " + lastName + " " +
"gender = " + gender + " " +
"age = " + age + " " +
"mood = " + mood + " " +
(spouse != null ? "spouse = " + spouse.getFirstName() + " " : "") +
"]";
}

public boolean equals(Object rhs)
{
if (rhs == this)
return true;

if (!(rhs instanceof Person))
return false;

Person other = (Person)rhs;
return (this.firstName.equals(other.firstName) &&
this.lastName.equals(other.lastName) &&
this.gender.equals(other.gender) &&
this.age == other.age);
}

private String firstName;
private String lastName;
private Gender gender;
private int age;
private Mood mood;
private Person spouse;
private Address[] addresses = new Address[3];
private List<Person> children = new ArrayList<Person>();
private Person mother;
private Person father;
}

即使包括所有這些代碼,清單 2 提供的家庭關係模型還是過於簡單。在這個層次結構中的某些地方,必須處理那些 null 值。但是,在 db4o 中,那個問題更應該在對象建模中解決,而不是在對象操作中解決。所以現在我可以放心地忽略它。





填充和測試對象模型

對於清單 2 中的 Person 類,需要重點注意的是,如果以關係的方式,使用父與子之間分層的、循環的引用來建模,那肯定會比較笨拙。通過一個實例化的對象模型可以更清楚地看到我所談到的複雜性,所以我將編寫一個探察測試來實例化 Person 類。 注意,清單 3 中省略了 JUnit 支架(scaffolding);我假設您可以從其他地方,包括本系列之前的文章學習 JUnit 4 API。通過閱讀本文的源代碼,還可以學到更多東西。


清單 3. 幸福家庭測試
                    
@Test public void testTheModel()
{
Person bruce = new Person("Bruce", "Tate",
Gender.MALE, 29, Mood.HAPPY);
Person maggie = new Person("Maggie", "Tate",
Gender.FEMALE, 29, Mood.HAPPY);
bruce.setSpouse(maggie);

Person kayla = maggie.haveBaby("Kayla", Gender.FEMALE);

Person julia = maggie.haveBaby("Julia", Gender.FEMALE);

assertTrue(julia.getFather() == bruce);
assertTrue(kayla.getFather() == bruce);
assertTrue(julia.getMother() == maggie);
assertTrue(kayla.getMother() == maggie);

int n = 0;
for (Iterator<Person> kids = bruce.getChildren(); kids.hasNext(); )
{
Person child = kids.next();

if (n == 0) assertTrue(child == kayla);
if (n == 1) assertTrue(child == julia);

n++;
}
}

目前一切尚好。所有方面都能通過測試,包括小孩 ArrayList 的使用中的長嗣身份。但是,當我增加 @Before@After 條件,以便用我的測試數據填充 db4o 數據庫時,事情開始變得更有趣。


清單 4. 將孩子發送到數據庫
                    
@Before public void prepareDatabase()
{
db = Db4o.openFile("persons.data");

Person bruce = new Person("Bruce", "Tate",
Gender.MALE, 29, Mood.HAPPY);
Person maggie = new Person("Maggie", "Tate",
Gender.FEMALE, 29, Mood.HAPPY);
bruce.setSpouse(maggie);

bruce.setHomeAddress(
new Address("5 Maple Drive", "Austin",
"TX", "12345"));
bruce.setWorkAddress(
new Address("5 Maple Drive", "Austin",
"TX", "12345"));
bruce.setVacationAddress(
new Address("10 Wanahokalugi Way", "Oahu",
"HA", "11223"));

Person kayla = maggie.haveBaby("Kayla", Gender.FEMALE);
kayla.setAge(8);

Person julia = maggie.haveBaby("Julia", Gender.FEMALE);
julia.setAge(6);

db.set(bruce);

db.commit();
}

注意,存儲整個家庭所做的工作仍然不比存儲單個 Person 對象所做的工作多。您可能還記得,在上一篇文章中,由於存儲的對象具有遞歸的性質,當把 bruce 引用傳遞給 db.set() 調用時,從 bruce 可達的所有對象都被存儲。不過眼見爲實,讓我們看看當運行我那個簡單的探察測試時,實際上會出現什麼情況。首先,我將測試當調用隨 Person 存儲的各種 Address 時,是否可以找到它們。然後,我將測試是否孩子們也被存儲。


清單 5. 搜索住房和家庭
                    
@Test public void testTheStorageOfAddresses()
{
List<Person> maleTates =
db.query(new Predicate<Person>() {
public boolean match(Person candidate) {
return candidate.getLastName().equals("Tate") &&
candidate.getGender().equals(Gender.MALE);
}
});
Person bruce = maleTates.get(0);

Address homeAndWork =
new Address("5 Maple Drive", "Austin",
"TX", "12345");
Address vacation =
new Address("10 Wanahokalugi Way", "Oahu",
"HA", "11223");

assertTrue(bruce.getHomeAddress().equals(homeAndWork));
assertTrue(bruce.getWorkAddress().equals(homeAndWork));
assertTrue(bruce.getVacationAddress().equals(vacation));
}

@Test public void testTheStorageOfChildren()
{
List<Person> maleTates =
db.query(new Predicate<Person>() {
public boolean match(Person candidate) {
return candidate.getLastName().equals("Tate") &&
candidate.getGender().equals(Gender.MALE);
}
});
Person bruce = maleTates.get(0);

int n = 0;
for (Iterator<Person> children = bruce.getChildren();
children.hasNext();
)
{
Person child = children.next();

System.out.println(child);

if (n==0) assertTrue(child.getFirstName().equals("Kayla"));
if (n==1) assertTrue(child.getFirstName().equals("Julia"));

n++;
}
}

查詢數組和集合

雖然 Collection 類型被當作 db4o 數據庫中的一級實例,但數組卻不是。如果對一個數組(不管是什麼類型的數組)執行一個本地查詢或基於原型的查詢,那麼不會返回任何可供查看或檢索的對象。 這有好的一面,也有不好的一面:對於大多數情況,基於數組類型的查詢所產生的對象遠遠超過預期。例如,執行對一個 Object 的查詢將返回一個小的對象模型。但是有時候,能夠查詢數組會很有用,不能查詢則會是一個問題。

類似地,Collections 可以 查詢這一事實也會產生負面影響:雖然查詢一個 ArrayList 是可行的,但這將返回對象數據庫中的每個 ArrayList,而不管它的上下文是什麼。實際上,在集合與數組之間作出選擇的最好方法是將數組用於 “內部” 集合(不能通過查詢來訪問),而將 Collection 類用於 “外部” 集合(應該可以通過查詢來訪問)。記住,在本地查詢中總是可以訪問數組,也就是說,如果需要在 Persons 數據庫中查詢居住在特定地址的 Person,那麼可以編寫一個查詢來直接檢索 Person,然後從中得出 Address,而不是直接從數組之外查詢 Address

理解關係

您可能會感到奇怪,清單 5 中顯示的基於 Collection 的類型(ArrayList)沒有被存儲爲 Person 類型的 “dependents”,而是被存儲爲一個成熟的對象。這還說得過去,但是當對對象數據庫中的 ArrayList 運行一個查詢時,它可能,有時候也確實會導致返回奇怪的結果。由於目前數據庫中只有一個 ArrayList,所以還不值得運行一個探察測試,看看當對它運行一個查詢時會出現什麼情況。我把這作爲留給您的練習。

自然地,存儲在一個集合中的 Person 也被當作數據庫中的一級實體,所以在查詢符合某個特定標準(例如所有女性 Person)的所有 Person 時,也會返回 ArrayList 實例中引用到的那些 Person,如清單 6 所示。


清單 6. 什麼是 Julia?
                    
@Test public void findTheGirls()
{
List<Person> girls =
db.query(new Predicate<Person>() {
public boolean match(Person candidate) {
return candidate.getGender().equals(Gender.FEMALE);
}
});
boolean maggieFound = false;
boolean kaylaFound = false;
boolean juliaFound = false;
for (Person p : girls)
{
if (p.getFirstName().equals("Maggie"))
maggieFound = true;
if (p.getFirstName().equals("Kayla"))
kaylaFound = true;
if (p.getFirstName().equals("Julia"))
juliaFound = true;
}

assertTrue(maggieFound);
assertTrue(kaylaFound);
assertTrue(juliaFound);
}

注意,對象數據庫將盡量地使引用 “correct” — 至少在知道引用的情況下如此。例如,分別在兩個不同的查詢中檢索一個 Person(也許是母親)和檢索另一個 Person(假設是女兒),仍然認爲她們之間存在一個雙向關係,如清單 7 所示。


清單 7. 保持關係的真實性
                    
@Test public void findJuliaAndHerMommy()
{
Person maggie = (Person) db.get(
new Person("Maggie", "Tate", Gender.FEMALE, 0, null)).next();
Person julia = (Person) db.get(
new Person("Julia", "Tate", Gender.FEMALE, 0, null)).next();

assertTrue(julia.getMother() == maggie);
}

當然,您正是希望對象數據庫具有這樣的行爲。還應注意,如果返回女兒對象的查詢的激活深度被設置得足夠低,那麼對 getMother() 的調用將返回 null,而不是實際的對象。這是因爲 Person 中的 mother 字段是相對於被檢索的原本對象的另一個 “跳躍(hop)”。(請參閱 前一篇文章,瞭解更多關於激活深度的信息。)





更新和刪除

至 此,您已經看到了 db4o 如何存儲和取出多個對象,但是對象數據庫如何處理更新和刪除呢?就像結構化對象一樣,多對象更新或刪除期間的很多工作都與管理更新深度有關,或者與級聯刪 除有關。現在您可能已經注意到,結構化對象與集合之間有很多相似之處,所以其中某一種實體的特性也適用於另一種實體。如果將 ArrayList 看作 “另一種結構化對象”,而不是一個集合,就很好理解了。

所以,根據到目前爲止您學到的東西,我應該可以更新數據庫中的某一個女孩。而且,爲了更新這個對象,只需將她父母中的一個重新存儲到數據庫中,如清單 8 所示。


清單 8. 生日快樂,Kayla!
                    
@Test public void kaylaHasABirthday()
{
Person maggie = (Person) db.get(
new Person("Maggie", "Tate", Gender.FEMALE, 0, null)).next();
Person kayla = (Person) db.get(
new Person("Kayla", "Tate", Gender.FEMALE, 0, null)).next();

kayla.setAge(kayla.getAge() + 1);
int kaylasNewAge = kayla.getAge();

db.set(maggie);

db.close();

db = Db4o.openFile("persons.data");

kayla = (Person) db.get(
new Person("Kayla", "Tate", Gender.FEMALE, 0, null)).next();
assert(kayla.getAge() == kaylasNewAge);
}

還記得嗎,在 前一篇文章 中,我必須顯式地關閉到數據庫的連接,以避免被誤診爲重取已經位於工作內存中的對象。

對於多樣性關係中的對象,其刪除工作非常類似於上一篇文章介紹索的結構化對象的刪除工作。只需注意級聯刪除,因爲它對這兩種對象可能都有影響。當執行級聯刪除時,將會從引用對象的每個地方徹底刪除對象。如果執行一個級聯刪除來從數據庫中刪除一個 Person,則那個 Person 的母親和父親在其 children 集合中突然有一個 null 引用,而不是有效的對象引用。

結束語

在很多方面,將數組和集合存儲到對象數據庫中並不總與存儲常規的結構化對象不同,只是要注意數組不能被直接查詢,而集合則可以。不管出於何種目的,這都意味着可以在建模時使用集合和數組,而不必等到持久引擎需要使用集合或數組時才使用它們。


第六部分 結構化對象和集合

面向對象應用程序大量使用繼承,並且它們常常使用繼承(或者 “是一個”)關係來分類和組織給定系統中的對象。在關係存儲模式中使用繼承比較困難,因爲這種模式沒有內在的繼承概念,但它是 OODNBMS 中的一個核心功能。在本期的面向 Java™ 開發人員的 db4o 指南 中,您將會發現,作爲一個核心功能,在 db4o 中創建查詢時使用繼承竟是如此的簡單(而且功能強大)。

在本系列文章中,我使用 Person 類型來演示 db4o 的所有基本原理。您已經學會了如何創建完整的 Person 對象圖,以細粒度方式(使用 db4o 本身的查詢功能來限制返回的實際對象圖)對其進行檢索,以及更新和刪除全部的對象圖(設定一些限制條件)等等。實際上,在面向對象的所有特性中,我們只漏掉了其中一個,那就是繼承。

我將演示的這個例子的最終目標是一個用於存儲僱員數據的數據管理系統,我一直致力於開發我的 Person 類型。我需要這樣一個系統:存儲某個公司的員工及其配偶和子女的信息,但是此時他們僅僅是該系統的 Person(或者,可以說 Employees 是一個 Person,但是 Persons 不是一個 Employee)。而且,我不希望 Employee 的行爲屬於 Person API 的一部分。從對象建模程序的角度公平地講,按照 is-a 模擬類型的能力就是面向對象的本質。

我會用 Person 類型中的一個字段來模擬僱傭 的概念。這是一種關係方法,而且不太適合用於對象設計。幸運的是,與大多數 OODBMS 系統一樣,db4o 系統對繼承有一個完整的理解。在存儲系統的核心使用繼承可以輕鬆地 “重構” 現有系統,可以在設計系統時更多地使用繼承,而不會使查詢工具變得複雜。您將會看到,這也使查詢特定類型的對象變得更加容易。

高度改進的 Person

清單 1 回顧了 Person 類型,該類型在本系列文章中一直作爲示例使用:


清單 1. 改進之前的示例……
                
package com.tedneward.model;

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

public class Person
{
public Person()
{ }
public Person(String firstName, String lastName, Gender gender, int age, Mood mood)
{
this.firstName = firstName;
this.lastName = lastName;
this.gender = gender;
this.age = age;
this.mood = mood;
}

public String getFirstName() { return firstName; }
public void setFirstName(String value) { firstName = value; }

public String getLastName() { return lastName; }
public void setLastName(String value) { lastName = value; }

public Gender getGender() { return gender; }

public int getAge() { return age; }
public void setAge(int value) { age = value; }

public Mood getMood() { return mood; }
public void setMood(Mood value) { mood = value; }

public Person getSpouse() { return spouse; }
public void setSpouse(Person value) {
// A few business rules
if (spouse != null)
throw new IllegalArgumentException("Already married!");

if (value.getSpouse() != null && value.getSpouse() != this)
throw new IllegalArgumentException("Already married!");

spouse = value;

// Highly sexist business rule
if (gender == Gender.FEMALE)
this.setLastName(value.getLastName());

// Make marriage reflexive, if it's not already set that way
if (value.getSpouse() != this)
value.setSpouse(this);
}

public Address getHomeAddress() { return addresses[0]; }
public void setHomeAddress(Address value) { addresses[0] = value; }

public Address getWorkAddress() { return addresses[1]; }
public void setWorkAddress(Address value) { addresses[1] = value; }

public Address getVacationAddress() { return addresses[2]; }
public void setVacationAddress(Address value) { addresses[2] = value; }

public Iterator<Person> getChildren() { return children.iterator(); }
public Person haveBaby(String name, Gender gender) {
// Business rule
if (this.gender.equals(Gender.MALE))
throw new UnsupportedOperationException("Biological impossibility!");

// Another highly objectionable business rule
if (getSpouse() == null)
throw new UnsupportedOperationException("Ethical impossibility!");

// Welcome to the world, little one!
Person child = new Person(name, this.lastName, gender, 0, Mood.CRANKY);
// Well, wouldn't YOU be cranky if you'd just been pushed out of
// a nice warm place?!?

// These are your parents...
child.father = this.getSpouse();
child.mother = this;

// ... and you're their new baby.
// (Everybody say "Awwww....")
children.add(child);
this.getSpouse().children.add(child);

return child;
}

public Person getFather() { return this.father; }
public Person getMother() { return this.mother; }

public String toString()
{
return
"[Person: " +
"firstName = " + firstName + " " +
"lastName = " + lastName + " " +
"gender = " + gender + " " +
"age = " + age + " " +
"mood = " + mood + " " +
(spouse != null ? "spouse = " + spouse.getFirstName() + " " : "") +
"]";
}

public boolean equals(Object rhs)
{
if (rhs == this)
return true;

if (!(rhs instanceof Person))
return false;

Person other = (Person)rhs;
return (this.firstName.equals(other.firstName) &&
this.lastName.equals(other.lastName) &&
this.gender.equals(other.gender) &&
this.age == other.age);
}

private String firstName;
private String lastName;
private Gender gender;
private int age;
private Mood mood;
private Person spouse;
private Address[] addresses = new Address[3];
private List<Person> children = new ArrayList<Person>();
private Person mother;
private Person father;
}

跟本系列的其他文章一樣,我不會在每次更改時都展示完整的 Person 類,只逐步展示每次更改。在這個例子中,我實際上並沒有更改 Person,因爲我將要擴展 Person,而不是修改它。





區別僱員

需要做的第一件事是使我的僱員管理系統能夠區別普通的 Person(例如僱員的配偶和/或子女)和 Employee。從純粹建模的立場來說,這個更改很簡單。我只是向 Person 引入了一個新的派生類,這個類和目前涉及到的其他類都在同一個包中。毫無疑問,我將會調用這個類 Employee,如清單 2 所示:


Listing 2. Employee 擴展 Person
                
package com.tedneward.model;

public class Employee extends Person
{
public Employee()
{ }
public Employee(String firstName, String lastName, String title,
Gender gender, int age, Mood mood)
{
super(firstName, lastName, gender, age, mood);

this.title = title;
}

public String getTitle() { return title; }
public void setTitle(String value) { title = value; }

public String toString()
{
return "[Employee: " + getFirstName() + " " + getLastName() + " " +
"(" + getTitle() + ")]";
}

private String title;
}

Employee 類的全部代碼都在清單 2 中。從 OODBMS 的角度看, Employee 中的其他方法意義不大。在本討論中需要記住的是 EmployeePerson 的一個子類(如果更加關心繫統的建模過程,可以設想 Employee 中的其他方法,例如 promote()demote()getSalary()setSalary()workLikeADog())。





測試新模型

對新模型的探察測試簡單明瞭。我創建一個叫做 InheritanceTest 的 JUnit 類,目前爲止,它是第一個較爲複雜的對象集,充當 OODBMS 最初的工作內容。爲了使輸出(將會在清單 6 中見到)更加清晰,我在清單 3 中展示了帶有 @Before 註釋的 prepareDatabase() 調用:


清單 3. 歡迎加入本公司(您現在爲我服務)
                
@Before public void prepareDatabase()
{
db = Db4o.openFile("persons.data");

// The Newards
Employee ted = new Employee("Ted", "Neward", "President and CEO",
Gender.MALE, 36, Mood.HAPPY);
Person charlotte = new Person("Charlotte", "Neward",
Gender.FEMALE, 35, Mood.HAPPY);
ted.setSpouse(charlotte);
Person michael = charlotte.haveBaby("Michael", Gender.MALE);
michael.setAge(14);
Person matthew = charlotte.haveBaby("Matthew", Gender.MALE);
matthew.setAge(8);
Address tedsHomeOffice =
new Address("12 Redmond Rd", "Redmond", "WA", "98053");
ted.setHomeAddress(tedsHomeOffice);
ted.setWorkAddress(tedsHomeOffice);
ted.setVacationAddress(
new Address("10 Wannahokalugi Way", "Oahu", "HA", "11223"));
db.set(ted);

// The Tates
Employee bruce = new Employee("Bruce", "Tate", "Chief Technical Officer",
Gender.MALE, 29, Mood.HAPPY);
Person maggie = new Person("Maggie", "Tate",
Gender.FEMALE, 29, Mood.HAPPY);
bruce.setSpouse(maggie);
Person kayla = maggie.haveBaby("Kayla", Gender.FEMALE);
Person julia = maggie.haveBaby("Julia", Gender.FEMALE);
bruce.setHomeAddress(
new Address("5 Maple Drive", "Austin",
"TX", "12345"));
bruce.setWorkAddress(
new Address("5701 Downtown St", "Austin",
"TX", "12345"));
// Ted and Bruce both use the same timeshare, apparently
bruce.setVacationAddress(
new Address("10 Wanahokalugi Way", "Oahu",
"HA", "11223"));
db.set(bruce);

// The Fords
Employee neal = new Employee("Neal", "Ford", "Meme Wrangler",
Gender.MALE, 29, Mood.HAPPY);
Person candi = new Person("Candi", "Ford",
Gender.FEMALE, 29, Mood.HAPPY);
neal.setSpouse(candi);
neal.setHomeAddress(
new Address("22 Gritsngravy Way", "Atlanta", "GA", "32145"));
// Neal is the roving architect
neal.setWorkAddress(null);
db.set(neal);

// The Slettens
Employee brians = new Employee("Brian", "Sletten", "Bosatsu Master",
Gender.MALE, 29, Mood.HAPPY);
Person kristen = new Person("Kristen", "Sletten",
Gender.FEMALE, 29, Mood.HAPPY);
brians.setSpouse(kristen);
brians.setHomeAddress(
new Address("57 Classified Drive", "Fairfax", "VA", "55555"));
brians.setWorkAddress(
new Address("1 CIAWasNeverHere Street", "Fairfax", "VA", "55555"));
db.set(brians);

// The Galbraiths
Employee ben = new Employee("Ben", "Galbraith", "Chief UI Director",
Gender.MALE, 29, Mood.HAPPY);
Person jessica = new Person("Jessica", "Galbraith",
Gender.FEMALE, 29, Mood.HAPPY);
ben.setSpouse(jessica);
ben.setHomeAddress(
new Address(
"5500 North 2700 East Rd", "Salt Lake City",
"UT", "12121"));
ben.setWorkAddress(
new Address(
"5600 North 2700 East Rd", "Salt Lake City",
"UT", "12121"));
ben.setVacationAddress(
new Address(
"2700 East 5500 North Rd", "Salt Lake City",
"UT", "12121"));
// Ben really needs to get out more
db.set(ben);

db.commit();
}

跟本系列早先的探察測試示例一樣,在每次測試完成後,我使用帶 @After 註釋的 deleteDatabase() 方法來刪除數據庫,以使各部分能夠很好地分隔開。

讓我們運行幾個查詢……

在實際運行這個方法之前,我將會檢查在系統中使用 Employee 會有哪些效果(如果有的話)。希望從數據庫獲取所有 Employee 信息,這很正常 — 或許當公司破產時他們將會全部被解僱(是的,我知道,這樣想很殘酷,但我只是對 2001 年的 dot-bomb 事故還有點心有餘悸)。最初的測試看起來很簡單,正如清單 4 所示:


清單 4. Ted 說,“你被解僱了!”
                
@Test public void testSimpleInheritanceQueries()
{
ObjectSet employees = db.get(Employee.class);
while (employees.hasNext())
System.out.println("Found " + employees.next());
}

當進行測試時,將會產生一個有趣的結果:數據庫(我自己、Ben、Neal、Brian 和 Bruce)中只返回了 Employee。OODBMS 識別出查詢受到子類型 Employee 的顯式約束,並且只選擇了符合返回條件的對象。因爲其他對象(配偶或者孩子)不屬於 Employee 類型,他們不符合條件,所以沒有被返回。

當運行一個返回所有 Person 的查詢時,將會更加有趣,如下所示:


清單 5. 找到所有人!
                
@Test public void testSimpleNonEmployeeQuery()
{
ObjectSet persons = db.get(Person.class);
while (persons.hasNext())
System.out.println("Found " + persons.next());
}

當運行這個查詢時,每個單一對象 — 包括以前返回的所有 Employee — 都被返回了。從某種程度上說,這是有意義的。因爲 Employee 是一個 Person,由於建立在 Java 代碼中的實現繼承關係,因此滿足返回查詢的必須條件。

db4o 中的繼承(以及多態)其實就是這麼簡單。沒有用於查詢語言的複雜的 IS 擴展,就不會引入不同於 Java 類型系統中現有概念的 “類型” 概念。我所指的只是期望作爲查詢的一部分的類型,而且這些是構成查詢的主要成分。這跟在 SQL 查詢中加入表格很相似,方法就是選擇其數據應爲查詢結果一部分的表格。額外的好處是,“父類型” 也是作爲查詢的一部分隱式地 “加入” 的。清單 6 顯示了 清單 3InheritanceTest 的輸出:


清單 6. 多態發揮作用
                
.Found [Employee: Ted Neward (President and CEO)]
Found [Person: firstName = Charlotte lastName = Neward gender = FEMALE age = 35
mood = HAPPY spouse = Ted ]
Found [Person: firstName = Michael lastName = Neward gender = MALE age = 14 mood
= CRANKY ]
Found [Person: firstName = Matthew lastName = Neward gender = MALE age = 8 mood
= CRANKY ]
Found [Employee: Bruce Tate (Chief Technical Officer)]
Found [Person: firstName = Maggie lastName = Tate gender = FEMALE age = 29 mood
= HAPPY spouse = Bruce ]
Found [Person: firstName = Kayla lastName = Tate gender = FEMALE age = 0 mood =
CRANKY ]
Found [Person: firstName = Julia lastName = Tate gender = FEMALE age = 0 mood =
CRANKY ]
Found [Employee: Neal Ford (Meme Wrangler)]
Found [Person: firstName = Candi lastName = Ford gender = FEMALE age = 29 mood =
HAPPY spouse = Neal ]
Found [Employee: Brian Sletten (Bosatsu Master)]
Found [Person: firstName = Kristen lastName = Sletten gender = FEMALE age = 29 m
ood = HAPPY spouse = Brian ]
Found [Employee: Ben Galbraith (Chief UI Director)]
Found [Person: firstName = Jessica lastName = Galbraith gender = FEMALE age = 29
mood = HAPPY spouse = Ben ]

您可能會感到奇怪,不管如何查詢,返回的對象仍然是適當的子類型對象。例如,跟預期的一樣,在上面的查詢中當 toString() 被每個返回的 Person 對象調用時,Person.toString() 也正被每個 Person 調用。然而,因爲 Employee 有一個重寫的 toString() 方法,因此關於動態綁定的常用規則就不適用了。存儲在 Employee 中的 Person 的各部分不會被 “切掉”,而當定期 SQL 查詢未能成功地將派生子類表加入到 table-per-class 模型中時,這種被 “切掉” 的現象就會發生。





原生繼承

當然,當繼承條件擴展到原生查詢中時,其功能就跟我所做過的簡單對象查詢一樣強大。進行調用時,查詢語法將會更加複雜,但是基本上遵循我以前所使用的語法,如清單 7 所示:


清單 7. 你是單身嗎?
                
@Test public void testNativeQuery()
{
List<Person> spouses =
db.query(new Predicate<Person>() {
public boolean match(Person candidate) {
return (candidate.getSpouse() instanceof Employee);
}
});
for (Person spouse : spouses)
System.out.println(spouse);
}

下面的查詢與我以前做過的查詢在思想上類似,考慮系統中所有的 Person,但是設置一個約束條件,只查找配偶也是一個 EmployeePerson — 調用 getSpouse(),將返回值傳遞給 Java instanceof 運算符,這樣就完成了查詢(記住 match() 調用只返回 true 或者 false,表示候選對象是否應該返回)。

請注意如何通過更改在 query() 調用中傳遞的 Predicate 來更改隱式選擇的類型條件,如清單 8 所示:


清單 8. 哇!辦公室戀情!
                
@Test public void testEmployeeNativeQuery()
{
List<Employee> spouses =
db.query(new Predicate<Person>() {
public boolean match(Person candidate) {
return (candidate.getSpouse() instanceof Employee);
}
});
for (Person spouse : spouses)
System.out.println(spouse);
}

當執行此查詢時,不會產生什麼結果,因爲現在此查詢只查找配偶也在公司工作的 Employee。目前,數據庫中的僱員都不滿足這個條件。如果公司僱傭 Charlotte,那麼會返回兩個 Employee:Ted 和 Charlotte(但是人家說辦公司戀情永遠不會發生)。

在很大程度上,就是這樣。繼承不會對更新、刪除和激活深度產生任何影響,只會影響到對象的查詢方面。但是回想起 Java 平臺提供的兩種形式的繼承:實現繼承和接口繼承。前者通過各種 extends 子句來實現,而後者通過 implements 來實現。如果 db4o 支持 extends,那麼它也一定支持 implements,您將會看到,這有利於實現強大的查詢功能。





都是關於接口的

就像任何 Java (或 C#)編程人員使用了一段時間這種語言後認識到的,接口對於建模非常有用。儘管不會經常看到,接口具有強大的 “隔離” 交叉在傳統實現繼承行中的對象的能力;通過使用接口,我可以聲明某些類型爲 Comparable 或者 Serializable,或者在本例中,Employable(是的,從設計的角度說這是大材小用了,但是用於教學還是很不錯的)。


清單 9. 嘿,不再是 2001 年了!來爲我工作吧!
                
package com.tedneward.model;

public interface Employable
{
public boolean willYouWorkForUs();
}

角色和對象

一些對象模型將不適合我用接口和繼承來模擬 Person 扮演的角色。例如,假設一個 Employee 的配偶決定也來此公司工作,有必要將他們從系統的 Person 中刪除,然後重新插入到 Employee 中嗎? 隨着時間的流逝,角色也可能並且經常變化。我們不要期望更改對象的基類和接口類型,以適應角色的轉變。

這是一個很普通的爭議,如果能夠從根本上解決的話,不屬於本文討論的範圍。目前我們只能說,我對繼承和接口的使用純粹是出於演示和教學的目的。要想進一步學習,請參考 參考資料 中的 “Role object” 一節。

要看接口是如何工作的,我需要 Employable 接口的一個實體類繼承,並且 — 或許您已經猜測到 — 這意味着創建一個 EmployablePerson 子類型來擴展 Person 和實現 Employable。我不會再次演示這些代碼(沒有必要演示,除了將 ** EMPLOYABLE ** 添加到 PersontoString() 末尾以外, PersonEmployablePerson.toString() 方法中)。我也會修改 prepareDatabase() 調用以返回 “Charlotte 是一個 EmployablePerson,而不只是一個 Person” 的事實。

現在,我會編寫一個遍歷數據庫的查詢,查找願意爲本公司工作的僱員的配偶或親人,如清單 10 所示。


清單 10. 有工作了,來看看吧……
                
@Test public void testEmployableQuery()
{
List<Employable> potentialEmployees =
db.query(new Predicate<Employable>() {
public boolean match(Employable candidate) {
return (candidate.willYouWorkForUs());
}
});
for (Employable e : potentialEmployees)
System.out.println("Eureka! " + e + " has said they'll work for us!");
}

毫無疑問,Charlotte 被返回了,說明她可能爲本公司工作。更好的是,這意味着我引入的任何接口都變成了一種限制查詢的新方式,不需要人工添加包含此信息的字段;只有 Charlotte 符合查詢條件,因爲她實現了這個接口,而其他配偶都沒有實現(至少到目前爲止)。

結束語

如 果說對象和繼承就像巧克力和花生醬的話,那對象和多態就好比手和手套。這兩個元素就像經理和他/她的高薪一樣般配。檢索數據時,任何存儲對象的系統都不得 不將繼承的概念引入它的存儲媒介和過濾器中。幸運的是,面向對象的 DBMS 使得這很容易實現,而且不必引入新的 query-predicate 術語。從長遠看來,引入繼承會使 OODBMS 容易使用 得多

 

來源:IBM developerworks(http://www.ibm.com/developerworks/cn/views/java/articles.jsp?view_by=search&search_by=db4o)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章