DekGenius.com
[ Team LiB ] Previous Section Next Section

5.9 Collections and Relationships

In a relational data model, relations are usually normalized. A relation is in first normal form if the cells of a table contain only a single atomic value, which is nondecomposable as far as the database is concerned. Initially, relational databases supported only simple types, such as integers, strings, and dates. Over time, they have added support for column types that can represent a set of data. But most relational database schema designs represent a collection of values with a set of rows.

You can represent a collection using a foreign key or a join table. We will examine each of these techniques in the following subsections. We'll consider the Movie and Role classes in the com.mediamania.content package and examine alternate ways of representing the relationship between these two classes in Java and a relational schema. For this discussion, we will ignore the inheritance relationship between Movie and MediaContent. We'll focus on the one-to-many relationship that exists between Movie and Role.

This mapping discussion is important when you are mapping between an existing relational schema and Java classes. If you're letting the JDO implementation generate a relational schema for you, or letting it generate your Java classes automatically from a relational schema, you do not need to be as concerned with the following discussion. However, as your object model and relational schema evolve, understanding the following material will become more important.

5.9.1 Using a Foreign Key

A one-to-many relationship between tables A and B usually is represented in a relational schema with a foreign key in B referencing the primary key in A. In the case of Movie and Role, the Role table should contain a foreign key that references the primary key of the Movie table. Example 5-3 uses this technique in the definition of the Movie and Role tables.

Example 5-3. SQL tables using a foreign key to represent a collection
CREATE TABLE Movie (
    oid         INTEGER,
    title       VARCHAR(24),
    rating      CHAR(4),
    genres      CHAR(16),
    PRIMARY KEY(oid)
)

CREATE TABLE Role (
    oid         INTEGER,
    name        VARCHAR(20),
    movie       INTEGER,     [1]
    PRIMARY KEY(oid),
    FOREIGN KEY(movie) REFERENCES Movie(oid)
)

Suppose you have Movie and Role tables, defined in SQL as shown in Example 5-3. With this schema, each Role row can reference only one Movie row. Multiple Role rows can reference the same Movie row via their movie column, declared on line [1]. Thus, the foreign-key column movie establishes the one-to-many relationship between Movie and Role in a relational schema.

The following SQL query accesses the Role rows that are associated with a specific Movie:

SELECT  name
FROM    Movie, Role
WHERE   title = 'Braveheart' AND Movie.oid = Role.movie

The join of the oid column in the Movie table with the movie column in the Role table associates the rows in the Role table with the one row in the Movie table that has a title column equal to 'Braveheart'.

You may have an existing relational schema that represents a collection or relationship using this foreign-key technique, and you may have to use this schema in your JDO application. Alternatively, if you do not have an existing schema, you may want to use a foreign key to represent your collection, as shown in Example 5-3. We will now examine several Java class designs to represent the relationship between Movie and Role with this relational schema.

5.9.1.1 Isomorphic mapping

Example 5-4 provides our first Java class design, in which we define a direct isomorphic mapping (identical form and structure) with the relational tables in Example 5-3.

Example 5-4. Isomorphic mapping between classes and tables
public class Movie {
    private String      theTitle;
    private String      movieRating;
    private String      genres;
}

public class Role {
    private String      name;
    private Movie       movie;     [1]
}

The Java classes do not have the oid table columns that are used to store the datastore identity in the relational tables. The Role class's movie field, declared on line [1], provides a reference to the associated Movie instance.

The following JDO metadata defines the mapping between the schema defined in Example 5-3 and the Java classes declared in Example 5-4:

<jdo>
    <package name="com.mediamania.content" >
        <class name="Movie" >
            <field name = "theTitle" >
                <extension vendor-name="vendorX" key="column" value="title" />
            </field>
            <field name = "movieRating" >
                <extension vendor-name="vendorX" key="column" value="rating" />
            </field>
            <field name = "genres" >
                <extension vendor-name="vendorX" key="column" value="genres" />
            </field>
            <extension vendor-name="vendorX" key="table" value="Movie" />
        </class>
        <class name="Role" >
            <field name="name" >
                <extension vendor-name="vendorX" key="column" value="name" />
            </field>
            <field name="movie" >
                <extension vendor-name="vendorX" key="column" value="movie" />
            </field>
            <extension vendor-name="vendorX" key="table" value="Role" />
        </class>
    </package>
</jdo>

However, the Java model in Example 5-4 does not provide a means to navigate from a Movie instance to its associated Role instances. Java and the JVM do not have the join facility found in a relational database. You could implement equivalent functionality in Java by examining all the Role instances to determine which instances reference a specific Movie instance. But this would be very inefficient if there were a large number of Role instances. Furthermore, this is not how you would normally represent and access such a relationship in Java.

If you are interested in accessing all the Role instances associated with a Movie referenced by the variable movie, and pm is initialized to the PersistenceManager, you can execute the following code:

Query q = pm.newQuery(Role.class);
q.setFilter("movie == param1");
q.declareParameters("Movie param1");
Collection result = (Collection) q.execute(movie);

This query returns an unmodifiable collection of Roles that refer to the Movie. The performance of this query would likely be similar to the performance you would get if the foreign key were represented by a collection, as we will describe in the following section.

You can also implement a method in the Movie class to add a Role to the movie:

    void addRole(Role role) {
        role.setMovie(this);
    }

This method removes the Role from whatever Movie it currently refers to and replaces it with the Movie (referenced by this). But this technique does not allow you to execute a portable query that navigates from a Movie to a Role, which can be done by using the contains( ) construct (described in Chapter 9). In order to do this, you would need to define a collection in Movie and map it to the datastore.

5.9.1.2 Defining a collection

You may want to define a collection in your Movie class that contains the set of associated Role instances, modeled by the foreign key movie (declared on line [1] in Example 5-3). Example 5-5 shows the Java classes for such a model.

Example 5-5. Using the foreign key to represent a collection
public class Movie {
    private String      theTitle;
    private String      movieRating;
    private String      genres;
    private Set         cast;     [1]     
}

public class Role {
    private String      name;
}

With this mapping, the movie column in the Role table represents the cast collection in the Movie class, which contains the Roles associated with a movie. Line [1] of the JDO metadata shown in Example 5-6 identifies the use of the movie column in the Role table for this purpose.

Example 5-6. JDO metadata for Java classes in Example 5-5 and schema in Example 5-3
<jdo>
    <package name="com.mediamania.content" >
        <class name="Movie" >
            <field name = "theTitle" >
                <extension vendor-name="vendorX" key="column" value="title" />
            </field>
            <field name = "movieRating" >
                <extension vendor-name="vendorX" key="column" value="rating" />
            </field>
            <field name = "genres" >
                <extension vendor-name="vendorX" key="column" value="genres" />
            </field>
            <field name="cast" >
                <collection element-type="Role"/>
            <extension vendor-name="vendorX" key="rel-column" value="Movie" />     [1]
            </field>
            <extension vendor-name="vendorX" key="table" value="Movie" />
        </class>
        <class name="Role" >
            <field name="name" >
                <extension vendor-name="vendorX" key="column" value="name" />
            </field>
            <extension vendor-name="vendorX" key="table" value="Role" />
        </class>
    </package>
</jdo>

The use of the rel-column on line [1] tells the implementation that the relation should be treated as a one-to-many association.

5.9.1.3 Defining a collection and a reference

Instead of using the Java model shown in Example 5-4, you are more likely to define the Movie class with a collection to contain the set of associated Role instances (as shown in line [1] of Example 5-7), in addition to the Movie reference in Role.

Example 5-7. Using a foreign key for both a collection and a reference in Java
public class Movie {
    private String      theTitle;
    private String      movieRating;
    private String      genres;
    private Set         cast;     [1]
}

public class Role {
    private String      name;
    private Movie       movie;     [2]
}

The metadata for the Java classes in Example 5-7 would be similar to Example 5-6, except we would also associate the movie field in the Role class with the movie column in the Role table. Adding a Role reference to a particular Movie instance's cast collection establishes a relationship between the Movie and Role instances. You can acquire an Iterator from a Movie instance's cast collection to access each Role instance associated with the Movie instance.

However, this model has a complication. Suppose you have two Movie instances. What happens if your Java application adds the same Role reference to the cast collection in both Movie instances? In Java, each cast collection could easily contain a reference to the same Role instance. But the collection is represented in the datastore via the foreign-key column named movie in the Role table. The movie column for a given Role row can reference only a single Movie row. How would this be handled at commit time? The implementation cannot store the fact that two Movie instances are referencing the same Role, given the schema defined in Example 5-3; it can store only one reference. The implementation should throw an exception at commit, or it may silently store only one of the Movie references. Consider the movie reference in the Role class, which can reference only a single Movie. If the Role instance is in memory, it may reference one of the Movie instances (let's call it M) that reference the Role in their cast collection. This may result in M being the one Movie that gets associated with the Role in the datastore.

However, if a Role can be referenced by multiple Movies and a Movie can reference multiple Roles, this is really a many-to-many relationship. But our design states that there should be a one-to-many relationship between Movie and Role. So, this situation should not occur if your Java application is honoring the cardinality of the relationship. Representing a many-to-many relationship in Java requires a collection in the classes at both ends of the relationship.

5.9.1.4 Managed relationships

Using a foreign key in the relational datastore to represent a collection in Java becomes especially cumbersome when the foreign key is represented by a reference at one end of the relationship and a collection at the other end. Some JDO implementations handle the mapping of a single foreign key to both sides of a relationship by providing a managed relationship. With this capability, if the application updates one side of a relationship, the JDO implementation updates the other side automatically. Some vendors do not support managed relationships, because they result in behavior that differs from the behavior of Java when using references and collections in non-JDO environments.

For example, if the application adds a Role instance to a Movie instance's cast collection, the implementation automatically sets the Role instance's movie reference to the Movie instance. Or, if the application removes a Role from a Movie instance's cast collection, the Role instance's movie reference is set to null automatically. Similarly, if the application sets the Role instance's movie reference to a particular Movie instance A, the implementation automatically removes the Role from the cast collection of the Movie instance currently referenced by movie (unless it is null) and it adds the Role to A's cast collection.

Currently, JDO does not support managed relationships, but some JDO implementations do support them. Implementations that support managed relationships provide a metadata extension that allows you to identify a field's inverse member, which is the member at the other end of the relationship. The metadata for specifying a managed relationship between Movie and Role would look like this:

<jdo>
    <package name="com.mediamania.content" >
        <class name="Movie" >
            <field name="cast" >
                <collection element-type="Role"/>
                <extension vendor-name="vendorX"     [1]
                           key="inverse" value="Role.movie"/>
            </field>
            <extension vendor-name="vendorX" key="table" value="Movie" />
        </class>
        <class name="Role" >
            <field name="movie" >
                <extension vendor-name="vendorX" key="column" value="movie"/>
                <extension vendor-name="vendorX"     [2]
                           key="inverse" value="Movie.cast"/>
            </field>
            <extension vendor-name="vendorX" key="table" value="Role" />
        </class>
    </package>
</jdo>

On line [1], an extension element is nested within the field element for Movie.cast to specify that Role.movie is its inverse member in the relationship. On line [2], an extension element is also nested in the field element for Role.movie to specify that Movies.cast is its inverse member.

Use of managed relationships in a JDO implementation is not portable to other JDO implementations. Many Java developers may consider such automatic maintenance behavior unusual. But it solves the problem of an application attempting to establish a relationship between Java instances that cannot be represented in the datastore with the schema defined in Example 5-3. A future JDO release may add support for managed relationships, if an approach can be designed that preserves JDO's level of transparency and consistency with Java.

5.9.2 Using a Join Table

We have presented three Java class designs that could be used to represent the schema defined in Example 5-3. Now let's consider another datastore representation of the Movie.cast collection. Some JDO implementations represent a collection with a set of rows in a join table. Each row contains the value for one collection element. Instead of having a foreign key in the Role table, a separate join table is defined to contain the elements of the cast collection. Example 5-8 provides a schema using a join table named Movie_cast.

Example 5-8. Use of a join table to represent a collection
CREATE TABLE Movie (
    oid         INTEGER,
    title       VARCHAR(24),
    rating      CHAR(4),
    genres      CHAR(16),
    PRIMARY KEY(oid)
)

CREATE TABLE Role (
    oid         INTEGER,
    name        VARCHAR(20),
    PRIMARY KEY(oid),

)

CREATE TABLE Movie_cast (
    movieoid    INTEGER NOT NULL,
    roleoid     INTEGER,
    PRIMARY KEY(movieoid, roleoid),
    FOREIGN KEY(movieoid) REFERENCES Movie(oid),     [1]
    FOREIGN KEY(roleoid)  REFERENCES Role(oid),     [2]
    CONSTRAINT r UNIQUE(roleoid)     [3]
)

The Movie_cast join table has two columns: movieoid references the associated Movie row (line [1]), and roleoid references the associated Role row (line [2]). Each element in a Movie.cast collection has a corresponding row in the Movie_cast table.

If a table like Movie_cast is used to represent a one-to-many relationship, you should define a unique constraint on the join table columns that correspond to the many side of the relationship. In this case, the roleoid has a unique constraint, shown on line [3], because it would be illegal to have the same Role appear more than once in the table. Even though the JDO implementation might allow you to add the Role to two different Movies, the datastore would disallow the operation at commit time.

Most JDO implementations let you specify the name of the join table representing a collection. We would specify the name of the table for the Movie.cast field by nesting a vendor-specific metadata extension within the collection element specified for Movie.cast. Most JDO implementations also let you specify the name of each column in the table.

Example 5-8 actually illustrates how many-to-many relationships normally are represented in a relational schema (except you would not have the UNIQUE constraint specified on line [3]). A given row in the Movie table can be associated with multiple rows in the Movie_cast table via the movieoid foreign key, and a given row in the Role table can be associated with multiple rows in the Movie_cast table. You would represent the many-to-many relationship in Java with a collection in both classes involved in the relationship. However, with this particular relational schema, it would be necessary to define a managed relationship to represent the many-to-many relationship. A single row in the Movie_cast table would represent the existence of an element in the collections of both classes involved in the many-to-many relationship.

5.9.3 One-to-One Relationships

In Java, you represent a one-to-one relationship between two classes by having a reference in each class that refers to an instance of the other class. As an example, consider the one-to-one relationship that exists between the Rental and RentalItem classes in the Media Mania application, illustrated in Figure 4-4. The Rental class has a field named rentalItem that references an instance of RentalItem. Likewise, the RentalItem class has a field named currentRental that references a Rental instance. We would likely define one or two methods that would preserve the relationship between these two classes and ensure that an instance of Rental and an instance of RentalItem refer to one another with these references.

For this example, we ignore the inheritance relationship between the Rental and Transaction classes. We define two relational tables, named Rental and RentalItem:

CREATE TABLE Rental (
    oid             INTEGER,
    item            INTEGER,     [1]
    return          TIMESTAMP,
    actualReturn    TIMESTAMP,
    code            INTEGER,
    PRIMARY KEY(oid),
    FOREIGN KEY(item)   REFERENCES RentalItem(oid),     [2]
    FOREIGN KEY(code)   REFERENCES RentalCode(oid)
    CONSTRAINT uniqitem UNIQUE(item)     [3]
)

CREATE TABLE RentalItem (
    oid             INTEGER,
    mediaItem       INTEGER,
    serial          VARCHAR(16),
    currentRental   INTEGER,
    PRIMARY KEY(oid),
    FOREIGN KEY(currentRental)  REFERENCES Rental(oid),
    FOREIGN KEY(mediaItem)      REFERENCES MediaItem(oid),
    CONSTRAINT uniqcurr UNIQUE(currentRental)
)

The Rental and RentalItem tables each have a foreign key that references the other table. The Rental table has a column named item, declared on line [1], that is a foreign key (line [2]) that references the RentalItem table. The RentalItem table has a column named currentRental, declared on line [4], that is a foreign key (line [5]) that references a row in the Rental table.

The uniqitem unique constraint on line [3] in the Rental table ensures that only a single row in Rental refers to a particular row in the RentalItem table. Likewise, the uniqcurr unique constraint on line [6] in the RentalItem table ensures that there is only a single row in the RentalItem table that refers to a particular row in the Rental table. While this relational representation directly mirrors our use of references in Java, it is actually redundant to maintain a foreign key in both tables in the relational model.

It is sufficient to define a foreign key in only one of the tables, having it reference the primary key of the other table. The tables could be defined as follows:

CREATE TABLE Rental (
    oid             INTEGER,
    return          TIMESTAMP,
    actualReturn    TIMESTAMP,
    code            INTEGER,

    item            INTEGER,     [1]
    PRIMARY KEY(oid),
    FOREIGN KEY(item)      REFERENCES RentalItem(oid),     [2]
    FOREIGN KEY(mediaItem) REFERENCES MediaItem(oid),
    CONSTRAINT uniqitem UNIQUE(item)     [3]
)

CREATE TABLE RentalItem (
    oid             INTEGER,
    mediaItem       INTEGER,
    serial          VARCHAR(16),
    PRIMARY KEY(oid)
)

The item column declared on line [1] in the Rental table is a foreign key (line [2]) that references a row in the RentalItem table. The uniqitem unique constraint on line [3] makes sure that only a single row in Rental refers to a particular row in the RentalItem table. The item column is sufficient to model the one-to-one relationship between Rental and RentalItem.

One-to-one relationships have some of the same issues that we explored with one-to-many relationships, relative to their representation in a relational datastore and how they are mapped into Java. To deal with these issues, some implementations support one-to-one managed relationships.

5.9.4 Representing Lists and Maps

Suppose we decide to use an ordered list of Roles in the Movie class. In Java, a List is used to represent an ordered collection. We redefine the Movie class as follows:

public class Movie {
    private String      title;
    private String      rating;
    private String      genres;
    private List        cast;
}

A JDO implementation must preserve a List's ordering in the datastore. To do so, it must maintain an ordering column to indicate the relative ordering of each collection element. If the collection is represented by a join table, as in Example 5-8, the ordering column is placed in the join table. The Movie_cast table then has the column declared on line [1]:

CREATE TABLE Movie_cast (
    movieoid    INTEGER,
    roleoid     INTEGER,
    elementidx  INTEGER,     [1]
    FOREIGN KEY(movieoid) REFERENCES Movie(oid)
    FOREIGN KEY(roleoid)  REFERENCES Role(oid)
)

If the collection is represented by a foreign key (as in Example 5-3), the ordering column is placed in the table containing the foreign key. Thus, the ordering column is placed directly in the Role table. Most implementations let you state the name of this ordering column.

By default, an implementation must preserve the ordering of the elements in a List in the datastore. Java does not provide an unordered collection class that allows duplicate elements. Some JDO implementations allow a List to be used to represent a collection when the ordering of the elements is not preserved in the datastore. You can specify this by nesting an extension element in the List's field or collection metadata element. If you do not need to preserve the order of a collection, this provides a more efficient mapping to the datastore.

If your persistent class has a Map, you must store the key and value of each Map element. The join table requires a column for the key and the value. Implementations usually let you declare the names of these columns. A Map does not require an ordering column.

    [ Team LiB ] Previous Section Next Section