The busy Java developer's guide to db4o: Arrays and collections

Handling multiplicity in object databases

Collections and arrays introduce new levels of complexity to the structured objects first discussed in The busy Java™ developer's guide to db4o: Beyond simple objects. Fortunately, db4o isn't the least bit fazed by handling multiplicity relationships -- and neither should you be.

Share:

Ted Neward, Principal, Neward & Associates

Ted Neward photoTed Neward is the principal of Neward & Associates, where he consults, mentors, teaches, and presents on Java, .NET, XML services, and other platforms. He resides near Seattle, Washington.



18 September 2007

Also available in Russian

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.

About this series

Information storage and retrieval has been nearly synonymous with RDBMS for about a decade now. But recently, that has begun to change. Java developers in particular are frustrated with the so-called object-relational impedance mismatch and impatient with the solutions that attempt to resolve it. This, along with the emergence of a viable alternative, has led to a renaissance of interest in object persistence and retrieval. The busy Java developer's guide to db4o introduces db4o, an open source database that leverages today's object-oriented languages, systems, and mindset. Download db4o; you'll need it to follow the examples.

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 childrenArrayList 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++;
        }
    }

Understanding relationships

Querying arrays and collections

Even though Collection types are treated as first-class instances inside the db4o database, arrays are not. Attempting to do a native query or a prototype-based query looking for an array, of any type, will not return any objects for examination or retrieval. This is both a positive and a negative: For most situations, querying based on an array type would yield far more objects than desired. Think, for example, what executing a query for an Object would return in even a small object model. But there are times when being able to query for an array would be useful, and not being able to do it a hindrance.

Similarly, the fact that Collectionscan be queried yields the opposite effect: Attempting to query for an ArrayList will work, and will return every ArrayList inside the object database, regardless of its context. Practically speaking, the best way to choose between collections and arrays is to use arrays for "internal" collections (that are not to be accessible via queries) and Collection classes for "external" collections (that should be accessible via queries). Remember, too, that arrays can always be accessed as part of a native query, meaning that if I need to query the Persons database for Persons who are living at a particular address, I can write a query to retrieve Persons directly, and evaluate the Address there, rather than trying to directly query for Addresses out of the arrays.

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.)


Updates and deletes

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 conclusion

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.

Resources

Learn

Get products and technologies

  • Download db4o: An open source native Java programming and .NET database.

Discuss

Comments

developerWorks: Sign in

Required fields are indicated with an asterisk (*).


Need an IBM ID?
Forgot your IBM ID?


Forgot your password?
Change your password

By clicking Submit, you agree to the developerWorks terms of use.

 


The first time you sign into developerWorks, a profile is created for you. Information in your profile (your name, country/region, and company name) is displayed to the public and will accompany any content you post, unless you opt to hide your company name. You may update your IBM account at any time.

All information submitted is secure.

Choose your display name



The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

Required fields are indicated with an asterisk (*).

(Must be between 3 – 31 characters.)

By clicking Submit, you agree to the developerWorks terms of use.

 


All information submitted is secure.

Dig deeper into Java technology on developerWorks


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Java technology
ArticleID=255250
ArticleTitle=The busy Java developer's guide to db4o: Arrays and collections
publish-date=09182007