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

Inheritance and polymorphism in db4o

Object-oriented applications make considerable use of inheritance, and they frequently want to use that inheritance (or "is-a") relationship to categorize and organize objects within a given system. This can prove difficult in a relational storage scheme, which has no intrinsic concept of inheritance, but in an OODBMS, it's a core feature. In this installment of The busy Java™ developer's guide to db4o, discover the surprising ease (and power) of using inheritance as a core feature when creating queries in db4o.

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.



23 October 2007

Also available in Russian

So far in this series, I've used the Person type to reveal almost all of the fundamentals of working with db4o. You've learned how to create entire graphs of Person objects, retrieve them in some fairly fine-grained manner (using db4o's native query functionality to constrain the actual object graph returned), update and delete entire graphs of objects (subject to some limitations), and so on. In fact, just one aspect of object-orientation has remained entirely outside of this discussion, and that is inheritance.

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.

While the end goal of my evolving example is a data management system for storing employee data, I've really focused on developing my Person type. I may want a system capable of storing information about people who work for a company, as well as their spouses and children, but at this point they're all just Persons to the system. (Or, if you'll pardon the poor English, Employeesis-aPerson, but Personsisn't-aEmployee.) Furthermore, I may want behavior for Employees that shouldn't be a part of the Person API. It's fair to say that from an object modeler's point of view, the ability to model types in is-a terms is the heart of what object-orientation is about.

While I could model the notion of employment using a field in the Person type, that's a relational approach and doesn't fit well within the object-design scenario. Fortunately, the db4o system, like most OODBMS systems, has an integral understanding of inheritance. Having inheritance at the core of a storage system makes it surprisingly easy to "refactor" the existing system into one that takes greater advantage of inheritance as a design concept, without complicating the query facilities. As you'll see, it also makes things much easier when querying for particular kinds of objects.

A highly evolved Person

Just in case you're joining the series now, Listing 1 is a recap of the Person type, which is the long-running example-in-progress for the series:

Listing 1. When we last left our hero ...
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 Person getFather() { return this.father; }
    public Person getMother() { return this.mother; }
    
    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;
}

As with the other articles in this series, I'm not going to show the Person class in full for each change I make, but just the incremental changes I make as I go along. In this case, I won't actually be making changes to Person at all because I'm going to extend Person rather than modify it.


Is-you or isn't-you?

The first thing I need to do is make it possible for my employee management system to recognize the difference between normal Persons (such as the spouses and/or children of employees) and Employees. From a pure modeling standpoint, this change is trivial. I just introduce a new derived class to Person, which is of the same package as the other classes thus far. Not surprisingly, I'll call this class Employee, shown in Listing 2:

Listing 2. Employee extends Person
package com.tedneward.model;

public class Employee extends Person
{
    public Employee()
    { }
    public Employee(String firstName, String lastName, String title,
                     Gender gender, int age, Mood mood)
    {
        super(firstName, lastName, gender, age, mood);
        
        this.title = title;
    }

    public String getTitle() { return title; }
    public void setTitle(String value) { title = value; }
    
    public String toString()
    {
        return "[Employee: " + getFirstName() + " " + getLastName() + " " +
            "(" + getTitle() + ")]";
    }
    
    private String title;
}

The Employee class doesn't actually have much more to it than what you see in Listing 2. From the perspective of the OODBMS, the additional methods on Employee are irrelevant. All you need to keep in mind for the discussion is that Employee is a subclass of Person. (If you are more interested in the modeling aspect of the system, imagine methods on Employee like promote(), demote(), getSalary() and setSalary(), and workLikeADog().)


Testing the new model

Exploration testing the new model is fairly straightforward. I create a JUnit class called InheritanceTest and set up what is, by now, a rather complicated set of objects to serve as the initial working contents of the OODBMS. Just to make the output (which you'll see in Listing 6) more clear, I've shown the details of the @Before-annotated prepareDatabase() call in Listing 3:

Listing 3. Welcome to the company (you belong to me now)
    @Before public void prepareDatabase()
    {
        db = Db4o.openFile("persons.data");

        // The Newards
        Employee ted = new Employee("Ted", "Neward", "President and CEO",
            Gender.MALE, 36, Mood.HAPPY);
        Person charlotte = new Person("Charlotte", "Neward",
            Gender.FEMALE, 35, Mood.HAPPY);
        ted.setSpouse(charlotte);
        Person michael = charlotte.haveBaby("Michael", Gender.MALE);
        michael.setAge(14);
        Person matthew = charlotte.haveBaby("Matthew", Gender.MALE);
        matthew.setAge(8);
        Address tedsHomeOffice = 
            new Address("12 Redmond Rd", "Redmond", "WA", "98053");
        ted.setHomeAddress(tedsHomeOffice);
        ted.setWorkAddress(tedsHomeOffice);
        ted.setVacationAddress(
            new Address("10 Wannahokalugi Way", "Oahu", "HA", "11223"));
        db.set(ted);
        
        // The Tates
        Employee bruce = new Employee("Bruce", "Tate", "Chief Technical Officer", 
            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);
        bruce.setHomeAddress(
            new Address("5 Maple Drive", "Austin",
                "TX", "12345"));
        bruce.setWorkAddress(
            new Address("5701 Downtown St", "Austin",
                "TX", "12345"));
        // Ted and Bruce both use the same timeshare, apparently
        bruce.setVacationAddress(
            new Address("10 Wanahokalugi Way", "Oahu",
                "HA", "11223"));
        db.set(bruce);
        
        // The Fords
        Employee neal = new Employee("Neal", "Ford", "Meme Wrangler",
            Gender.MALE, 29, Mood.HAPPY);
        Person candi = new Person("Candi", "Ford",
            Gender.FEMALE, 29, Mood.HAPPY);
        neal.setSpouse(candi);
        neal.setHomeAddress(
            new Address("22 Gritsngravy Way", "Atlanta", "GA", "32145"));
        // Neal is the roving architect
        neal.setWorkAddress(null);
        db.set(neal);
        
        // The Slettens
        Employee brians = new Employee("Brian", "Sletten", "Bosatsu Master",
            Gender.MALE, 29, Mood.HAPPY);
        Person kristen = new Person("Kristen", "Sletten",
            Gender.FEMALE, 29, Mood.HAPPY);
        brians.setSpouse(kristen);
        brians.setHomeAddress(
            new Address("57 Classified Drive", "Fairfax", "VA", "55555"));
        brians.setWorkAddress(
            new Address("1 CIAWasNeverHere Street", "Fairfax", "VA", "55555"));
        db.set(brians);
        
        // The Galbraiths
        Employee ben = new Employee("Ben", "Galbraith", "Chief UI Director",
            Gender.MALE, 29, Mood.HAPPY);
        Person jessica = new Person("Jessica", "Galbraith",
            Gender.FEMALE, 29, Mood.HAPPY);
        ben.setSpouse(jessica);
        ben.setHomeAddress(
            new Address(
                "5500 North 2700 East Rd", "Salt Lake City", 
                "UT", "12121"));
        ben.setWorkAddress(
            new Address(
                "5600 North 2700 East Rd", "Salt Lake City",
                "UT", "12121"));
        ben.setVacationAddress(
            new Address(
                "2700 East 5500 North Rd", "Salt Lake City",
                "UT", "12121"));
            // Ben really needs to get out more
        db.set(ben);
        
        db.commit();
    }

As with previous exploration testing examples in this series, I use the @After-annotated deleteDatabase() method to delete the database after each test, just to keep things nicely partitioned.

Let's just run a few queries ...

Before actually running that method, I'll check to see what effect, if any, having Employees in the system will have. It's natural to want to fetch all the Employees from the database — perhaps to fire them all as part of the company going bankrupt. (Yes, I know, it's cruel to think about it, but I'm still a little scarred from the dot-bomb collapse of 2001.) The initial test looks pretty straightforward, as you can see in Listing 4:

Listing 4. The Ted says, 'You're fired!'
@Test public void testSimpleInheritanceQueries()
{
    ObjectSet employees = db.get(Employee.class);
    while (employees.hasNext())
        System.out.println("Found " + employees.next());
}

When run, the test produces an interesting result: only the Employees in the database (myself, Ben, Neal, Brian, and Bruce) are returned. Implicitly, the OODBMS has recognized that the query is constrained explicitly by the subtype Employee, and has only selected those objects that fit that criteria for return. Because none of the other objects (the spouses or the children) are of type Employee, they aren't eligible and therefore aren't sent back.

Things get more interesting when I run a simple query to return all Persons, like so:

Listing 5. Find all humans!
@Test public void testSimpleNonEmployeeQuery()
{
    ObjectSet persons = db.get(Person.class);
    while (persons.hasNext())
        System.out.println("Found " + persons.next());
}

When I run this query, every single object — including all the Employees returned earlier — is sent back. At one level, this makes sense. Because Employeeis-aPerson, by virtue of the implementation-inheritance relationship established in the Java code, the criteria necessary for return from this query is met.

Inheritance (and, by extension, polymorphism) in db4o really is that simple. No complicated IS extensions to a query language, no introduced notion of "type" beyond what already exists in the Java type system. I simply indicate the type desired as part of the query, and those are the types that will form the basis of the query. It's almost like joining tables in an SQL query, by selecting tables whose data should be part of the results of the query. The added bonus, in this case, is that "parent types" are also implicitly "joined" as part of the query. Listing 6 shows the output from my InheritanceTest back in Listing 3:

Listing 6. Polymorphism at work
.Found [Employee: Ted Neward (President and CEO)]
Found [Person: firstName = Charlotte lastName = Neward gender = FEMALE age = 35
mood = HAPPY spouse = Ted ]
Found [Person: firstName = Michael lastName = Neward gender = MALE age = 14 mood
 = CRANKY ]
Found [Person: firstName = Matthew lastName = Neward gender = MALE age = 8 mood
= CRANKY ]
Found [Employee: Bruce Tate (Chief Technical Officer)]
Found [Person: firstName = Maggie lastName = Tate gender = FEMALE age = 29 mood
= HAPPY spouse = Bruce ]
Found [Person: firstName = Kayla lastName = Tate gender = FEMALE age = 0 mood =
CRANKY ]
Found [Person: firstName = Julia lastName = Tate gender = FEMALE age = 0 mood =
CRANKY ]
Found [Employee: Neal Ford (Meme Wrangler)]
Found [Person: firstName = Candi lastName = Ford gender = FEMALE age = 29 mood =
 HAPPY spouse = Neal ]
Found [Employee: Brian Sletten (Bosatsu Master)]
Found [Person: firstName = Kristen lastName = Sletten gender = FEMALE age = 29 m
ood = HAPPY spouse = Brian ]
Found [Employee: Ben Galbraith (Chief UI Director)]
Found [Person: firstName = Jessica lastName = Galbraith gender = FEMALE age = 29
 mood = HAPPY spouse = Ben ]

It may surprise you that the objects returned are still objects of the appropriate subtype, despite how they were queried. For example, in the above query, when toString() is called on each of the Person objects returned, Person.toString() is invoked for each Person, as expected. Because Employees have an overridden toString() method, however, the usual rules regarding dynamic binding kick in. The parts of Person that are stored in Employee aren't "sliced off," as can happen in a regular SQL query that fails to join every derived subclass table in a table-per-class model.


Inheriting from the natives

Of course, inheritance criteria extends into native queries just as strongly as it does the simple object queries I've done so far. The query syntax gets just a tad more complicated when the call is made, but fundamentally it follows from the syntax I've already used before, shown in Listing 7:

Listing 7. Are you a company widow?
@Test public void testNativeQuery()
{
    List<Person> spouses =
        db.query(new Predicate<Person>() {
            public boolean match(Person candidate) {
                return (candidate.getSpouse() instanceof Employee);
            }
        });
    for (Person spouse : spouses)
        System.out.println(spouse);
}

I begin with a query similar, in spirit, to the one I did earlier, looking through all the Persons in the system, but I constrain it further by looking for only those Persons who have a spouse that is an Employee— a call to getSpouse(), with the return value passed to the Java instanceof operator, and it's done. (Remember that the match() call only returns true or false, indicating whether the candidate object should be returned.)

Notice how changing the type of the Predicate passed in the query() call can change the type criteria implicitly selected, shown in Listing 8:

Listing 8. Scandalous! Office romance!
@Test public void testEmployeeNativeQuery()
{
    List<Employee> spouses =
        db.query(new Predicate<Person>() {
            public boolean match(Person candidate) {
                return (candidate.getSpouse() instanceof Employee);
            }
        });
    for (Person spouse : spouses)
        System.out.println(spouse);
}

When executed, this query produces no results because now the query is looking only for Employees with a spouse also employed by the company. Currently, no employee in the database satisfies that criteria. Should the company decide to hire Charlotte, then two Employees would be returned, however: Ted and Charlotte. (And they say office romance never works.)

For the most part, again, that's it. Inheritance doesn't have any effect on updates, deletes, or activation depth, just the query aspect of objects. But recall that the Java platform offers two forms of inheritance: implementation inheritance, via the ubiquitous extends clause, and interface inheritance, via implements. If db4o supports extends, then it also must support implements, which makes for a pretty powerful query capability, as you'll see.


It's all about the interface

As just about any Java (or C#) programmer learns after about six weeks of using the language, interfaces are highly useful for modeling. Although not often seen as such, interfaces present a powerful ability to "segregate" types across traditional implementation inheritance lines; through the use of an interface, I can declare that certain types are Comparable, or Serializable, or in this case, Employable. (Yes, this is overkill from a design perspective, but it serves the pedagogical purpose nicely.)

Listing 9. Hey, it's not 2001 anymore! Come work for me!
package com.tedneward.model;

public interface Employable
{
    public boolean willYouWorkForUs();
}

To see the interface at work, I need a concrete class inheriting from the Employable interface, and — you probably guessed it already — that means creating an EmployablePerson subtype that extends Person and implements Employable. I won't show that code here (there's not much worth showing, aside from adding ** EMPLOYABLE ** to the end of Person's toString() in the EmployablePerson.toString() method). I will also modify the prepareDatabase() call to return the fact that Charlotte is an EmployablePerson, instead of just a Person.

Now, I can write a query that iterates through the database, looking for spouses or relatives of our current employees that might be willing to work for the company, shown in Listing 10

Listing 10. There's this job, see ...
@Test public void testEmployableQuery()
{
    List<Employable> potentialEmployees =
        db.query(new Predicate<Employable>() {
            public boolean match(Employable candidate) {
                return (candidate.willYouWorkForUs());
            }
        });
    for (Employable e : potentialEmployees)
        System.out.println("Eureka! " + e + " has said they'll work for us!");
}

Sure enough, Charlotte is returned as being employable. What's better, this means that any interface I introduce becomes a new way to constrain queries, without artificially introducing fields to carry the information; Charlotte fits the query criteria solely because she implements that interface, whereas none of the other spouses do (at least, for now).

In conclusion

Roles and objects

Some object models will, if you'll pardon the pun, object to my using interfaces and inheritance to model what is, essentially, a role played by Persons. For example, assume the spouse of an Employee decides to come work for the company -- does it make sense to remove them as a Person from the system and re-insert them as an Employee? Roles can, and frequently do, change over time. Objects aren't supposed to change their base and interface types to go along with every shift in role.

This is a fair argument, and one that can't be solved in this particular article, if it can be solved at all. For now, let's just say that my use of inheritance and interfaces is purely demonstrative and pedagogical. For further study, see "Role object" in the Resources section.

If objects and inheritance go together like chocolate and peanut butter, objects and polymorphism are like hand and glove. The two elements fit just as snugly as a manager and his or her high salary. Any system that stores objects has to be able to understand and project this notion of inheritance into the storage media and filter along it when retrieving data. Fortunately, object-oriented DBMSs render this a trivial undertaking, and they let you do it without having to introduce new query-predicate terms. In the long run, that makes the OODBMS much easier to use when inheritance is part of the picture.

Resources

Learn

  • "The busy Java developer's guide to db4o: Beyond simple objects" (Ted Neward, developerWorks, July 2007): Get started with structured objects and exploration testing in db4o.
  • "Role object" (Dirk Bäumer, Dirk Riehle, Wolf Siberski, and Martina Wulf; Pattern Languages of Program Design 4, Addison-Wesley, 2000): Explains role objects, wherein representing roles as individual objects enables you to keep different contexts separate and thus simplify system configuration.
  • 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 (Ted Neward, developerWorks series): Introduces db4o, an open source database that leverages today's object-oriented languages, systems, and mindset.
  • The db4o home page: 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

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=263800
ArticleTitle=The busy Java developer's guide to db4o: Structured objects and collections
publish-date=10232007