第一部分 简介和概览
早就听说数据库间的大战以关系型数据库的胜利告终。然而,这之后,编程界真的就风平浪静、一片繁荣了吗?持肯定观点的人恐怕最近都没有试过使用关系数据库来支持 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
类的一个实例放入数据库,需要如下几个步骤:
- 需要创建关系模式,向数据库描述类型。
- 需要创建映射文件,用这些文件将列和数据库的表映射到域模型的类和字段。
- 在代码中,需要通过 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
类型,并且有效地忽略 lastName
和 age
字段。在表中,这个调用基本上相当于 SQL 查询的 SELECT * FROM Person WHERE firstName = "Brian"
。(虽然如此,在尝试将 OODBMS 查询映射到 SQL 时还是要谨慎一些:这种类比并不完善,并且会对特定查询的性质和性能产生误解)。
查询返回的对象是一个 ObjectSet
,它类似于一个 JDBC ResultSet
(一个简单的对象容器)。使用由 ObjectSet
Iterator
接口遍历结果非常简单。使用 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 identifier 或 OID(其发音类似于 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 { 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 drinkers = db.query(new Predicate() { 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 maleGalbraiths = db.query(new Predicate() { 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 maleGalbraiths = db.query(new Predicate() { 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 中,使用 ObjectClass
为 Person
类型配置默认激活深度:
清单 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 maleGalbraiths = db.query(new Predicate() { 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 maleGalbraiths = db.query(new Predicate() { 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 maleGalbraiths = db.query(new Predicate() { 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 results = db.get(new Person("Jessica", "Galbraith", null, 0, null)); assertFalse(results.hasNext()); }
|
执行该操作时要小心,因为它意味着其他引用了被消除层叠的对象的对象将拥有一个对 null 的引用 —— db4o 对象数据库在防止删除被引用对象上使用的引用一致性 在这里没有什么作用。 (引用一致性是 db4o 普遍需要的特性,据说开发团队正在考虑在未来某个版本中加入这一特性。对于使用 db4o 的开发人员来说,关键在于要以一种不违反最少意外原则 的方式实现,甚至某些时候, 即使是在关系数据库中, 打破一致性规则实际上也是一种理想的实践。)
结束语
本文是该系列文章的分水岭:在此之前, 我使用的所有示例都基于非常简单的对象,从应用角度来讲, 那些例子都不现实,其主要作用只是为了使您理解 OODBMS,而不是被存储的对象。 理解像 db4o 这样的 OODBMS 是如何通过引用存储相关对象,是比较复杂的事情。 幸运的是,一旦您掌握了这些行为(通过解释和理解),您所要做的就只是 开始调整代码来实现这些行为。
在本文中,您看到了一些基本例子,通过调整复杂代码来实现 db4o 对象模型。 学习了如何对结构化对象执行一些简单 CRUD 操作,同时,也看到了一些 不可避免的问题和解决方法。
其实,目前的结构化对象例子仍然比较简单, 对象之间还只是直接引用关系。 许多夫妻都知道,结婚一段时间后,孩子将会出现。 本系列的下一文章中,我将继续 探索 db4o 中的结构化对象的创建与操作,看看在引入若干子对象后, ben
和 jess
对象将发生什么。
第五部分 数组和集合
集合和数组为 面向 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
定义一个新字段:一个 Person
ArrayList
,它同样也有一些相关的方法,以便进行适当的封装。
接下来,由于大多数小孩都有父母,我还将添加两个字段来表示母亲和父亲,并增加适当的 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 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 children = new ArrayList(); 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 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 maleTates = db.query(new Predicate() { 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 maleTates = db.query(new Predicate() { 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 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 girls = db.query(new Predicate() { 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 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 children = new ArrayList(); 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
中的其他方法意义不大。在本讨论中需要记住的是 Employee
是 Person
的一个子类(如果更加关心系统的建模过程,可以设想 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 显示了 清单 3 中 InheritanceTest
的输出:
清单 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 spouses = db.query(new Predicate() { public boolean match(Person candidate) { return (candidate.getSpouse() instanceof Employee); } }); for (Person spouse : spouses) System.out.println(spouse); } |
下面的查询与我以前做过的查询在思想上类似,考虑系统中所有的 Person
,但是设置一个约束条件,只查找配偶也是一个 Employee
的 Person
— 调用 getSpouse()
,将返回值传递给 Java instanceof
运算符,这样就完成了查询(记住 match()
调用只返回 true 或者 false,表示候选对象是否应该返回)。
请注意如何通过更改在 query()
调用中传递的 Predicate
来更改隐式选择的类型条件,如清单 8 所示:
清单 8. 哇!办公室恋情!
@Test public void testEmployeeNativeQuery() { List spouses = db.query(new Predicate() { 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 **
添加到 Person
的 toString()
末尾以外, Person
在 EmployablePerson.toString()
方法中)。我也会修改 prepareDatabase()
调用以返回 “Charlotte 是一个 EmployablePerson
,而不只是一个 Person
” 的事实。
现在,我会编写一个遍历数据库的查询,查找愿意为本公司工作的雇员的配偶或亲人,如清单 10 所示。
清单 10. 有工作了,来看看吧……
@Test public void testEmployableQuery() { List potentialEmployees = db.query(new Predicate() { 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)