In the previous article in this series, I began talking about how db4o handles structured objects, or objects that contain nonprimitive fields. As I showed, adding complexity to object relationships has some serious ramifications for the db4o persistence model. I talked about the importance of addressing things like activation depth, cascading updates and deletes, and referential integrity, which db4o does not support during deletion. I also introduced a developer testing tactic called exploration testing, which incidentally led to a first exercise in using the db4o API.
In this article, I continue my introduction to the storage and manipulation
of structured objects in db4o, starting with a look at multiplicity
relationships, where objects hold collections of objects as fields. (A
collection in this case refers to both Collection
classes like ArrayList and the standard language arrays.)
You will see that db4o handles multiplicity without much difficulty. You'll also
become more familiar with db4o's handling of cascading updates and activation depth.
Handling multiplicity relationships
The old Person class is definitely getting more
complicated as this series goes on. When we left off in my previous
discussion about structured objects, I had added a spouse field to
Person, as well as some business rules to go with
it. As I noted at the end of that article, the comforts of domestic life can
result in the arrival of one or more "little someones" to share the house
with. Before I begin adding children into the domestic mix, however, I want to
make sure my Persons actually have someplace to
live. While I'm at it, I'd like to give them a clear destination for work, and
at least the option of having a nice summer home for vacations. An Address type should do the trick for all three.
Listing 1. Adding an Address type to the Person class
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, as you can see, is nothing more than a
simple data object. Adding it to the Person class
means that Person will have an array of Persons as a field, called addresses. The first
address will always be the home address, the second the work address, and the
third (if not null) the vacation home. All this will be protected for future
encapsulation via methods, of course.
With that settled, I'm ready to enhance the Person class to support children, so I'll give Person a new field: an ArrayList of Person, which
(again) will have methods around it for proper encapsulation.
Next, because most children have parents, I'll add two more fields for
mother and father, along with the appropriate accessor/mutator methods. I'll
add a new method to the Person class, enabling it
to create a new Person, fittingly named
haveBaby. I'll also add some business rules that support the
biological requirements of having a baby, and add this new little Person to the children
ArrayList created for the mother and father fields. All
that done, I'll return the baby to the caller.
Listing 2 shows the new Person class defined to
handle this multiplicity of relationships.
Listing 2. Domestic life defined as a multiplicity relationship
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;
}
|
Granted, even with all that code, Listing 2 presents an oversimplified model of domestic relations. At some point in the hierarchy, I'll have to deal with all those null values. That particular problem is more an exercise in object modeling than in object manipulation in db4o, however. So for now, I can safely ignore it.
Seeding and testing the object model
The important thing to note about the Person
class in Listing 2 is that it would be decidedly awkward to model in a
relational manner, using a hierarchical and circular set of references between
parents and children. You'll be able to see the complexity I'm talking about
even better by looking at an instantiated object model, so I'll write up an
exploration test to instantiate the Person class.
Note that I've left the JUnit scaffolding out of Listing 3; I'm presuming
that you can learn about the JUnit 4 API from other sources, including the
previous article in this series. You'll also learn more by reading the source
code for this article.
Listing 3. The happy family test
@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++;
}
}
|
So far, so good. Everything checks out, including the primogeniture
inherent in the use of an ArrayList for the
children. Things start to get more interesting, though, when I add @Before and @After conditions
to seed the db4o database with my test data.
Listing 4. Sending the children to the database
@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();
}
|
Notice that it's still no more work to store the entire family than it
would be to store a single Person object. You may
recall from previous articles that because the objects stored are recursive by
nature, objects reachable from bruce are stored
when the bruce reference is passed in to the db.set() call. Talk is cheap, though, so let's see
what actually happens when I run my simple exploration test. First, I'll test whether the
various Addresses stored with my Person are found when called. Second, I'll test to see
that the children are also present and accounted for.
Listing 5. Searching for home and family
@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++;
}
}
|
It might strike you as strange that the Collection-based types shown in Listing 5 (the ArrayList) aren't stored as "dependents" of the Person type, but as full-fledged objects in their own
right. This makes sense once it's pointed out, but it can, and sometimes does,
lead to strange results when running a query against the ArrayList type in an object database. Because there is
only one ArrayList in the database thus far,
it wouldn't be worthwhile to run an exploration test to see what happens when I run
queries against it. I'll leave that exercise to you.
Naturally, Persons stored inside a collection
are also treated as first-class entities in the database, so finding all the
Persons who meet a particular criteria — such as
finding all Persons who are female) — will also find
those Persons referenced from within the ArrayList instances, as demonstrated in Listing 6.
Listing 6. Where's Julia?
@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);
}
|
Note, too, that the object database will keep the references "correct" —
at least so far as it knows them. For example, retrieving a Person (the mother, perhaps) and another Person (say, the daughter) in separate queries will still
be recognized as having a bidirectional relationship between them, as shown in
Listing 7.
Listing 7. Keeping relationships real
@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);
}
|
Of course, this is exactly the way you want an object database to behave.
Also note that if the activation depth for the query that returns the
daughter object, above, were set low enough, the call to getMother() would return null, instead of the actual
object. This is because the mother field in Person is another "hop" from the original object
retrieved. (See the previous article for more about activation depth.)
So far, you've seen how db4o handles storing and fetching multiples of
objects, but what does the object database do with updates and deletes? Just
as with structured objects, much of what goes on
during a multiple-object update or delete has to do with managing the depth of
the update or dealing with cascading deletes. You may have noticed by now
that there are many parallels between structured objects and collections, so
that much of what there is to say about one type of entity is applicable to
the other. This makes sense if you view ArrayList
as "another structured object" instead of as a collection.
So, based on what you've learned so far, it seems that I should be able to update one of the girls in the database, and update the object by simply re-storing one or the other of her parents into the database, as shown in Listing 8.
Listing 8. Happy birthday, Kayla!
@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);
}
|
Recall from the previous article that I must explicitly close the connection to the database to avoid the false positive of re-fetching an object already in working memory.
Deletion works pretty much the same way for objects in multiplicity
relationships as it did for the structured objects explored last time. You
only need to watch out for cascading deletes, which can affect both kinds of
objects. When you do a cascading delete the object will be completely removed
from every place where it has been referenced. If you do a cascading delete to
remove a Person from the database, that Person's mother and father objects will in turn suddenly
have a null reference in their children collection, instead of a valid
object reference.
In many ways, storing arrays and collections into an object database isn't all that different from storing regular structured objects, with the simple caveat that arrays can't be queried directly whereas collections can. For all intents and purposes, this means that you can use collections and arrays as you model, rather than waiting for your persistence engine to demand the use of one or the other.
Learn
-
"The busy Java developer's guide to db4o: Beyond
simple objects" (Ted Neward, developerWorks, July 2007): Get started with structured objects and learn
more about how db4o handles activation depth, cascading updates and deletes, and referential integrity.
-
"The busy Java developer's guide to db4o: Queries,
updates, and identity" (Ted Neward, developerWorks, March 2007): Explore db4o's various mechanisms for
finding and retrieving data, including the OID.
-
Have
a Little Respect for SQL Databases" (Jack D. Herrington, DevX.com, October 2003):
Makes the case for what relational databases do well, including referential integrity.
-
In pursuit of code quality (Andrew Glover,
developerWorks series): Learn more about developer testing techniques like exploration testing.
-
The busy Java developer's guide to db4o
introduces db4o, an open source database that leverages today's object-oriented
languages, systems, and mindset (Ted Neward, developerWorks series).
-
db4o: Learn more about db4o.
-
ODBMS.org: An excellent collection of free material on object database technology.
-
The developerWorks Java technology zone: Hundreds of
articles about every aspect of Java programming.
-
New to IBM Information Management: Still not sold on OODBMS? Get more information about IBM's powerful family of relational database management system (RDBMS) servers.
Get products and technologies
-
Download db4o: An open source native Java
programming and .NET database.
Discuss
-
developerWorks
blogs: Get involved in the developerWorks community.
Comments (Undergoing maintenance)






