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; }
在基于 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 在某些方面非常类似于某些对象关系型映射库的操作方式,原因是对象检索最简单的形式就是按例查询(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);
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 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);
在前面的示例中,查询所有 firstName 字段等于 “Brian” 的 Person 类型,并且有效地忽略 lastName 和 age 字段。在表中,这个调用基本上相当于 SQL 查询的 SELECT * FROM Person WHERE firstName = "Brian"。(虽然如此,在尝试将 OODBMS 查询映射到 SQL 时还是要谨慎一些:这种类比并不完善,并且会对特定查询的性质和性能产生误解)。
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);
// ... 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 岁。)
// ... 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() 将该值放入我的模板对象中。
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; } }
// 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 只能执行相等匹配,而无法进行比较查询。
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();
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 对象发回给对全部集合执行谓词的客户机(换言之,正是早先拒绝使用的方法)
// ... 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!"); } } }); }
如果您已经阅读了本系列中的前两篇文章,那么应该熟悉我的非常简单的数据库。目前,它由一种类型组成,即 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; }
// 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);
第二步,我需要更改 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 也是这样做的。)
// 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();
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]
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”。
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); }
考察 db4o 的这个领域是一项棘手的任务,也给了我一个机会 展示一位好友教给我的策略:探察测试。(感谢 Stu Halloway,据我所知,他是第一个拟定该说法的人。) 探察测试,简要而言,是一系列单元测试,不仅测试待查的库,还可探究 API 以确保库行为与预期一致。该方法具有一个有用的副作用,未来的库版本可以放到探察测试代码中,编译并且测试。如果代码不能编译或者无法通过所有的探察测试,则显然意味着库没有做到向后兼容,您就可以在用于生产系统之前发现这个问题。
对 db4o API 的探察测试使我能够使用一种 “before” 方法来创建数据库并使用 Person 填充数据库,并使用 “after” 方法来删除数据库并消除测试过程中发生的误判(false positive)。若非如此,我将不得不记得每次手工删除 persons.data 文件。 坦白说,我并不相信自己在探索 API 的时候还能每次都记得住。
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); } } }
// See ObjectClass for more info Configuration config = Db4o.configure(); ObjectClass oc = config.objectClass("com.tedneward.model.Person"); oc.minimumActivationDepth(10);
@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);
@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);
随着这个系列深入下去,之前的 Person 类肯定会变得更加复杂。在 关于结构化对象的上一次讨论 结束的时候,我在 Person 中添加了一个 spouse 字段和一些相应的业务逻辑。在那篇文章的最后我提到,舒适的家庭生活会导致一个或更多 “小人儿” 降临到这个家庭。但是,在增加小孩到家庭中之前,我想先确保我的 Person 真正有地方可住。我要给他们一个工作场所,或者还有一个很好的夏日度假屋。一个 Address 类型应该可以解决所有这三个地方。
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);
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 中的 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);
@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");
@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)”。(请参阅 前一篇文章,了解更多关于激活深度的信息。)
对于多样性关系中的对象,其删除工作非常类似于上一篇文章介绍索的结构化对象的删除工作。只需注意级联删除,因为它对这两种对象可能都有影响。当执行级联删除时,将会从引用对象的每个地方彻底删除对象。如果执行一个级联删除来从数据库中删除一个 Person,则那个 Person 的母亲和父亲在其 children 集合中突然有一个 null 引用,而不是有效的对象引用。
在本系列文章中,我使用 Person 类型来演示 db4o 的所有基本原理。您已经学会了如何创建完整的 Person 对象图,以细粒度方式(使用 db4o 本身的查询功能来限制返回的实际对象图)对其进行检索,以及更新和删除全部的对象图(设定一些限制条件)等等。实际上,在面向对象的所有特性中,我们只漏掉了其中一个,那就是继承。
我将演示的这个例子的最终目标是一个用于存储雇员数据的数据管理系统,我一直致力于开发我的 Person 类型。我需要这样一个系统:存储某个公司的员工及其配偶和子女的信息,但是此时他们仅仅是该系统的 Person(或者,可以说 Employees是一个Person,但是 Persons不是一个Employee)。而且,我不希望 Employee 的行为属于 Person API 的一部分。从对象建模程序的角度公平地讲,按照 is-a 模拟类型的能力就是面向对象的本质。
我会用 Person 类型中的一个字段来模拟雇佣 的概念。这是一种关系方法,而且不太适合用于对象设计。幸运的是,与大多数 OODBMS 系统一样,db4o 系统对继承有一个完整的理解。在存储系统的核心使用继承可以轻松地 “重构” 现有系统,可以在设计系统时更多地使用继承,而不会使查询工具变得复杂。您将会看到,这也使查询特定类型的对象变得更加容易。
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 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; }
@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);
public interface Employable { public boolean willYouWorkForUs(); }
角色和对象
一些对象模型将不适合我用接口和继承来模拟 Person 扮演的角色。例如,假设一个 Employee 的配偶决定也来此公司工作,有必要将他们从系统的 Person 中删除,然后重新插入到 Employee 中吗? 随着时间的流逝,角色也可能并且经常变化。我们不要期望更改对象的基类和接口类型,以适应角色的转变。
@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 符合查询条件,因为她实现了这个接口,而其他配偶都没有实现(至少到目前为止)。