2006 年 11 月 20 這篇文章是 開源面向對象數據庫 db4o 之旅 系列文章的第二篇,介紹了面向對象數據庫 db4o 的安裝、啟動以及三種查詢語言,並對三種查詢語言做了比較。
前言
在 開源面向對象數據庫 db4o 之旅 系列文章的第一部分:初識 db4o 中,作者介紹了 db4o 的歷史和現狀,應用領域,以及和 ORM 等的比較。在這篇文章中,作者將會介紹 db4o 的安裝、啟動以及三種不同的查詢方式:QBE(Query by Example)、SODA(Simple Object Database Access) 以及 NQ(Native Queries),並分別通過這三種不同的途徑實現了兩個關聯對象的查詢。本文還示范了開發中最經常用到的幾個典型功能的 db4o 實現。
下載和安裝 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 對象數據庫引擎的主要包結構:
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:
清單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:
清單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:
清單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(Query by Example)
QBE 規范可在這裡下載。QBE 最初由 IBM 提出,同時業界也有許多和 QBE 兼容的接口,包括聞名的 Paradox。有些系統,比如微軟的 Access,它的基於表單的查詢也是受到了部分 QBE 思想的啟發。在 db4o 中,用戶可借用 QBE 快速上手,可以很輕易適應 db4o 存取數據的方式。
當利用 QBE 為 db4o 提供模板(example)對象時,db4o 將返回所有和非默認值字段匹配的全部對象。內部是通過反射所有的字段和構造查詢表達式(所有非默認值字段結合”AND”表達式)來實現。
例如,利用 QBE 查找到車牌號為“川A00000”的車主姓名,這是一個級聯查詢。清單4:
清單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:
清單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(Native Queries)
出色總是在最後出場,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:
清單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(); } } }
QQRead.com 推出數據恢復指南教程 數據恢復指南教程 數據恢復故障解析 常用數據恢復方案 硬盤數據恢復教程 數據保護方法 數據恢復軟件 專業數據恢復服務指南
必須指出 NQ 的一個的問題是:在內部,db4o 設法把 NQ 轉換成 SODA。但並不是所有的查詢表達式都可以成功轉換。有些查詢表達式的流向圖(flowgraph)非常難於分析。這種情況下,db4o 將不得不實例化一些持久對象來真實地運行 NQ 表達式。
正在開發中的 NQ 查詢優化器就可以化解這個障礙,它將分析 NQ 表達式的每個部分,以確保最少量的實例化對象,以此提高性能。當然,優化器的不是靈丹妙藥,要害還需要自己多優化代碼。
開發 Java EE 項目經常會用到分頁,怎樣用 NQ 實現呢?向數據庫寫入六條記錄。清單7:
清單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 才能最高效能地為我所用。
參考資料
學習
獲得產品和技術
作者簡介
Rosen Jiang 來自成都,是 db4o 和 OO 的忠實 fans,是 2005 年 db4o 的 dvp 獲得者之一。他正在 J2me 應用中使用 db4o,你可以通過 [email protected] 和他聯系。
Chris 來自香港,熱愛開源和 db4o。他創辦了中國最火熱的 Java 和開源社區 Matrix(http://www.Matrix.org.cn), 你可以通過 [email protected] 和他聯系。
張黃矚,熱愛開源軟件,熟悉 Java/C/C++ 編程語言,對數據庫技術網絡技術均感愛好。你可以通過 [email protected] 聯系他。