在 開源面向對象數據庫 db4o 之旅 系列文章的第一部分:初識 db4o 中,作者介紹了 db4o 的歷史和現狀,應用領域,以及和 ORM 等的比較。在這篇文章中,作者將會介紹 db4o 的安裝、啟動以及三種不同的查詢方式:QBE(Query by Example)、SODA(Simple Object Database Access) 以及 NQ(Native Queries),并分別通過這三種不同的途徑實現了兩個關聯對象的查詢。本文還示范了開發中最經常用到的幾個典型功能的 db4o 實現。
db4o 所有最新的版本都可以直接在官方網站上下載,進入 db4o 的下載頁面,我們可以看到最新的 for Java 穩定版本是 5.5,包括 JAR、源代碼、入門文檔、API 等內容的完整的打包文件只有 6 MB,db4o 還有一個對象數據庫管理工具 ObjectManager,目前版本是 1.8(請在參考資源中下載)。
接著在 Eclipse 中新建 Java 項目,把 db4o 對象數據庫引擎包 db4o-5.5-java5.jar 導入進項目。由于 db4o 支持多種版本的 JDK,除了 for JDK 5.0 的 db4o-5.5-java5.jar 外,還有 for JDK 1.1、1.2-1.4 的 JAR 包,以適應多種環境。與 Hibernate、iBATIS SQL Maps 相比,db4o 更加自然,無需過多地引用第三方支持庫。
db4o 怎樣進行對象持久化呢?通過瀏覽目錄可以發現,與傳統的 RDBMS 一樣,db4o 也有自己的數據庫文件, 在 db4o 中數據庫文件的后綴名是“*.yap”。讓我們先來了解一下 db4o 對象數據庫引擎的主要包結構:
-
com.db4o
com.db4o 包含了使用 db4o 時最經常用到的功能。兩個最重要的接口是 com.db4o.Db4o 和 com.db4o.ObjectContainer。com.db4o.Db4o 工廠是運行 db4o 的起點,這個類中的靜態方法可以開啟數據庫文件、啟動服務器或連接一個已經存在的服務器,還可以在開啟數據庫之前進行 db4o 環境配置。com.db4o.ObjectContainer 接口很重要,開發過程中 99% 的時間都會用到它,ObjectContainer 可在單用戶模式下作為數據庫實例,也可作為 db4o 服務器的客戶端。每個 ObjectContainer 實例都有自己的事務。所有的操作都有事務保證。當打開 ObjectContainer,就已經進入事務了,commit() 或 rollback() 時,下一個事務立即啟動。每個 ObjectContainer 實例維護它自己所管理的已存儲和已實例化對象,在需要 ObjectContainer 的時候,它會一直保持開啟狀態,一旦關閉,內存中數據庫所引用的對象將被丟棄。 -
com.db4o.ext
你也許想知道為什么在 ObjectContainer 中只能看見很少的方法,原因如下:db4o 接口提供了兩個途徑,分別在 com.db4o 和 com.db4o.ext 包中。這樣做首先是為了讓開發者能快速上手;其次為了讓其他產品能更容易的復制基本的 db4o 接口;開發者從這一點上也能看出 db4o 是相當輕量級的。每個 com.db4o.ObjectContainer 對象也是 com.db4o.ext.ExtObjectContainer 對象。可以轉換成 ExtObjectContainer 獲得更多高級特性。 -
com.db4o.config
com.db4o.config 包含了所有配置 db4o 所需的類。 -
com.db4o.query
com.db4o.query 包包含了構造“原生查詢, NQ(Native Queries)”所需的 Predicate 類。NQ 是 db4o 最主要的查詢接口。
db4o 提供兩種運行模式,分別是本地模式和服務器模式。本地模式是指直接在程序里打開 db4o 數據庫文件進行操作:
ObjectContainer db = Db4o.openFile("auto.yap"); |
而服務器模式則是客戶端通過 IP 地址、端口以及授權口令來訪問服務器:
服務器端:ObjectServer server=Db4o.openServer("auto.yap",1212); server.grantAccess("admin","123456"); |
客戶端:
ObjectContainer db=Db4o.openClient("192.168.0.10",1212,"admin","123456"); |
兩種方式都可以得到 ObjectContainer 實例,就目前 Java EE 應用環境來看,服務器模式更有現實意義;而本地模式更適合于嵌入式應用。為了簡化演示,本文在下面的例子都將采用本地模式。
在下面的例子里,我們都會用到下面兩個對象: People 和 AutoInfo 對象。
People 對象:
清單1. People 對象
package bo; public class People { private java.lang.Integer _id; private java.lang.String _name; private java.lang.String _address; private java.util.List<AutoInfo> _autoInfoList; public java.lang.Integer getId() { return _id; } public void setId(java.lang.Integer _id) { this._id = _id; } public java.lang.String getName() { return _name; } public void setName(java.lang.String _name) { this._name = _name; } public java.lang.String getAddress() { return _address; } public void setAddress(java.lang.String _address) { this._address = _address; } public java.util.List<AutoInfo> getAutoInfoList() { return this._autoInfoList; } public void addAutoInfo(AutoInfo _autoInfoList) { if (null == this._autoInfoList) this._autoInfoList = new java.util.ArrayList<AutoInfo>(); this._autoInfoList.add(_autoInfoList); } } |
AutoInfo 對象:
清單2. AutoInfo 對象
package bo; public class AutoInfo{ private java.lang.Integer _id; private java.lang.String _licensePlate; private bo.People _ownerNo; public java.lang.Integer getId () { return _id; } public void setId (java.lang.Integer _id) { this._id = _id; } public java.lang.String getLicensePlate () { return _licensePlate; } public void setLicensePlate (java.lang.String _licensePlate) { this._licensePlate = _licensePlate; } public bo.People getOwnerNo () { return this._ownerNo; } public void setOwnerNo (bo.People _ownerNo) { this._ownerNo = _ownerNo; } } |
利用 set 方法把新對象存入 ObjectContainer,而對 ObjectContainer 中已有對象進行 set 操作則是更新該對象。db4o 保存數據庫很簡單,下面就是一個段完整的保存對象的代碼:
AutoInfo 對象:
清單3
package com; import bo.AutoInfo; import bo.People; import com.db4o.Db4o; import com.db4o.ObjectContainer; public class DB4OTest{ public static void main(String[] args){ //打開數據庫 ObjectContainer db = Db4o.openFile("auto.yap"); try{ //構造 People 對象 People peo = new People(); peo.setId(1); peo.setAddress("成都市"); peo.setName("張三"); //構造 AutoInfo 對象 AutoInfo ai = new AutoInfo(); ai.setId(1); ai.setLicensePlate("川A00000"); //設置 People 和 AutoInfo 的關系 ai.setOwnerNo(peo); peo.addAutoInfo(ai); //保存對象 db.set(peo); }finally{ //關閉連接 db.close(); } } } |
當我們運行上述代碼,db4o 會自動創建“auto.yap”文件。讓我們來看看到底保存成功沒有,打開 ObjectManager 工具,圖 1 所示。
圖1. 對象數據庫管理工具

“File”->“Open File”->選擇剛才我們保存的“auto.yap”文件(“auto.yap”文件可在項目的根目錄下找到),最新的 ObjectManager 1.8 版本為我們提供了“Read Only”方式讀取數據庫文件,避免 ObjectManager 占用數據庫文件所導致的程序異常。
打開之后,圖 2 所示,剛才存貯的 People 對象已經在數據庫中了,并且還可以很直觀的看到 AutoInfo 對象也放入了 ArrayList 中。這種可視化的對象關系有利于我們對數據的理解,是傳統 RDBMS 無法比擬的。有些開發者會說 ObjectManager 工具略顯簡單,這點我想隨著 db4o 的不斷發展會加入更多的特性。在這個工具中,我們意外的發現了 Java 集合對象的蹤影,db4o 把與 ArrayList 有直接關系的所有接口和父類都保存了,這樣顯得更直觀。
在此,我保留了 _id 屬性,這是因為通常在 Java EE 環境中,DAO 第一次不是把整個對象都返回到表現層,而是只返回了“標題”、“發布時間”這些信息(并隱式的返回id),接著 DAO 與數據庫斷開;要查看詳情(比如文章內容)就需要進行 findById 操作,這時 DAO 要再次與數據庫交互,只有唯一標識符才能正確地找到對象。這種懶加載方式也是很多書籍所推薦的。
回到本文的范例程序中,這個 _id 屬性可由人工編碼實現的“序列”進行賦值,當然 db4o 也提供了內部標識符 Internal IDs,圖 2 中的 id=1669;以及 UUIDs。
圖2. 對象結構

和 RDBMS 一樣,db4o 也有自己的查詢語言,分別是 QBE(Query by Example)、NQ(Native Queries)、SODA(Simple Object Database Access),db4o 更推薦使用 NQ 進行查詢。NQ 方式提供了非常強大的查詢功能,支持原生語言,也就意味著你可以使用 Java 來判斷該對象是否符合條件,這是其他數據庫查詢語言無法比擬的。在某些情況下, db4o 核心會將 NQ 翻譯成 SODA 以獲得更高的性能。下面詳細介紹一下這三種查詢語言。
QBE 規范可在這里下載。QBE 最初由 IBM 提出,同時業界也有許多和 QBE 兼容的接口,包括著名的 Paradox。有些系統,比如微軟的 Access,它的基于表單的查詢也是受到了部分 QBE 思想的啟發。在 db4o 中,用戶可借用 QBE 快速上手,可以很容易適應 db4o 存取數據的方式。
當利用 QBE 為 db4o 提供模板(example)對象時,db4o 將返回所有和非默認值字段匹配的全部對象。內部是通過反射所有的字段和構造查詢表達式(所有非默認值字段結合”AND”表達式)來實現。
例如,利用 QBE 查找到車牌號為“川A00000”的車主姓名,這是一個級聯查詢。
清單4
package com; import java.util.List; import bo.AutoInfo; import com.db4o.Db4o; import com.db4o.ObjectContainer; public class DB4OTest{ public static void main(String[] args){ //打開數據庫 ObjectContainer db = Db4o.openFile("auto.yap"); try{ //構造模板對象 AutoInfo ai = new AutoInfo(); ai.setLicensePlate("川A00000"); //查詢對象 List<AutoInfo> list = db.get(ai); for(int x = 0; x < list.size(); x++){ System.out.println("車主姓名:"+list.get(x).getOwnerNo().getName()); } }finally{ //關閉連接 db.close(); } } } |
但是 QBE 也有明顯的限制:db4o 必須反射模板(example)對象的所有成員;無法執行更進一步的查詢表達式(例如 AND、OR、NOT 等等);不能約束 0(整型)、””(空字符串)或者 null(對象),因為這些都被認為是不受約束的。要繞過這些限制,db4o 提供了 NQ(Native Queries)。
SODA(Simple Object Database Access)
SODA ,簡單對象數據庫訪問,請查看官方站點,其中一位主要維護者是 Carl Rosenberger,Carl 正是 db4o 首席架構師。
SODA 就是一種與數據庫通訊的對象 API。最終的目標是實現類型安全、對象復用、最小的字符串使用、與編程語言無關等特性。SODA 是 db4o 最底層的查詢 API,目前 SODA 中使用字符串來定義字段,這樣將不能實現類型安全也無法在編譯時檢查代碼,而且寫起來較麻煩,當然要達到設計目標這個階段是必須的。大部分情況下 NQ(Native Queries)是很好的查詢接口,不過遇到動態生成查詢的時候 SODA 就大有作為了。
通過 SODA 查找到車牌號為“川A00000”的車主姓名:
清單5
package com; import java.util.List; import bo.AutoInfo; import com.db4o.Db4o; import com.db4o.ObjectContainer; import com.db4o.query.Query; public class DB4OTest{ public static void main(String[] args){ //打開數據庫 ObjectContainer db = Db4o.openFile("auto.yap"); try{ //構造查詢對象 Query query=db.query(); //設置被約束實例 query.constrain(AutoInfo.class); //設置被約束實例的字段和約束條件 query.descend("_licensePlate").constrain("川A00000"); //查詢對象 List<AutoInfo> list = query.execute(); for(int x = 0; x < list.size(); x++){ System.out.println("車主姓名:"+list.get(x).getOwnerNo().getName()); } }finally{ //關閉連接 db.close(); } } } |
通過 API,發現 Query 實例增加了 sortBy 按字段排序方法和 orderAscending正序、orderDescending 倒序排列方法,SODA 比 QBE 更進了一步。
精彩總是在最后出場,NQ 才是 db4o 查詢方式中最精彩的地方!有沒有想過用你熟悉的的編程語言進行數據庫查詢呢?要是這樣,你的查詢代碼將是 100% 的類型安全、100% 的編譯時檢查以及 100% 的可重構,很奇妙吧?NQ 可以做到這些。
有兩篇論文專門講解了 NQ 的基本概念和設計思路,分別是 《Cook/Rosenberger,持久對象原生數據庫查詢語言》 和 《Cook/Rai,Safe Query Objects: Statically Typed Objects as Remotely Executable Queries》。作為結果集的一部分,NQ 表達式必須返回 true 值來標記特定實例。如果可能的話 db4o 將嘗試優化 NQ 表達式,并依賴索引來運行表達式。
通過 NQ 查找到車牌號為“川A00000”的車主姓名:
清單6
package com; import java.util.List; import bo.AutoInfo; import com.db4o.Db4o; import com.db4o.ObjectContainer; import com.db4o.query.Predicate; public class DB4OTest{ public static void main(String[] args){ //打開數據庫 ObjectContainer db = Db4o.openFile("auto.yap"); try{ List <AutoInfo> list = db.query(new Predicate<AutoInfo>() { public boolean match(AutoInfo ai) { //這樣才是類型安全的 return ai.getLicensePlate().equals("川A00000"); } }); for(int x = 0; x < list.size(); x++){ System.out.println(list.get(x).getOwnerNo().getName()); } }finally{ //關閉連接 db.close(); } } } |
必須指出 NQ 的一個的問題是:在內部,db4o 設法把 NQ 轉換成 SODA。但并不是所有的查詢表達式都可以成功轉換。有些查詢表達式的流向圖(flowgraph)非常難于分析。這種情況下,db4o 將不得不實例化一些持久對象來真實地運行 NQ 表達式。
正在開發中的 NQ 查詢優化器就可以化解這個障礙,它將分析 NQ 表達式的每個部分,以確保最少量的實例化對象,以此提高性能。當然,優化器的不是靈丹妙藥,關鍵還需要自己多優化代碼。
開發 Java EE 項目經常會用到分頁,怎樣用 NQ 實現呢?向數據庫寫入六條記錄:
清單7
package com; import java.util.List; import bo.AutoInfo; import com.db4o.Db4o; import com.db4o.ObjectContainer; import com.db4o.query.Predicate; public class DB4OTest{ public static void main(String[] args){ //打開數據庫 ObjectContainer db = Db4o.openFile("auto.yap"); try{ List<AutoInfo> list = db.query(new Predicate<AutoInfo>() { public boolean match(AutoInfo ai) { return true; } }); //記錄總數 Integer count = list.size(); //每頁兩條,分三頁 for(int x = 0; x < 3; x++){ System.out.println("第"+x+"頁:"+list.get(x*2).getLicensePlate()); System.out.println("第"+x+"頁:"+list.get(x*2+1).getLicensePlate()); } }finally{ //關閉連接 db.close(); } } } |
我們發現,在進行 NQ 查詢時并沒有加入任何條件(無條件返回 true),是不是相當于遍歷了整個數據庫?db4o 的設計者早就想到了這個問題,當 db.query() 執行完畢返回 list 實例的時候,db4o 只是與數據庫同步取出內部 IDs 而已,并沒有把所有的 AutoInfo 對象全部取出,只有在 list.get(x*2).getLicensePlate() 之后才會去根據 IDs 取出記錄。所以不必擔心性能問題。
db4o 為開發者提供了多種查詢方式,這些方式都很靈活。要引起大家注意的是:靈活在帶來便利的同時也對開發者自身素質提出了更高的要求,(比如排序,既可以用 SODA 也可以用 Java 集合對象實現)在開發過程中一定要形成某種統一的開發模式,這樣 db4o 才能最高效能地為我所用。
學習
- “面向對象數據庫 db4o 之旅,第 1 部分:初識 db4o”:(developerWorks Java ,2006 年 10月):介紹了 db4o 的歷史和現狀,應用領域,以及和 ORM 等的比較。
- db4o 開發者論壇:討論 db4o 技術。
- db4o 中國開發者論壇:討論 db4o 技術的中文論壇。
- ODMG 官方網站:了解 ODMG 技術。
- SODA 官方網站:了解簡單對象數據庫訪問技術。
- Java 技術專區:這里有數百篇有關 Java 編程各方面的文章。
- developerWorks Java 論壇:學習 Java 技術
獲得產品和技術
作者簡介
? Rosen Jiang 來自成都,是 db4o 和 OO 的忠實 fans,是 2005 年 db4o 的 dvp 獲得者之一。他正在 J2me 應用中使用 db4o,你可以通過 rosener_722@hotmail.com 和他聯系。
? Chris 來自香港,熱愛開源和 db4o。他創辦了中國最火熱的 Java 和開源社區 Matrix(http://www.Matrix.org.cn), 你可以通過 chris@Matrix.org.cn 和他聯系。
?
? 張黃矚,熱愛開源軟件,熟悉 Java/C/C++ 編程語言,對數據庫技術網絡技術均感興趣。你可以通過 zhanghuangzhu@gmail.com 聯系他。
IBM DeveloperWorks(IBM DW) 版權所有!引用、轉貼本文應注明本文來自 IBM DW。