Documentation Archive Developer
Search
PATH  Documentation > WebObjects 4.5 > EOF Developer's Guide

Table of Contents Previous Section

Modeling Relationships

How you model relationships is perhaps the most complex and interesting part of a database-to-objects mapping. This section describes some of the finer points of relationship modeling.

Modeling Optional To-One Relationships

A to-one relationship is optional if the relationship's destination object can be null (nil in Objective-C). For example, a Member entity's creditCard relationship is optional if a Member object isn't required to have a CreditCard object.

Note: Occasionally, a mandatory to-one relationship doesn't resolve to a destination object. For example, suppose your application's Movie database contains legacy data for which relational integrity constraints weren't strictly enforced. As a result, some Movies don't have corresponding Studios even though the Movies-to-Studios relationship is mandatory. The techniques for handling these errant to-one relationships are the same as those for handling optional to-one relationships.

You can model an optional to-one relationship many different ways, depending on how you represent the relationship in the database-as a foreign key to primary key join or as a primary key to primary key join.

Note: For to-one relationships, Enterprise Objects Framework doesn't support primary key to foreign key joins. The destination join attribute in a to-one relationship must be the destination entity's primary key.

Using a foreign key to primary key join, you include the destination row's primary key in the source row. For example, in the relationship shown in Figure 24, the creditCard relationship's source table (MEMBER) has a foreign key (CARD_NUMBER) to the destination table (CREDIT_CARD). Using this approach, you can model an optional
to-one relationship as a true to-one relationship just as you would model a mandatory to-one relationship.

Figure 24. Storing a Foreign Key in the Source Table

A foreign key to primary key join is the best way to model a to-one relationship for use with Enterprise Objects Framework. If you have control over the design of the database schema, use foreign key to primary key joins for to-one relationships whenever possible.

Alternatively, you can use a primary key to primary key join that includes the source's primary key in the destination table. For example, in the relationship shown in Figure 25, the talentPhoto relationship's destination table (TALENT_PHOTO) has a foreign key (TALENT_ID) to the source table (TALENT). For reasons described below, this arrangement requires special handling.

Figure 25. Storing a Foreign Key in the Destination Table

When Enterprise Objects Framework fetches an enterprise object, it attempts to assign destination objects to any of the object's to-one relationships. If a destination object hasn't already been fetched, Enterprise Objects Framework creates a fault to stand in for the destination object until it is actually needed. (For more information on faulting, see the chapter "Behind the Scenes".)

The exception to this is when the relationship is based on a foreign key to primary key join and the relationship's source object doesn't have a corresponding destination. Instead of creating a fault, Enterprise Objects Framework assigns null (or nil in Objective-C) to the source object's relationship property. For example, if a Member doesn't have a corresponding CreditCard, the corresponding MEMBER record's CARD_NUMBER value is NULL. When Enterprise Objects Framework sees the null-valued CARD_NUMBER attribute, it sets the Member's creditCard property to null.

On the other hand, Enterprise Objects Framework can't detect that a primary key to primary key relationship doesn't have a destination. For example, Enterprise Objects Framework can't tell that a TALENT record doesn't have a corresponding TALENT_PHOTO record until it tries to fetch a TALENT_PHOTO with the same TALENT_ID value and fails. Consequently, in a primary key to primary key relationship, Enterprise Objects Framework always assigns a fault if a corresponding destination object hasn't been fetched. If no such destination exists, Enterprise Objects Framework throws an exception when it tries to resolve the fault.

An optional primary key to primary key relationship (such as talentPhoto) can be handled in a number of ways:

The following sections describe each approach.

Use a Mandatory To-One Relationship

This approach is used for the Movies database to model Talent's talentPhoto relationship. Although a Talent object doesn't have to have a photo, it does have to have a corresponding TalentPhoto object. As shown in Figure 26, a Talent object that doesn't have a photo has a TalentPhoto object whose photo attribute is null (nil in Objective-C).

Figure 26. A Destination Object with null-Valued Attributes

This approach doesn't require any code. In the Advanced Relationship Inspector for the Talent entity's talentPhoto relationship, you simply set the relationship to propagate primary key. Propagate primary key tells Enterprise Objects Framework to propagate the primary key of the source entity into newly inserted objects in the destination entity (instead of generating a primary key value for the destination). With this configuration, Enterprise Objects Framework inserts a new Talent object, it inserts a corresponding TalentPhoto object if the Talent object doesn't already have one assigned to it.

Use a To-Many Relationship

To-many relationships use a different faulting mechanism than to-ones. A fault for a to-many relationship replaces itself with an NSArray of corresponding destination objects, and it doesn't throw an exception if it doesn't find any. If you use a to-many relationship to model an optional to-one and no destination object exists, the array is simply empty. If the relationship does have a destination object, it's the first and only object in the array.

You can design your enterprise object's API to hide the to-many implementation. For example, suppose that Talent's talentPhoto relationship was modeled as a to-many. To design a Talent enterprise object that acts as if its talentPhoto relationship is an optional to-one, you could name the to-many relationship (and the corresponding instance variable) "_talentPhotoArray" and implement the following two accessor methods:

In Java:

public void setTalentPhoto(TalentPhoto talentPhoto)
{
willChange();
_talentPhotoArray.removeAllObjects();
if (talentPhoto != null)
_talentPhotoArray.addObject(talentPhoto);
}

public TalentPhoto talentPhoto()
{
willRead();
if (_talentPhotoArray.count() > 0)
return _talentPhotoArray.objectAtIndex(0);
return null;
}
In Objective-C:

- (void)setTalentPhoto:(TalentPhoto *)talentPhoto
{
[self willChange];
[_talentPhotoArray removeAllObjects];
if (_talentPhotoArray)
[_talentPhotoArray addObject:talentPhoto];
}

- (id)talentPhoto
{
if ([_talentPhotoArray count])
return [_talentPhotoArray objectAtIndex:0];
return nil;
}

Handle the Exception

You can use a to-one relationship if you handle any exceptions that are thrown when a fault doesn't resolve to a destination object. For example, in the Talent enterprise object, you would implement the talentPhoto relationship "get" method as follows:

In Java:

public TalentPhoto talentPhoto()
{
try {
// If the receiver is a fault, sending it a willRead
// message attempts to resolve it. If the
// corresponding row doesn't exist in the database,
// an exception is thrown.
talentPhoto.willRead();
} catch (NSException e) {
talentPhoto = null;
}

return talentPhoto;
}
In Objective-C:

- (TalentPhoto *)talentPhoto
{
NS_DURING
// If the receiver is a fault, sending it a self
// message attempts to resolve it. If the
// corresponding row doesn't exist in the database,
// an exception is raised.
[talentPhoto self];
NS_HANDLER
[talentPhoto autorelease];
talentPhoto = nil;
NS_ENDHANDLER

return talentPhoto;
}
Sending willRead (or self in Objective-C) to a fault triggers it to fetch its corresponding enterprise object. If a Talent instance doesn't have a corresponding TalentPhoto, sending willRead to the talentPhoto property throws an exception. In the talentPhoto method above, the exception handler simply sets the property to null (first autoreleasing the talentPhoto fault in Objective-C).

Implement databaseContextFailedToFetchObject

With the EODatabaseContext delegate method databaseContextFailedToFetchObject (databaseContext:failedToFetchObject:globalID: in Objective-C), you can prevent an exception from being thrown when a fault doesn't resolve to a destination object. This method is invoked when a fault for a to-one relationship can't find its corresponding object in the database. By returning false (NO in Objective-C), you can prevent the EODatabaseContext from raising an exception.

For example, to handle mandatory to-one relationships with errant data (source rows that don't have corresponding destinations), you could implement the delegate method to insert the empty object, thereby supplying the missing destination object:

In Java:

public boolean databaseContextFailedToFetchObject(
EODatabaseContext context,
Object object,
EOGlobalID gid)
{
// Perform a check to determine whether to intervene
if (...) {
// Set values in your object (if necesssary).
object.editingContext().insertObject(object);
return false;
}
return true;
}
In Objective-C:

- (BOOL)databaseContext:(EODatabaseContext *)context
failedToFetchObject:(id)object
globalID:(EOGlobalID *)gid
{
// Perform a check to determine whether to intervene
if (...) {
// Set values in your object (if necesssary).
[[object editingContext] insertObject:object];
return NO;
}
return YES;
}
In the above implementations, the delegate method first checks to see if it should intervene. For example, the method might check to see if object is an instance of the TalentPhoto class. If the delegate determines that object represents a destination object that's missing from the database, the delegate queues object for insertion into the database by inserting it into its editing context. It returns false (NO in Objective-C) indicating that the delegate has handled the error and that the EODatabaseContext shouldn't throw an exception.

Modeling Many-To-Many Relationships

To model a many-to-many relationship between objects is simple: each object manages a collection of the other kind. For example, consider the many-to-many relationship between employees and projects. To model this relationship in objects, an Employee has an NSArray, projects, of all the projects he or she works on; and a Project class has an NSArray, employees, of all its members.

To model a many-to-many relationship in a database, you have to create an intermediate table (also known as a correlation or join table). For example, the database for employees and projects might have EMPLOYEE, PROJECT, and EMP_PROJ tables, where EMP_PROJ is the correlation table. The appendix "Entity-Relationship Modeling" provides more information on the tables behind a many-to-many relationship.

Given the relational database representation of a many-to-many, how do you get the object model you want? You don't want to see evidence of the correlation table in your object model, and you don't want to write code to maintain database correlation rows. With Enterprise Objects Framework, you don't have to. Simply use flattened relationships as described in the chapter "Using EOModeler" to hide your correlation tables.

A model with the following features has the effect of hiding the EMP_PROJ correlation table from its object model altogether:

Consequently, EmpProj enterprise objects are never created, Employees have an array of related Projects, and Projects have an array of related Employees. Furthermore, Enterprise Objects Framework automatically manages rows in the EMP_PROJ correlation table.

However, what do you do when a correlation table contains extra attributes that are interesting? For example, the MOVIE_ROLE table in the sample Movies database is a correlation table between movies and the actors who star in them. In addition to foreign keys for MOVIE and TALENT, the MOVIE_ROLE table also contains the name of the role the actor plays in the film. In this case, MovieRole enterprise objects actually have a place in the object model even though they're fetched from a correlation table.

If you want to programmatically access both a Movie's roles and its actors directly from the Movie object, you should do the following:

  1. Create a movieRole relationship from Movie to MovieRole and set it to be a class property.

  2. Create a talent relationship from MovieRole to Talent.

  3. Define an actors method in the Movie class that returns the Talent objects by getting them from the corresponding MovieRoles.
Because MovieRole corresponds to a data-bearing correlation table, you shouldn't create a flattened relationship from Movie to Talent. If your application fetches correlation records as enterprise objects, consistency problems can arise if it also manages a flattened relationship. For example, suppose you did flatten the talent relationship into the Movie entity. Movie objects would then have an array of MovieRole objects and an array of Talent objects. If your application adds a new MovieRole to a Movie's roles array, the corresponding actors array doesn't reflect the addition until the new MovieRole is saved to the database and the Movie is refetched.

Instead, if you create an actors method that traverses the object graph through the Movie's MovieRole objects, you avoid any consistency problems.

For display purposes, you don't even need an accessor method to bypass a correlation object. Instead, you can use key paths. For example, you can use the key path roles.talent to access a Movie's Talent objects in a master-detail configuration between a Movie EODisplayGroup and a Talent EODisplayGroup.

Table of Contents Next Section