集合和数组为 面向 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
将有一个名为 addresses 的 Address
数组作为字段。第一个地址是家庭住址,第二个是工作地址,第三个(如果不为 null 的话)是度假屋地址。当然,这些都被设置为 protected,以便将来通过方法来封装。
完成这些设置后,现在可以增强 Person
类,使之支持小孩,所以我将为 Person
定义一个新字段:一个 Person
ArrayList
,它同样也有一些相关的方法,以便进行适当的封装。
接下来,由于大多数小孩都有父母,我还将添加两个字段来表示母亲和父亲,并增加适当的 accessor/mutator 方法。我将为 Person
类增加一个新的方法,使之可以创建一个新的 Person
,这个方法有一个贴切的名称,即 haveBaby 。此外还增加一些业务规则,以支持生小孩的生物学需求,并将这个新的小 Person
添加到为母亲和父亲字段创建的 children ArrayList
中。做完这些之后,再将这个婴儿返回给调用者。
清单 2 显示,新定义的 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 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; } |
即使包括所有这些代码,清单 2 提供的家庭关系模型还是过于简单。在这个层次结构中的某些地方,必须处理那些 null 值。但是,在 db4o 中,那个问题更应该在对象建模中解决,而不是在对象操作中解决。所以现在我可以放心地忽略它。
![]() ![]() |
![]()
|
对于清单 2 中的 Person
类,需要重点注意的是,如果以关系的方式,使用父与子之间分层的、循环的引用来建模,那肯定会比较笨拙。通过一个实例化的对象模型可以更清楚地看到我所谈到的复杂性,所以我将编写一个探察测试来实例化 Person
类。 注意,清单 3 中省略了 JUnit 支架(scaffolding);我假设您可以从其他地方,包括本系列之前的文章学习 JUnit 4 API。通过阅读本文的源代码,还可以学到更多东西。
@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<Person> 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 数据库时,事情开始变得更有趣。
@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
时,是否可以找到它们。然后,我将测试是否孩子们也被存储。
@Test public void testTheStorageOfAddresses() { List<Person> maleTates = db.query(new Predicate<Person>() { 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<Person> maleTates = db.query(new Predicate<Person>() { 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<Person> 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++; } } |
![]() |
|
您可能会感到奇怪,清单 5 中显示的基于 Collection
的类型(ArrayList
)没有被存储为 Person
类型的 “dependents”,而是被存储为一个成熟的对象。这还说得过去,但是当对对象数据库中的 ArrayList
运行一个查询时,它可能,有时候也确实会导致返回奇怪的结果。由于目前数据库中只有一个 ArrayList
,所以还不值得运行一个探察测试,看看当对它运行一个查询时会出现什么情况。我把这作为留给您的练习。
自然地,存储在一个集合中的 Person
也被当作数据库中的一级实体,所以在查询符合某个特定标准(例如所有女性 Person
)的所有 Person
时,也会返回 ArrayList
实例中引用到的那些 Person
,如清单 6 所示。
@Test public void findTheGirls() { List<Person> girls = db.query(new Predicate<Person>() { 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 所示。
@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 所示。
@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 引用,而不是有效的对象引用。
![]() ![]() |
![]() |
在很多方面,将数组和集合存储到对象数据库中并不总与存储常规的结构化对象不同,只是要注意数组不能被直接查询,而集合则可以。不管出于何种目的,这都意味着可以在建模时使用集合和数组,而不必等到持久引擎需要使用集合或数组时才使用它们。