Skip to main content

The busy Java developer's guide to db4o: Beyond simple objects

Create, update, and delete structured objects using db4o

Ted Neward, Principal, Neward & Associates
Ted Neward photo
Ted 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.

Summary:  So far, creating objects and manipulating them in db4o looks pretty easy -- maybe a little too easy, in fact. In this article, db4o enthusiast Ted Neward shows you what happens when simple objects become structured ones (that is, objects that reference objects) and issues like infinite recursion, cascading behavior, and referential integrity come into play.

View more content in this series

Date:  26 Jun 2007
Level:  Introductory
Activity:  4320 views

So far in The busy Java developer's guide to db4o, I've looked at the various ways db4o can be used to store Java objects without relying on mapping files. Freedom from object-relational mapping is one of the perks of using a native object database (if not the point of it), but the object model I've used to demonstrate this freedom has been unrealistically simplistic. Most enterprise systems require creating and manipulating fairly complex objects, also known as structured objects, so this article shifts the discussion toward structured object creation.

A structured object is, in essence, an object that holds references to other objects. While db4o allows you to perform all the usual CRUD operations on structured objects, you cannot expect to do so without encountering some complexity. I explore some of the main culprits of that complexity in this article (such as infinite recursion, cascading behavior, and referential integrity) and delve into more advanced structured-object handling in the next one. As a bonus, I also introduce exploration testing: a little-known testing technique that allows you to test both a class library and the db4o API.

From simple to structured

Listing 1 is a recap of the simple Person class I've used in my introduction to db4o so far:


Listing 1. Person
                
package com.tedneward.model;

public class Person
{
    public Person()
    { }
    public Person(String firstName, String lastName, int age, Mood mood)
    {
        this.firstName = firstName;
        this.lastName = lastName;
        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 int getAge() { return age; }
    public void setAge(int value) { age = value; }
    
    public Mood getMood() { return mood; }
    public void setMood(Mood value) { mood = value; }

    public String toString()
    {
        return 
            "[Person: " +
            "firstName = " + firstName + " " +
            "lastName = " + lastName + " " +
            "age = " + age + " " + 
            "mood = " + mood +
            "]";
    }
    
    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.age == other.age);
    }
    
    private String firstName;
    private String lastName;
    private int age;
    private Mood mood;
}

String in OODBMS systems

You may recall that the Person type I've used for examples in previous articles has employed Strings as fields. In Java and .NET String is an object type, inheriting from Object, which seems to pose a discrepancy. In fact, most OODBMS systems, including db4o, treat Strings differently than they do other objects, particularly given the String's immutable nature.

The simple Person class has served me well as an example for introducing basic storage, query, and retrieval operations in db4o, but it doesn't address the real-world complexity of enterprise programming. For example, it probably isn't uncommon for the Persons in your database to have a home address. Some might also have a spouse, and possibly children.

I want to add a field in the database for "Spouse," which means extending Person to reference a Spouse object. Given that I'm dealing with some business rules, I also need to add a Gender enumerated type with appropriate modifications and an equals() method to the constructor. In Listing 2, the Person type gets a spouse field and an appropriate get/set method pair, this time with some business rules attached:


Listing 2. Is this person marriageable?
                
package com.tedneward.model;
public class Person {
    // . . .

    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);
    }
    
    private Person spouse;    
}

Listing 3 shows the code to create two marriageable Persons; it is pretty much as you might expect:


Listing 3. Goin' to the chapel, gonna get married ...
                
import java.util.*;
import com.db4o.*;
import com.db4o.query.*;
import com.tedneward.model.*;

public class App
{
    public static void main(String[] args)
        throws Exception
    {
        ObjectContainer db = null;
        try
        {
            db = Db4o.openFile("persons.data");

            Person ben = new Person("Ben", "Galbraith", 
                Gender.MALE, 29, Mood.HAPPY);
            Person jess = new Person("Jessica", "Smith", 
                Gender.FEMALE, 29, Mood.HAPPY);
            
            ben.setSpouse(jess);
            
            System.out.println(ben);
            System.out.println(jess);
            
            db.set(ben);
            
            db.commit();
            
            List<Person> maleGalbraiths = 
                db.query(new Predicate<Person>() {
                    public boolean match(Person candidate) {
                        return candidate.getLastName().equals("Galbraith") &&
                                candidate.getGender().equals(Gender.MALE);
                    }
                });
            for (Person p : maleGalbraiths)
            {
                System.out.println("Found " + p);
            }
        }
        finally
        {
            if (db != null)
                db.close(); 
        }
    }
}

Complexity rears its shaggy head

A couple of important things are happening here, aside from the potentially objectionable business rules. First, when the ben object is stored into the database, the OODBMS clearly does something beyond just storing a single object. Upon retrieving the ben object back again, the associated spouse has not only also been stored but also automatically retrieved.

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. See the db4o home page to download db4o now; you'll need it to follow the examples.

When you think about it, this has some scary implications. While it isn't impossible to see how the OODBMS avoids an infinite recursion scenario, the much scarier proposition comes when thinking about an object with dozens, possibly hundreds or thousands of object references, each with references of its own. Just consider what will happen when the model represents children, parents, and so on. Retrieving a single Person out of the database could very well lead to retrieving all of humanity back to the beginning of time. That's a lot of objects to pull across a network!

Fortunately, all but the most primitive of OODBMS's address these concerns, and db4o is no exception.

Exploration testing db4o

Exploring this area of db4o is a tricky undertaking and gives me the opportunity to show off a tactic taught to me by good friend: exploration tests. (Thanks to Stu Halloway, who first coined the phrase, as far as I know.) An exploration test, in a nutshell, is a series of unit tests written not only to test the library in question, but also to explore the API and ensure that the library behaves the way it is expected to. As a useful side-effect of this approach, future library versions can then be dropped into the exploration test code, compiled, and tested. If the code doesn't compile or the exploration tests don't all pass, then the library is clearly not backward-compatible, and you will find out before ever trying to use it in a production system.

Exploration testing the db4o API also allows me to set up a "before" method to create the database and populate it with Persons and an "after" method to destroy the database and eliminate the possibility of a false positive appearing during the tests. Without that, I have to remember to manually delete the persons.data file each time. Frankly, I don't trust myself to remember that every time when I'm spelunking an API.

For my exploration testing of db4o, I'm going to use the JUnit 4 test library in console mode. Before I write any tests, the StructuredObjectTest class looks as shown in Listing 4:


Listing 4. Test-infecting the db4o API
                
import java.io.*;
import java.util.*;
import com.db4o.*;
import com.db4o.query.*;
import com.tedneward.model.*;

import org.junit.Before;
import org.junit.After; 
import org.junit.Ignore; 
import org.junit.Test; 
import static org.junit.Assert.*;

public class StructuredObjectsTest
{
    ObjectContainer db;

    @Before public void prepareDatabase()
    {
        db = Db4o.openFile("persons.data");

        Person ben = new Person("Ben", "Galbraith", 
            Gender.MALE, 29, Mood.HAPPY);
        Person jess = new Person("Jessica", "Smith", 
            Gender.FEMALE, 29, Mood.HAPPY);
    
        ben.setSpouse(jess);
    
        db.set(ben);
    
        db.commit();
    }
    
    @After public void deleteDatabase()
    {
        db.close();
        new File("persons.data").delete();
    }


    @Test public void testSimpleRetrieval()
    {
        List<Person> maleGalbraiths = 
            db.query(new Predicate<Person>() {
                public boolean match(Person candidate) {
                    return candidate.getLastName().equals("Galbraith") &&
                            candidate.getGender().equals(Gender.MALE);
                }
            });
            
        // Should only have one in the returned set
        assertEquals(maleGalbraiths.size(), 1);

        // (Shouldn't display to the console in a unit test, but this is an
        // exploration test, not a real unit test)
        for (Person p : maleGalbraiths)
        {
            System.out.println("Found " + p);
        }
    }
}

Naturally, running the JUnit test runner against this test suite produces the expected output: either the "." or the green bar, depending on my choice of test runners (console or GUI). Note that it's normally frowned upon to write data to the console -- that data should be verified using assertions, not eyeballs -- but in the case of an exploration test, it's a good way to see what I get back before asserting the appropriate data. If all else fails, I can always comment out the System.out.println calls. (Feel free to add to the tests however you like to test other aspects of the db4o API.)

From this point forward, assume that code samples are test methods inside the test suite shown in Listing 4 (as indicated by the @Test annotation on the method signature).


Storing and retrieving structured objects

Storing structured objects is, for the most part, an exercise in what's been done before: call db.set() on the object and the OODBMS takes care of the rest. The object on which the set() call is made doesn't much matter because the OODBMS tracks objects by an Object identifier (OID) (see " The busy Java developer's guide to db4o: Queries, updates, and identity") and so does not know to store the same object twice.

Retrieving structured objects is what raises the hair on the back of my neck. If the object being retrieved (either through a QBE or native query) holds a number of object references, and if each of those objects holds a number of object references, and so on, and so on. It sounds kind of like a bad Ponzi scheme, doesn't it?

Getting around infinite recursion

Despite the initial reaction of most developers (best summarized as "They can't really be doing that, right?"), infinite recursion is exactly how db4o approaches structured object retrieval, to a point. In fact, this kind of behavior is exactly what most programmers would want because we generally expect the objects we've created to "just be there" when we go looking for them. At the same time, we obviously don't want to pull the whole world across the wire, at least not all at once.

db4o compromises by constraining the number of objects retrieved, using what it calls activation depth, which indicates the number of levels down the object graph to be retrieved. In other words, activation depth is a count of the number of references identified from the root object that db4o will traverse and return as part of a query. In the previous case, when retrieving Ben, the default activation depth of 5 is sufficient to also retrieve Jessica because she only requires a single reference-traversal. Any objects more than 5 reference-hops away from Ben will not be retrieved, and their references will be left to be null. It would then be my job to explicitly activate those objects from the database using the activate() method on the ObjectContainer.

In the case where I might want to change the default activation depth, I can do so in a fine-grained manner, by using db4o's activationDepth() method on the Configuration class (returned from db.configure()) to set the default to some other value. Alternatively, I can configure the activation depth on a per-class basis. In Listing 5, I use ObjectClass to configure the activation depth for the Person type:


Listing 5. Using ObjectClass to configure activation depth
                
// See ObjectClass for more info
Configuration config = Db4o.configure();
ObjectClass oc = config.objectClass("com.tedneward.model.Person");
oc.minimumActivationDepth(10);


Updating structured objects

Updates are another concern: What happens if I update an object in the graph but don't explicitly set it? Just as the initial call to set() stores dependent objects that reference the object being stored, when an object is passed into the ObjectContainer, db4o traverses the references and stores objects it finds into the database as well, as shown in Listing 6:


Listing 6. Updating referenced objects
                
@Test public void testDependentUpdate()
{
    List<Person> maleGalbraiths = 
        db.query(new Predicate<Person>() {
            public boolean match(Person candidate) {
                return candidate.getLastName().equals("Galbraith") &&
                        candidate.getGender().equals(Gender.MALE);
            }
        });
        
    Person ben = maleGalbraiths.get(0);
        
    // Happy birthday, Jessica!
    ben.getSpouse().setAge(ben.getSpouse().getAge() + 1);

    // We only have a reference to Ben, so store that and commit
    db.set(ben);
    db.commit();

    // Find Jess, make sure she's 30
    Person jess = (Person)db.get(
            new Person("Jessica", "Galbraith", null, 0, null)).next();
    assertTrue(jess.getAge() == 30);
}

Although the change was made to the jess object, the ben object holds a reference to jess. The updated change on the in-memory jess Person therefore persists through to the database.

Well, not really. Okay, I lied completely.

Testing for false positives

As it turns out, this is one area where the exploration test fails and yields a false positive. Although it's not obvious from the documentation, the ObjectContainer maintains a cache of activated objects, so when the test in Listing 6 retrieves the Jessica object from the container, it returns the in-memory object containing the change, not the actual data written to disk. Which, in turn, hides the fact that the default update depth on a type is 1, meaning only primitive values (including Strings) will be stored on a set() call. To see this behavior in action, I have to modify my test somewhat, as shown in Listing 7:


Listing 7. Testing for a false positive
                
@Test(expected=AssertionError.class)
public void testDependentUpdate()
{
    List<Person> maleGalbraiths = 
        db.query(new Predicate<Person>() {
            public boolean match(Person candidate) {
                return candidate.getLastName().equals("Galbraith") &&
                        candidate.getGender().equals(Gender.MALE);
            }
        });
            
    Person ben = maleGalbraiths.get(0);
    assertTrue(ben.getSpouse().getAge() == 29);
    
    // Happy Birthday, Jessica!
    ben.getSpouse().setAge(ben.getSpouse().getAge() + 1);

    // We only have a reference to Ben, so store that and commit
    db.set(ben);
    db.commit();
        
    // Close the ObjectContainer, then re-open it
    db.close();
    db = Db4o.openFile("persons.data");

    // Find Jess, make sure she's 30
    Person jess = (Person)db.get(
            new Person("Jessica", "Galbraith", null, 0, null)).next();
    assertTrue(jess.getAge() == 30);
}

When I do this, I get the AssertionFailure, which puts the lie to my former statements about updates to objects cascading down through the graph. (You can prime JUnit to expect this failure by setting the expected value on the @Test annotation of the class type you expect to be thrown.)

Set cascading behavior

That db4o simply returns cached objects instead of more implicitly handling them is something of a contentious issue. Most programmers believe either that the behavior is destructive and counterintuitive, or that it is exactly the way an OODBMS should behave. Without getting into the merits of either position, it is important to understand the database's default behavior and know how to modify it. In Listing 8, I've used the ObjectClass.setCascadeOnUpdate() method to change db4o's default update behavior for a particular type. Note, however, that I had to set the method to true before opening the ObjectContainer. Listing 8 shows the revised, correctly cascading test.


Listing 8. Set cascading behavior to true
                
@Test
public void testWorkingDependentUpdate()
{
    // the cascadeOnUpdate() call must be done while the ObjectContainer
    // isn't open, so close() it, setCascadeOnUpdate, then open() it again
    db.close();
    Db4o.configure().objectClass(Person.class).cascadeOnUpdate(true);
    db = Db4o.openFile("persons.data");

    List<Person> maleGalbraiths = 
        db.query(new Predicate<Person>() {
            public boolean match(Person candidate) {
                return candidate.getLastName().equals("Galbraith") &&
                        candidate.getGender().equals(Gender.MALE);
            }
        });
           
    Person ben = maleGalbraiths.get(0);
    assertTrue(ben.getSpouse().getAge() == 29);
        
    // Happy Birthday, Jessica!
    ben.getSpouse().setAge(ben.getSpouse().getAge() + 1);

    // We only have a reference to Ben, so store that and commit
    db.set(ben);
    db.commit();
        
    // Close the ObjectContainer, then re-open it
    db.close();
        
    db = Db4o.openFile("persons.data");

    // Find Jess, make sure she's 30
    Person jess = (Person)db.get(
            new Person("Jessica", "Galbraith", null, 0, null)).next();
    assertTrue(jess.getAge() == 30);
}

You can set cascading behavior not only for updates but also for retrieval (thus creating an activation depth of "unlimited") and deletion -- which is the last exercise I attempt on my newly sophisticated Person object.


Deleting structured objects

Deleting an object from the database works along the same lines as retrieval and update: By default, when an object is deleted, none of the objects it references are deleted with it. Again, this is generally desirable behavior, as shown in Listing 9:


Listing 9. Deleting a structured object
                
@Test
public void simpleDeletion()
{
  Person ben = (Person)db.get(new Person("Ben", "Galbraith", null, 0, null)).next();
  db.delete(ben);
        
  Person jess = (Person)db.get(new Person("Jessica", "Galbraith", null, 0, null)).next();
  assertNotNull(jess);
}

At times, however, you will want to force objects referenced by a deleted object to be deleted as well. Just as with activation and updates, you can set this behavior by making a call on the Configuration class, as shown in Listing 10:


Listing 10. Configuration.setCascadeOnDelete()
                
@Test
public void cascadingDeletion()
{
    // the cascadeOnUpdate() call must be done while the ObjectContainer
    // isn't open, so close() it, setCascadeOnUpdate, then open() it again
    db.close();
    Db4o.configure().objectClass(Person.class).cascadeOnDelete(true);
    db = Db4o.openFile("persons.data");

    Person ben = 
        (Person)db.get(new Person("Ben", "Galbraith", null, 0, null)).next();
    db.delete(ben);
        
    ObjectSet<Person> results = 
        db.get(new Person("Jessica", "Galbraith", null, 0, null));
    assertFalse(results.hasNext());
}

Do this with care, however, for it means that any other objects referencing a cascade-deleted object will now hold references to null -- there is no sense of referential integrity in a db4o object database to prevent the deletion of referenced objects. (Referential integrity is a commonly requested feature in db4o, and I'm told the development team is thinking about ways to add it in a future version. The key will be to do it in a way that doesn't violate the Principle of Least Surprise for developers using db4o, given that sometimes, even in a relational database, breaking integrity is actually a desirable practice.)


In conclusion

This article marks a watershed moment in this series: Up until this point, I've based all of my examples on fairly simplistic objects. While the examples were unrealistic from an application perspective, they did allow you to focus on understanding the OODBMS, rather than the objects it stored. Understanding how an OODBMS like db4o stores associated objects held through references is a tricky business. Fortunately, once you've got the behavior down (thoroughly explained and understood), you just need to begin adjusting your code to take its behavior into account.

In this article, you've seen some initial examples of adjusting complex code to account for db4o's object model. You've learned how to perform simple CRUD operations on structured objects, and along the way, you've seen some of the problems and workarounds inevitably encountered.

Thus far, however, my structured object examples still retain some degree of simplicity in that I've only manipulated direct references to them. As many couples learn, once you've been married for a while, the subject of children comes into play. In my next article in this series, I'll continue to explore structured object creation and manipulation in db4o, by seeing what happens to my ben and jess objects when I introduce a few children into their happy picture.


Resources

Learn

Get products and technologies

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

Discuss

About the author

Ted Neward photo

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

Comments (Undergoing maintenance)



Trademarks  |  My developerWorks terms and conditions

Help: Update or add to My dW interests

What's this?

This little timesaver lets you update your My developerWorks profile with just one click! The general subject of this content (AIX and UNIX, Information Management, Lotus, Rational, Tivoli, WebSphere, Java, Linux, Open source, SOA and Web services, Web development, or XML) will be added to the interests section of your profile, if it's not there already. You only need to be logged in to My developerWorks.

And what's the point of adding your interests to your profile? That's how you find other users with the same interests as yours, and see what they're reading and contributing to the community. Your interests also help us recommend relevant developerWorks content to you.

View your My developerWorks profile

Return from help

Help: Remove from My dW interests

What's this?

Removing this interest does not alter your profile, but rather removes this piece of content from a list of all content for which you've indicated interest. In a future enhancement to My developerWorks, you'll be able to see a record of that content.

View your My developerWorks profile

Return from help

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Java technology
ArticleID=236262
ArticleTitle=The busy Java developer's guide to db4o: Beyond simple objects
publish-date=06262007
author1-email=ted@tedneward.com
author1-email-cc=jaloi@us.ibm.com

My developerWorks community

Tags

Help
Use the search field to find all types of content in My developerWorks with that tag.

Use the slider bar to see more or fewer tags.

Popular tags shows the top tags for this particular content zone (for example, Java technology, Linux, WebSphere).

My tags shows your tags for this particular content zone (for example, Java technology, Linux, WebSphere).

Use the search field to find all types of content in My developerWorks with that tag. Popular tags shows the top tags for this particular content zone (for example, Java technology, Linux, WebSphere). My tags shows your tags for this particular content zone (for example, Java technology, Linux, WebSphere).

Rate a product. Write a review.

Special offers