在本系列文章中,我使用 Person
类型来演示 db4o 的所有基本原理。您已经学会了如何创建完整的 Person
对象图,以细粒度方式(使用 db4o 本身的查询功能来限制返回的实际对象图)对其进行检索,以及更新和删除全部的对象图(设定一些限制条件)等等。实际上,在面向对象的所有特性中,我们只漏掉了其中一个,那就是继承。
|
我将演示的这个例子的最终目标是一个用于存储雇员数据的数据管理系统,我一直致力于开发我的 Person
类型。我需要这样一个系统:存储某个公司的员工及其配偶和子女的信息,但是此时他们仅仅是该系统的 Person
(或者,可以说 Employees
是一个 Person
,但是 Persons
不是一个 Employee
)。而且,我不希望 Employee
的行为属于 Person
API 的一部分。从对象建模程序的角度公平地讲,按照 is-a 模拟类型的能力就是面向对象的本质。
我会用 Person
类型中的一个字段来模拟雇佣 的概念。这是一种关系方法,而且不太适合用于对象设计。幸运的是,与大多数 OODBMS 系统一样,db4o 系统对继承有一个完整的理解。在存储系统的核心使用继承可以轻松地 “重构” 现有系统,可以在设计系统时更多地使用继承,而不会使查询工具变得复杂。您将会看到,这也使查询特定类型的对象变得更加容易。
清单 1 回顾了 Person
类型,该类型在本系列文章中一直作为示例使用:
import java.util.List;
import java.util.ArrayList;
import java.util.Iterator;
public class Person
... {
publicPerson()
...{}
publicPerson(StringfirstName,StringlastName,Gendergender,intage,Moodmood)
...{
this.firstName=firstName;
this.lastName=lastName;
this.gender=gender;
this.age=age;
this.mood=mood;
}
publicStringgetFirstName()...{returnfirstName;}
publicvoidsetFirstName(Stringvalue)...{firstName=value;}
publicStringgetLastName()...{returnlastName;}
publicvoidsetLastName(Stringvalue)...{lastName=value;}
publicGendergetGender()...{returngender;}
publicintgetAge()...{returnage;}
publicvoidsetAge(intvalue)...{age=value;}
publicMoodgetMood()...{returnmood;}
publicvoidsetMood(Moodvalue)...{mood=value;}
publicPersongetSpouse()...{returnspouse;}
publicvoidsetSpouse(Personvalue)...{
//Afewbusinessrules
if(spouse!=null)
thrownewIllegalArgumentException("Alreadymarried!");
if(value.getSpouse()!=null&&value.getSpouse()!=this)
thrownewIllegalArgumentException("Alreadymarried!");
spouse=value;
//Highlysexistbusinessrule
if(gender==Gender.FEMALE)
this.setLastName(value.getLastName());
//Makemarriagereflexive,ifit'snotalreadysetthatway
if(value.getSpouse()!=this)
value.setSpouse(this);
}
publicAddressgetHomeAddress()...{returnaddresses[0];}
publicvoidsetHomeAddress(Addressvalue)...{addresses[0]=value;}
publicAddressgetWorkAddress()...{returnaddresses[1];}
publicvoidsetWorkAddress(Addressvalue)...{addresses[1]=value;}
publicAddressgetVacationAddress()...{returnaddresses[2];}
publicvoidsetVacationAddress(Addressvalue)...{addresses[2]=value;}
publicIterator<Person>getChildren()...{returnchildren.iterator();}
publicPersonhaveBaby(Stringname,Gendergender)...{
//Businessrule
if(this.gender.equals(Gender.MALE))
thrownewUnsupportedOperationException("Biologicalimpossibility!");
//Anotherhighlyobjectionablebusinessrule
if(getSpouse()==null)
thrownewUnsupportedOperationException("Ethicalimpossibility!");
//Welcometotheworld,littleone!
Personchild=newPerson(name,this.lastName,gender,0,Mood.CRANKY);
//Well,wouldn'tYOUbecrankyifyou'djustbeenpushedoutof
//anicewarmplace?!?
//Theseareyourparents...
child.father=this.getSpouse();
child.mother=this;
//...andyou'retheirnewbaby.
//(Everybodysay"Awwww....")
children.add(child);
this.getSpouse().children.add(child);
returnchild;
}
publicPersongetFather()...{returnthis.father;}
publicPersongetMother()...{returnthis.mother;}
publicStringtoString()
...{
return
"[Person:"+
"firstName="+firstName+""+
"lastName="+lastName+""+
"gender="+gender+""+
"age="+age+""+
"mood="+mood+""+
(spouse!=null?"spouse="+spouse.getFirstName()+"":"")+
"]";
}
publicbooleanequals(Objectrhs)
...{
if(rhs==this)
returntrue;
if(!(rhsinstanceofPerson))
returnfalse;
Personother=(Person)rhs;
return(this.firstName.equals(other.firstName)&&
this.lastName.equals(other.lastName)&&
this.gender.equals(other.gender)&&
this.age==other.age);
}
privateStringfirstName;
privateStringlastName;
privateGendergender;
privateintage;
privateMoodmood;
privatePersonspouse;
privateAddress[]addresses=newAddress[3];
privateList<Person>children=newArrayList<Person>();
privatePersonmother;
privatePersonfather;
}
跟本系列的其他文章一样,我不会在每次更改时都展示完整的 Person
类,只逐步展示每次更改。在这个例子中,我实际上并没有更改 Person
,因为我将要扩展 Person
,而不是修改它。
需要做的第一件事是使我的雇员管理系统能够区别普通的 Person
(例如雇员的配偶和/或子女)和 Employee
。从纯粹建模的立场来说,这个更改很简单。我只是向 Person
引入了一个新的派生类,这个类和目前涉及到的其他类都在同一个包中。毫无疑问,我将会调用这个类 Employee
,如清单 2 所示:
public class Employee extends Person
... {
publicEmployee()
...{}
publicEmployee(StringfirstName,StringlastName,Stringtitle,
Gendergender,intage,Moodmood)
...{
super(firstName,lastName,gender,age,mood);
this.title=title;
}
publicStringgetTitle()...{returntitle;}
publicvoidsetTitle(Stringvalue)...{title=value;}
publicStringtoString()
...{
return"[Employee:"+getFirstName()+""+getLastName()+""+
"("+getTitle()+")]";
}
privateStringtitle;
}
Employee
中的其他方法意义不大。在本讨论中需要记住的是
Employee
是
Person
的一个子类(如果更加关心系统的建模过程,可以设想
Employee
中的其他方法,例如
promote()
、
demote()
、
getSalary()
、
setSalary()
和
workLikeADog()
)。
对新模型的探察测试简单明了。我创建一个叫做 InheritanceTest
的 JUnit 类,目前为止,它是第一个较为复杂的对象集,充当 OODBMS 最初的工作内容。为了使输出(将会在清单 6 中见到)更加清晰,我在清单 3 中展示了带有 @Before
注释的 prepareDatabase()
调用:
... {
db=Db4o.openFile("persons.data");
//TheNewards
Employeeted=newEmployee("Ted","Neward","PresidentandCEO",
Gender.MALE,36,Mood.HAPPY);
Personcharlotte=newPerson("Charlotte","Neward",
Gender.FEMALE,35,Mood.HAPPY);
ted.setSpouse(charlotte);
Personmichael=charlotte.haveBaby("Michael",Gender.MALE);
michael.setAge(14);
Personmatthew=charlotte.haveBaby("Matthew",Gender.MALE);
matthew.setAge(8);
AddresstedsHomeOffice=
newAddress("12RedmondRd","Redmond","WA","98053");
ted.setHomeAddress(tedsHomeOffice);
ted.setWorkAddress(tedsHomeOffice);
ted.setVacationAddress(
newAddress("10WannahokalugiWay","Oahu","HA","11223"));
db.set(ted);
//TheTates
Employeebruce=newEmployee("Bruce","Tate","ChiefTechnicalOfficer",
Gender.MALE,29,Mood.HAPPY);
Personmaggie=newPerson("Maggie","Tate",
Gender.FEMALE,29,Mood.HAPPY);
bruce.setSpouse(maggie);
Personkayla=maggie.haveBaby("Kayla",Gender.FEMALE);
Personjulia=maggie.haveBaby("Julia",Gender.FEMALE);
bruce.setHomeAddress(
newAddress("5MapleDrive","Austin",
"TX","12345"));
bruce.setWorkAddress(
newAddress("5701DowntownSt","Austin",
"TX","12345"));
//TedandBrucebothusethesametimeshare,apparently
bruce.setVacationAddress(
newAddress("10WanahokalugiWay","Oahu",
"HA","11223"));
db.set(bruce);
//TheFords
Employeeneal=newEmployee("Neal","Ford","MemeWrangler",
Gender.MALE,29,Mood.HAPPY);
Personcandi=newPerson("Candi","Ford",
Gender.FEMALE,29,Mood.HAPPY);
neal.setSpouse(candi);
neal.setHomeAddress(
newAddress("22GritsngravyWay","Atlanta","GA","32145"));
//Nealistherovingarchitect
neal.setWorkAddress(null);
db.set(neal);
//TheSlettens
Employeebrians=newEmployee("Brian","Sletten","BosatsuMaster",
Gender.MALE,29,Mood.HAPPY);
Personkristen=newPerson("Kristen","Sletten",
Gender.FEMALE,29,Mood.HAPPY);
brians.setSpouse(kristen);
brians.setHomeAddress(
newAddress("57ClassifiedDrive","Fairfax","VA","55555"));
brians.setWorkAddress(
newAddress("1CIAWasNeverHereStreet","Fairfax","VA","55555"));
db.set(brians);
//TheGalbraiths
Employeeben=newEmployee("Ben","Galbraith","ChiefUIDirector",
Gender.MALE,29,Mood.HAPPY);
Personjessica=newPerson("Jessica","Galbraith",
Gender.FEMALE,29,Mood.HAPPY);
ben.setSpouse(jessica);
ben.setHomeAddress(
newAddress(
"5500North2700EastRd","SaltLakeCity",
"UT","12121"));
ben.setWorkAddress(
newAddress(
"5600North2700EastRd","SaltLakeCity",
"UT","12121"));
ben.setVacationAddress(
newAddress(
"2700East5500NorthRd","SaltLakeCity",
"UT","12121"));
//Benreallyneedstogetoutmore
db.set(ben);
db.commit();
}
跟本系列早先的探察测试示例一样,在每次测试完成后,我使用带 @After
注释的 deleteDatabase()
方法来删除数据库,以使各部分能够很好地分隔开。
在实际运行这个方法之前,我将会检查在系统中使用 Employee
会有哪些效果(如果有的话)。希望从数据库获取所有 Employee
信息,这很正常 — 或许当公司破产时他们将会全部被解雇(是的,我知道,这样想很残酷,但我只是对 2001 年的 dot-bomb 事故还有点心有余悸)。最初的测试看起来很简单,正如清单 4 所示:
... {
ObjectSetemployees=db.get(Employee.class);
while(employees.hasNext())
System.out.println("Found"+employees.next());
}
当进行测试时,将会产生一个有趣的结果:数据库(我自己、Ben、Neal、Brian 和 Bruce)中只返回了 Employee
。OODBMS 识别出查询受到子类型 Employee
的显式约束,并且只选择了符合返回条件的对象。因为其他对象(配偶或者孩子)不属于 Employee
类型,他们不符合条件,所以没有被返回。
当运行一个返回所有 Person
的查询时,将会更加有趣,如下所示:
... {
ObjectSetpersons=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[Person:firstName = CharlottelastName = Newardgender = FEMALEage = 35
mood = HAPPYspouse = Ted]
Found[Person:firstName = MichaellastName = Newardgender = MALEage = 14 mood
= CRANKY]
Found[Person:firstName = MatthewlastName = Newardgender = MALEage = 8 mood
= CRANKY]
Found[Employee:BruceTate(ChiefTechnicalOfficer)]
Found[Person:firstName = MaggielastName = Tategender = FEMALEage = 29 mood
= HAPPYspouse = Bruce]
Found[Person:firstName = KaylalastName = Tategender = FEMALEage = 0 mood =
CRANKY]
Found[Person:firstName = JulialastName = Tategender = FEMALEage = 0 mood =
CRANKY]
Found[Employee:NealFord(MemeWrangler)]
Found[Person:firstName = CandilastName = Fordgender = FEMALEage = 29 mood =
HAPPYspouse = Neal]
Found[Employee:BrianSletten(BosatsuMaster)]
Found[Person:firstName = KristenlastName = Slettengender = FEMALEage = 29 m
ood = HAPPYspouse = Brian]
Found[Employee:BenGalbraith(ChiefUIDirector)]
Found[Person:firstName = JessicalastName = Galbraithgender = FEMALEage = 29
mood = HAPPYspouse = Ben]
您可能会感到奇怪,不管如何查询,返回的对象仍然是适当的子类型对象。例如,跟预期的一样,在上面的查询中当 toString()
被每个返回的 Person
对象调用时,Person.toString()
也正被每个 Person
调用。然而,因为 Employee
有一个重写的 toString()
方法,因此关于动态绑定的常用规则就不适用了。存储在 Employee
中的 Person
的各部分不会被 “切掉”,而当定期 SQL 查询未能成功地将派生子类表加入到 table-per-class 模型中时,这种被 “切掉” 的现象就会发生。
当然,当继承条件扩展到原生查询中时,其功能就跟我所做过的简单对象查询一样强大。进行调用时,查询语法将会更加复杂,但是基本上遵循我以前所使用的语法,如清单 7 所示:
... {
List<Person>spouses=
db.query(newPredicate<Person>()...{
publicbooleanmatch(Personcandidate)...{
return(candidate.getSpouse()instanceofEmployee);
}
});
for(Personspouse:spouses)
System.out.println(spouse);
}
下面的查询与我以前做过的查询在思想上类似,考虑系统中所有的 Person
,但是设置一个约束条件,只查找配偶也是一个 Employee
的 Person
— 调用 getSpouse()
,将返回值传递给 Java instanceof
运算符,这样就完成了查询(记住 match()
调用只返回 true 或者 false,表示候选对象是否应该返回)。
请注意如何通过更改在 query()
调用中传递的 Predicate
来更改隐式选择的类型条件,如清单 8 所示:
... {
List<Employee>spouses=
db.query(newPredicate<Person>()...{
publicbooleanmatch(Personcandidate)...{
return(candidate.getSpouse()instanceofEmployee);
}
});
for(Personspouse:spouses)
System.out.println(spouse);
}
当执行此查询时,不会产生什么结果,因为现在此查询只查找配偶也在公司工作的 Employee
。目前,数据库中的雇员都不满足这个条件。如果公司雇佣 Charlotte,那么会返回两个 Employee
:Ted 和 Charlotte(但是人家说办公司恋情永远不会发生)。
在很大程度上,就是这样。继承不会对更新、删除和激活深度产生任何影响,只会影响到对象的查询方面。但是回想起 Java 平台提供的两种形式的继承:实现继承和接口继承。前者通过各种 extends 子句来实现,而后者通过 implements 来实现。如果 db4o 支持 extends,那么它也一定支持 implements,您将会看到,这有利于实现强大的查询功能。
就像任何 Java (或 C#)编程人员使用了一段时间这种语言后认识到的,接口对于建模非常有用。尽管不会经常看到,接口具有强大的 “隔离” 交叉在传统实现继承行中的对象的能力;通过使用接口,我可以声明某些类型为 Comparable
或者 Serializable
,或者在本例中,Employable
(是的,从设计的角度说这是大材小用了,但是用于教学还是很不错的)。
public interface Employable
... {
publicbooleanwillYouWorkForUs();
}
要看接口是如何工作的,我需要 Employable
接口的一个实体类继承,并且 — 或许您已经猜测到 — 这意味着创建一个 EmployablePerson
子类型来扩展 Person
和实现 Employable
。我不会再次演示这些代码(没有必要演示,除了将 ** EMPLOYABLE **
添加到 Person
的 toString()
末尾以外, Person
在 EmployablePerson.toString()
方法中)。我也会修改 prepareDatabase()
调用以返回 “Charlotte 是一个 EmployablePerson
,而不只是一个 Person
” 的事实。
现在,我会编写一个遍历数据库的查询,查找愿意为本公司工作的雇员的配偶或亲人,如清单 10 所示。
... {
List<Employable>potentialEmployees=
db.query(newPredicate<Employable>()...{
publicbooleanmatch(Employablecandidate)...{
return(candidate.willYouWorkForUs());
}
});
for(Employablee:potentialEmployees)
System.out.println("Eureka!"+e+"hassaidthey'llworkforus!");
}
毫无疑问,Charlotte 被返回了,说明她可能为本公司工作。更好的是,这意味着我引入的任何接口都变成了一种限制查询的新方式,不需要人工添加包含此信息的字段;只有 Charlotte 符合查询条件,因为她实现了这个接口,而其他配偶都没有实现(至少到目前为止)。
如果说对象和继承就像巧克力和花生酱的话,那对象和多态就好比手和手套。这两个元素就像经理和他/她的高薪一样般配。检索数据时,任何存储对象的系统都不得不将继承的概念引入它的存储媒介和过滤器中。幸运的是,面向对象的 DBMS 使得这很容易实现,而且不必引入新的 query-predicate 术语。从长远看来,引入继承会使 OODBMS 容易使用 得多。
学习
- 您可以参阅本文在 developerWorks 全球站点上的 英文原文 。
- “ 面向 Java 开发人员的 db4o 指南: 超越简单对象”(Ted Neward,developerWorks,2007 年 7 月):开始在 db4o 中使用结构化对象和探察测试。
- “Role object”(Dirk Bäumer,Dirk Riehle,Wolf Siberski,and Martina Wulf;Pattern Languages of Program Design 4,Addison-Wesley,2000):解释了角色对象,在其中将角色表示成独立的对象,以将不同的上下文分开,从而简化系统配置。
- 追求代码质量 (Andrew Glover, developerWorks 系列):学习更多有关开发人员测试技术的内容,比如探察测试。
- 面向 Java 开发人员的 db4o 指南 (Ted Neward, developerWorks 系列): 介绍 db4o,一个使用目前的面向对象语言、系统及理念的开源数据库。
- db4o 主页:学习更多关于 db4o 的知识。
- ODBMS.org:其中有许多优秀的关于对象数据库技术的免费资料
- developerWorks Java 技术专区:其中有数百篇关于 Java 编程各方面的文章。
- IBM® Information Management 新手入门:仍然没有使用 OODBMS 吗?了解更多关于 IBM 强大的关系数据库管理系统(RDBMS)服务器家族的信息。