db4o 面向对象数据库的安装使用
from IBM
这篇文章是 开源面向对象数据库 db4o 之旅 系列文章的第二篇,介绍了面向对象数据库 db4o 的安装、启动以及三种查询语言,并对三种查询语言做了比较。
在 开源面向对象数据库 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:
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:
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 所示。
“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。
|
和 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,你可以通过 [email protected] 和他联系。 |
|
Chris 来自香港,热爱开源和 db4o。他创办了中国最火热的 Java 和开源社区 Matrix(http://www.Matrix.org.cn), 你可以通过 [email protected] 和他联系。 |
|
张黄瞩,热爱开源软件,熟悉 Java/C/C++ 编程语言,对数据库技术网络技术均感兴趣。你可以通过 [email protected] 联系他。 |