Link Attributes

When modeling many real-world problems, it is common to encounter situations that require associating data with a link between two objects. This document describes how to model these situations using the persistence system.

Introduction

When creating a model for use in a particular domain, there are usually cases in which a simple association between two object types will not suffice. A Group may contain Users through a members association, and for each member that Group may want to record the date on which the User became a member of the Group. This is a case in which the link between two objects must carry more information than just that the two objects are associated. The UML terminology for this is a qualified association and it is described with the following model.

 ____________                           ____________ 
|   Group    |                         |    User    |
|------------|                         |------------|
|            | n                     n |            |
|            |------------------------>|            |
|            |           |     members |            |
|____________|           |             |____________|
                         |
                         |
                         |
                 ________|________ 
                |                 |
                |-----------------|
                |  membershipDate |
                |                 |
                |_________________|

A model like the one depicted above can be created in sql with the following 3 tables.

create table groups (
      group_id     integer primary key;
      name         varchar(300);
      ....
);

create table users (
      user_id      integer primary key;
      name         varchar(300);
      email        varchar(100);
      ....
);

create table group_member_map (
      group_id     integer references groups;
      member_id    integer references users;
      membership_date date default sysdate;
      primary key (group_id, member_id);
);
    

This association can then easily be transformed to PDL through the use of the association keyword. When defining an association between two objects using the association block, it is possible to add extra properties that can store information associated with a particular link between two objects.

object type Group {
    BigInteger[1..1] id = groups.group_id;
    String[1..1]     name = groups.name;
    // ...
}

object type User {
    BigInteger[1..1] id = users.user_id;
    String[1..1]     name = users.name;
    String[1..1]     email = users.email;
    // ...
}

association {
    Group[0..n] groups = 
                join users.user_id to group_member_map.member_id,
                join group_member_map.group_id to groups.group_id;
    User[0..n]  members = 
                join groups.group_id to group_member_map.group_id,
                join group_member_map.member_id to users.user_id;

    Date[1..1]  membershipDate = group_member_map.membership_date;
}
    

NoteNote
 

Note that in this case, all events are auto-generated and will only work if the link attribute (in this case, membershipDate) is defined within the mapping table. If the DDL generator is used, the above association definition will create the correct mapping table.

NoteNote
 

It is also possible to have a link attribute that returns a full object type (for example another User) rather than a simple object type (for example a Date). Everything in this example is the same if you replace Date with User. The only extra assumption made is that the full object type (for example User) only has a single Object Key and that key is the item stored in the mapping table (for example user_id must be stored in the mapping table in the case of User).

Link Attribute Events

The above PDL defines the desired logical model along with the membershipDate link attribute and by default it generates all of the needed events. However, it is also possible to override the events. This may be desirable if, for instance, the link attribute was stored in a fourth table. Therefore, this section describes how to override the auto-generated events. However, overriding events is HIGHLY DISCOURAGED. If you find the urge to do this, you may want to reexamine what you are trying to accomplish as there may be a more efficient way to accomplish the same task.

The Retrieve Event

Below is the retrieve event for the members role. The map block contains mappings not only for the user being fetched, but also for the membershipDate link attribute. The link attribute mappings are distinguished because they are not prefixed by a role name.

retrieve members {
    do {
        select *
        from users u, user_group_map m
        where u.user_id = m.user_id
        and m.group_id = :groups.id
    } map {
        members.id = u.user_id;
        members.email = u.email;
        membershipDate = m.membership_date;
    }
}

Below is the retrieve event for the groups role. It is similar to the retrieve members event, but it fetches groups instead. The mappings for the link attributes are identical to those above.

retrieve groups {
    do {
        select *
        from groups g, user_group_map m
        where g.group_id = m.group_id
        and m.user_id = :members.id
    } map {
        groups.id = g.group_id;
        groups.name = g.name;
        membershipDate = m.membership_date;
    }
}

The Add Event

Initially, you might attempt to write the add event for this association as follows:

add members {
    do {
        insert into user_group_map
        (user_id, group_id, membership_date)
        values
        (:members.id, :groups.id, :membershipDate)
    }
}

If you then wanted to be able to add a group to a user, you would write the following event:

add groups {
    do {
        insert into user_group_map
        (user_id, group_id, membership_date)
        values
        (:members.id, :groups.id, :membershipDate)
    }
}

Since the body of both events is exactly the same, you can use the following convenient shorthand to specify one event for both roles:

add {
    do {
        insert into user_group_map
        (user_id, group_id, membership_date)
        values
        (:members.id, :groups.id, :membershipDate)
    }
}

The Remove Event

The following is the remove event using a similar shorthand notation:

remove {
    do {
        delete from user_group_map
        where user_id = :members.id
        and group_id = :groups.id
    }
}

The Update Event

Finally, in order for link attributes to be useful, you need to be able to update a link. In order to tell the persistence system how to update a link attribute, you would write the following event:

update {
    do {
        update user_group_map
        set membership_date = :membershipDate
        where user_id = :members.id
        and group_id = :groups.id
    }
}

Link Attributes API

Once the PDL has been written to describe link attributes, the java API may access the values using the following methods:

Creating and Initializing Links

The DataAssociation.add(DataObject object) method is used when creating a link between two objects. If the association was defined with the association keyword, this method returns the link DataObject. The link DataObject can then be used to set any link attributes that have been defined. This is illustrated in the following example:

// Put the current session in a variable for convenience.
Session ssn = SessionManager.getSession();

// Get a user.
DataObject user = ssn.retrieve(new OID("User", ...));
// Get a group.
DataObject group = ssn.retrieve(new OID("Group", ...));

// Get the "members" association so that we can add a member to the
// group.
DataAssociation members = (DataAssociation) group.get("members");

// Add the user to the association.
DataObject link = members.add(user);
java.util.Date now = new java.util.Date();
link.set("membershipDate", now);

// Persist the changes.
group.save();

NoteNote
 

Only group.save() is called to persist the changes. Changes to the links are automatically persisted when the parent object is saved.

Reading Link Attributes

The DataAssociationCursor.getLinkProperty(java.lang.String) method can be used to read the values of a link attribute while iterating over a cursor.

// Put the current session in a variable for convenience.
Session ssn = SessionManager.getSession();

// Get a group.
DataObject group = ssn.retrieve(new OID("Group", ...));

// Get the members association.
DataAssociation members = (DataAssociation) group.get("members");

// Iterate over the members of the group and print out the
// membership date.
DataAssociationCursor cursor = members.getDataAssociationCursor();
while (cursor.next()) {
    // Fetch the users email for the current row.
    String email = cursor.get("email");
    // Fetch the link property for the current row.
    java.util.Date membershipDate =
        (java.util.Date) cursor.getLinkProperty("membershipDate");

    System.out.println(email + ": " + membershipDate);
}

Updating Link Attributes

The DataAssociationCursor.getLink() method can be used to fetch the DataObject that represents a link. This DataObject can then be used to modify the link and persist the results.

// Put the current session in a variable for convenience.
Session ssn = SessionManager.getSession();

// Get a group.
DataObject group = ssn.retrieve(new OID("Group", ...));

// Get the members association.
DataAssociation members = (DataAssociation) group.get("members");

// Iterate over the members of the group and modify each link.
DataAssociationCursor cursor = members.getDataAssociationCursor();
while (cursor.next()) {
    // Fetch the link object for the current row.
    DataObject link = cursor.getLink();

    // Get the membership date from the link data object.
    java.util.Date oldDate = 
       (java.util.Date) link.get("membershipDate");

    // Increment the membership date by 1 millisecond.
    java.util.Date newDate = 
       new java.util.Date(oldDate.getTime() + 1);

    // Set the new date
    link.set("membershipDate", newDate);
}

// Persist all the changed links.
group.save();

NoteNote
 

Only group.save() is called to persist the changes. Changes to the links are automatically persisted when the parent object is saved.

Filtering and Ordering based on Link Attributes

To access the values of a link attribute when filtering or setting the order of a data association, simply put the special "link" prefix in front of the link attribute name.

// Put the current session in a variable for convenience.
Session ssn = SessionManager.getSession();

// Get a group.
DataObject group = ssn.retrieve(new OID("Group", ...));

// Get the members association.
DataAssociation members = (DataAssociation) group.get("members");

// Iterate over new members of the group.
DataAssociationCursor cursor = members.getDataAssociationCursor();

// Get yesterday's date.
java.util.Date yesterday =
 new java.util.Date(System.currentTimeMilis() - 1000 * 60 * 60 * 24);

// Restrict to recent members
Filter f = cursor.addFilter("link.membershipDate > :date");
f.set("date", yesterday);

// Order by membership date, descending
cursor.setOrder("link.membershipDate desc");

while (cursor.next()) {
    ...
}