面向对象应用程序大量使用继承,并且它们常常使用继承(或者 “是一个”)关系来分类和组织给定系统中的对象。在关系存储模式中使用继承比较困难,因为这种模式没有内在的继承概念,但它是 OODNBMS 中的一个核心功能。在本期的 面向 Java™ 开发人员的 db4o 指南 中,您将会发现,作为一个核心功能,在 db4o 中创建查询时使用继承竟是如此的简单(而且功能强大)。
在本系列文章中,我使用 Person
类型来演示 db4o 的所有基本原理。您已经学会了如何创建完整的 Person
对象图,以细粒度方式(使用 db4o 本身的查询功能来限制返回的实际对象图)对其进行检索,以及更新和删除全部的对象图(设定一些限制条件)等等。实际上,在面向对象的所有特性中,我们只漏掉了其中一个,那就是继承。
|
我将演示的这个例子的最终目标是一个用于存储雇员数据的数据管理系统,我一直致力于开发我的 Person
类型。我需要这样一个系统:存储某个公司的员工及其配偶和子女的信息,但是此时他们仅仅是该系统的 Person
(或者,可以说 Employees
是一个 Person
,但是 Persons
不是一个 Employee
)。而且,我不希望 Employee
的行为属于 Person
API 的一部分。从对象建模程序的角度公平地讲,按照 is-a 模拟类型的能力就是面向对象的本质。
我会用 Person
类型中的一个字段来模拟雇佣 的概念。这是一种关系方法,而且不太适合用于对象设计。幸运的是,与大多数 OODBMS 系统一样,db4o 系统对继承有一个完整的理解。在存储系统的核心使用继承可以轻松地 “重构” 现有系统,可以在设计系统时更多地使用继承,而不会使查询工具变得复杂。您将会看到,这也使查询特定类型的对象变得更加容易。
清单 1 回顾了 Person
类型,该类型在本系列文章中一直作为示例使用:
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<Person> 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<Person> children = new ArrayList<Person>(); private Person mother; private Person father; } |
跟本系列的其他文章一样,我不会在每次更改时都展示完整的 Person
类,只逐步展示每次更改。在这个例子中,我实际上并没有更改 Person
,因为我将要扩展 Person
,而不是修改它。
|
|
需要做的第一件事是使我的雇员管理系统能够区别普通的 Person
(例如雇员的配偶和/或子女)和 Employee
。从纯粹建模的立场来说,这个更改很简单。我只是向 Person
引入了一个新的派生类,这个类和目前涉及到的其他类都在同一个包中。毫无疑问,我将会调用这个类 Employee
,如清单 2 所示:
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()
调用:
@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 所示:
@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
的查询时,将会更加有趣,如下所示:
@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
的输出:
.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 所示:
@Test public void testNativeQuery() { List<Person> spouses = db.query(new Predicate<Person>() { 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 所示:
@Test public void testEmployeeNativeQuery() { List<Employee> spouses = db.query(new Predicate<Person>() { 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
(是的,从设计的角度说这是大材小用了,但是用于教学还是很不错的)。
package com.tedneward.model; public interface Employable { public boolean willYouWorkForUs(); } |
|
要看接口是如何工作的,我需要 Employable
接口的一个实体类继承,并且 — 或许您已经猜测到 — 这意味着创建一个 EmployablePerson
子类型来扩展 Person
和实现 Employable
。我不会再次演示这些代码(没有必要演示,除了将 ** EMPLOYABLE **
添加到 Person
的 toString()
末尾以外, Person
在 EmployablePerson.toString()
方法中)。我也会修改 prepareDatabase()
调用以返回 “Charlotte 是一个 EmployablePerson
,而不只是一个 Person
” 的事实。
现在,我会编写一个遍历数据库的查询,查找愿意为本公司工作的雇员的配偶或亲人,如清单 10 所示。
@Test public void testEmployableQuery() { List<Employable> potentialEmployees = db.query(new Predicate<Employable>() { 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 容易使用 得多 。