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 对象数据库引擎的主要包结构:
com.db4ocom.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 应用环境来看,服务器模式更有现实意义;而本地模式更适合于嵌入式应用。为了简化演示,本文在下面的例子都将采用本地模式。
(本文来源于图老师网站,更多请访问http://m.tulaoshi.com/bianchengyuyan/)在下面的例子里,我们都会用到下面两个对象: 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.ListAutoInfo _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.ListAutoInfo getAutoInfoList() {return this._autoInfoList;}public void addAutoInfo(AutoInfo _autoInfoList) {if (null == this._autoInfoList)this._autoInfoList = new java.util.ArrayListAutoInfo();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;}}
(本文来源于图老师网站,更多请访问http://m.tulaoshi.com/bianchengyuyan/)
利用 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");//查询对象ListAutoInfo 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");//查询对象ListAutoInfo 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 PredicateAutoInfo() {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{ListAutoInfo list = db.query(new PredicateAutoInfo() {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 开发者论坛:讨论 db4o 技术。
db4o 中国开发者论坛:讨论 db4o 技术的中文论坛。
ODMG 官方网站:了解 ODMG 技术。
SODA 官方网站:了解简单对象数据库访问技术。
Java 技术专区:这里有数百篇有关 Java 编程各方面的文章。
developerWorks Java 论坛:学习 Java 技术
获得产品和技术
下载 db4o 对象数据库治理工具 ObjectManager 1.8 for Windows。
作者简介
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 联系他。