在本系列的前一篇文章中,我开始谈到了 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
...
{
publicAddress()
...{
}
publicAddress(Stringstreet,Stringcity,Stringstate,Stringzip)
...{
this.street=street;this.city=city;
this.state=state;this.zip=zip;
}
publicStringtoString()
...{
return"[Address:"+
"street="+street+""+
"city="+city+""+
"state="+state+""+
"zip="+zip+"]";
}
publicinthashCode()
...{
returnstreet.hashCode()&city.hashCode()&
state.hashCode()&zip.hashCode();
}
publicbooleanequals(Objectobj)
...{
if(obj==this)
returnthis;
if(objinstanceofAddress)
...{
Addressrhs=(Address)obj;
return(this.street.equals(rhs.street)&&
this.city.equals(rhs.city)&&
this.state.equals(rhs.state)&&
this.zip.equals(rhs.zip));
}
else
returnfalse;
}

publicStringgetStreet()...{returnthis.street;}
publicvoidsetStreet(Stringvalue)...{this.street=value;}

publicStringgetCity()...{returnthis.city;}
publicvoidsetCity(Stringvalue)...{this.city=value;}

publicStringgetState()...{returnthis.state;}
publicvoidsetState(Stringvalue)...{this.state=value;}

publicStringgetZip()...{returnthis.zip;}
publicvoidsetZip(Stringvalue)...{this.zip=value;}
privateStringstreet;
privateStringcity;
privateStringstate;
privateStringzip;
}
可以看到,Address 只是一个简单的数据对象。将它添加到 Person 类中意味着 Person 将有一个名为 addresses 的 Address 数组作为字段。第一个地址是家庭住址,第二个是工作地址,第三个(如果不为 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
...
{
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;
}
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;
}
即使包括所有这些代码,清单 2 提供的家庭关系模型还是过于简单。在这个层次结构中的某些地方,必须处理那些 null 值。但是,在 db4o 中,那个问题更应该在对象建模中解决,而不是在对象操作中解决。所以现在我可以放心地忽略它。
对于清单 2 中的 Person 类,需要重点注意的是,如果以关系的方式,使用父与子之间分层的、循环的引用来建模,那肯定会比较笨拙。通过一个实例化的对象模型可以更清楚地看到我所谈到的复杂性,所以我将编写一个探察测试来实例化 Person 类。注意,清单 3 中省略了 JUnit 支架(scaffolding);我假设您可以从其他地方,包括本系列之前的文章学习 JUnit 4 API。通过阅读本文的源代码,还可以学到更多东西。
@Test
public
void
testTheModel()
...
{
Personbruce=newPerson("Bruce","Tate",
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);
assertTrue(julia.getFather()==bruce);
assertTrue(kayla.getFather()==bruce);
assertTrue(julia.getMother()==maggie);
assertTrue(kayla.getMother()==maggie);
intn=0;
for(Iterator<Person>kids=bruce.getChildren();kids.hasNext();)
...{
Personchild=kids.next();
if(n==0)assertTrue(child==kayla);
if(n==1)assertTrue(child==julia);
n++;
}
}
目前一切尚好。所有方面都能通过测试,包括小孩 ArrayList 的使用中的长嗣身份。但是,当我增加 @Before 和 @After 条件,以便用我的测试数据填充 db4o 数据库时,事情开始变得更有趣。
@Before
public
void
prepareDatabase()
...
{
db=Db4o.openFile("persons.data");
Personbruce=newPerson("Bruce","Tate",
Gender.MALE,29,Mood.HAPPY);
Personmaggie=newPerson("Maggie","Tate",
Gender.FEMALE,29,Mood.HAPPY);
bruce.setSpouse(maggie);
bruce.setHomeAddress(
newAddress("5MapleDrive","Austin",
"TX","12345"));
bruce.setWorkAddress(
newAddress("5MapleDrive","Austin",
"TX","12345"));
bruce.setVacationAddress(
newAddress("10WanahokalugiWay","Oahu",
"HA","11223"));
Personkayla=maggie.haveBaby("Kayla",Gender.FEMALE);
kayla.setAge(8);
Personjulia=maggie.haveBaby("Julia",Gender.FEMALE);
julia.setAge(6);
db.set(bruce);
db.commit();
}
注意,存储整个家庭所做的工作仍然不比存储单个 Person 对象所做的工作多。您可能还记得,在上一篇文章中,由于存储的对象具有递归的性质,当把 bruce 引用传递给 db.set() 调用时,从 bruce 可达的所有对象都被存储。不过眼见为实,让我们看看当运行我那个简单的探察测试时,实际上会出现什么情况。首先,我将测试当调用随 Person 存储的各种 Address 时,是否可以找到它们。然后,我将测试是否孩子们也被存储。
@Test
public
void
testTheStorageOfAddresses()
...
{
List<Person>maleTates=
db.query(newPredicate<Person>()...{
publicbooleanmatch(Personcandidate)...{
returncandidate.getLastName().equals("Tate")&&
candidate.getGender().equals(Gender.MALE);
}
});
Personbruce=maleTates.get(0);
AddresshomeAndWork=
newAddress("5MapleDrive","Austin",
"TX","12345");
Addressvacation=
newAddress("10WanahokalugiWay","Oahu",
"HA","11223");
assertTrue(bruce.getHomeAddress().equals(homeAndWork));
assertTrue(bruce.getWorkAddress().equals(homeAndWork));
assertTrue(bruce.getVacationAddress().equals(vacation));
}

@Test
public
void
testTheStorageOfChildren()
...
{
List<Person>maleTates=
db.query(newPredicate<Person>()...{
publicbooleanmatch(Personcandidate)...{
returncandidate.getLastName().equals("Tate")&&
candidate.getGender().equals(Gender.MALE);
}
});
Personbruce=maleTates.get(0);
intn=0;
for(Iterator<Person>children=bruce.getChildren();
children.hasNext();
)
...{
Personchild=children.next();
System.out.println(child);
if(n==0)assertTrue(child.getFirstName().equals("Kayla"));
if(n==1)assertTrue(child.getFirstName().equals("Julia"));
n++;
}
}
|
您可能会感到奇怪,清单 5 中显示的基于 Collection 的类型(ArrayList)没有被存储为 Person 类型的 “dependents”,而是被存储为一个成熟的对象。这还说得过去,但是当对对象数据库中的 ArrayList 运行一个查询时,它可能,有时候也确实会导致返回奇怪的结果。由于目前数据库中只有一个 ArrayList,所以还不值得运行一个探察测试,看看当对它运行一个查询时会出现什么情况。我把这作为留给您的练习。
自然地,存储在一个集合中的 Person 也被当作数据库中的一级实体,所以在查询符合某个特定标准(例如所有女性 Person)的所有 Person 时,也会返回 ArrayList 实例中引用到的那些 Person,如清单 6 所示。
@Test
public
void
findTheGirls()
...
{
List<Person>girls=
db.query(newPredicate<Person>()...{
publicbooleanmatch(Personcandidate)...{
returncandidate.getGender().equals(Gender.FEMALE);
}
});
booleanmaggieFound=false;
booleankaylaFound=false;
booleanjuliaFound=false;
for(Personp: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 所示。
@Test
public
void
findJuliaAndHerMommy()
...
{
Personmaggie=(Person)db.get(
newPerson("Maggie","Tate",Gender.FEMALE,0,null)).next();
Personjulia=(Person)db.get(
newPerson("Julia","Tate",Gender.FEMALE,0,null)).next();
assertTrue(julia.getMother()==maggie);
}
当然,您正是希望对象数据库具有这样的行为。还应注意,如果返回女儿对象的查询的激活深度被设置得足够低,那么对 getMother() 的调用将返回 null,而不是实际的对象。这是因为 Person 中的 mother 字段是相对于被检索的原本对象的另一个 “跳跃(hop)”。(请参阅 前一篇文章,了解更多关于激活深度的信息。)
至此,您已经看到了 db4o 如何存储和取出多个对象,但是对象数据库如何处理更新和删除呢?就像结构化对象一样,多对象更新或删除期间的很多工作都与管理更新深度有关,或者与级联删除有关。现在您可能已经注意到,结构化对象与集合之间有很多相似之处,所以其中某一种实体的特性也适用于另一种实体。如果将 ArrayList 看作 “另一种结构化对象”,而不是一个集合,就很好理解了。
所以,根据到目前为止您学到的东西,我应该可以更新数据库中的某一个女孩。而且,为了更新这个对象,只需将她父母中的一个重新存储到数据库中,如清单 8 所示。
@Test
public
void
kaylaHasABirthday()
...
{
Personmaggie=(Person)db.get(
newPerson("Maggie","Tate",Gender.FEMALE,0,null)).next();
Personkayla=(Person)db.get(
newPerson("Kayla","Tate",Gender.FEMALE,0,null)).next();
kayla.setAge(kayla.getAge()+1);
intkaylasNewAge=kayla.getAge();
db.set(maggie);
db.close();
db=Db4o.openFile("persons.data");
kayla=(Person)db.get(
newPerson("Kayla","Tate",Gender.FEMALE,0,null)).next();
assert(kayla.getAge()==kaylasNewAge);
}
还记得吗,在 前一篇文章 中,我必须显式地关闭到数据库的连接,以避免被误诊为重取已经位于工作内存中的对象。
对于多样性关系中的对象,其删除工作非常类似于上一篇文章介绍索的结构化对象的删除工作。只需注意级联删除,因为它对这两种对象可能都有影响。当执行级联删除时,将会从引用对象的每个地方彻底删除对象。如果执行一个级联删除来从数据库中删除一个 Person,则那个 Person 的母亲和父亲在其 children 集合中突然有一个 null 引用,而不是有效的对象引用。
在很多方面,将数组和集合存储到对象数据库中并不总与存储常规的结构化对象不同,只是要注意数组不能被直接查询,而集合则可以。不管出于何种目的,这都意味着可以在建模时使用集合和数组,而不必等到持久引擎需要使用集合或数组时才使用它们。
学习
- 您可以参阅本文在 developerWorks 全球站点上的 英文原文 。
- “面向 Java 开发人员的 db4o 指南: 超越简单对象”(Ted Neward,developerWorks,2007 年 7 月):开始使用结构化对象,并学习更多关于 db4o 如何处理激活深度、级联更新与删除和参照完整性的知识。
- “面向 Java 开发人员的 db4o 指南: 查询,更新和一致性”(Ted Neward,developerWorks,2007 年 3 月):探索 db4o 的各种发现和检索数据的机制,包括 OID。
- “Have a Little Respect for SQL Databases”(Jack D. Herrington,DevX.com,2003 年 10 月):构建关系数据库适用的案例,包括参照完整性。
- 追求代码质量(Andrew Glover,developerWorks 系列):学习更多关于诸如探察测试之类的开发人员测试技巧的知识。
- 面向 Java 开发人员的 db4o 指南 介绍了 db4o,它是一种充分利用现在的面向对象语言、系统和理念的开放源代码数据库。(Ted Neward,developerWorks 系列)。
- “开源面向对象数据库 db4o 之旅”:这个系列的文章对 db4o 的特性、查询与更新方式进行了介绍。
- db4o:学习更多关于 db4o 的知识。
- ODBMS.org:其中有很多很好的关于对象数据库技术的免费资料。
- developerWorks Java 技术专区:这里有数百篇关于 Java 编程方方面面的文章。
- IBM 信息管理新手入门:仍然没有使用 OODBMS 吗?了解更多关于 IBM 强大的关系数据库管理系统(RDBMS)服务器家族的信息。
本文探讨了db4o数据库如何处理复杂的对象关系,包括多样性关系中的对象集合存储和操作。通过示例展示了如何定义和存储带有集合属性的对象,并演示了查询、更新和删除这些对象的过程。

被折叠的 条评论
为什么被折叠?



