[Zope-dev] Re: RFC: RelationAware class for relations betweenobjects

Evan Simpson evan@4-am.com
Wed, 30 Apr 2003 16:13:20 -0500


Shane Hathaway wrote:
> Steve Alexander wrote:
>> What about making relationships among pre-existing objects that were 
>> not designed with relationships in mind?
> 
> As it stands, Jobs have no relationships with Users, but now you want to 
> relate Jobs to Users.

I'd say that this could be a fairly common use case.  Here's my take on 
what Relationships should be, using ERM jargon (see:
http://www.cs.jcu.edu.au/ftp/web/teaching/Subjects/cp1500/1998/Lecture_Notes/er_model/rships.html)

A relation is a mapping from roles to entities.  These entities don't 
need to know anything about the relation in order to be a part of it. 
The relation may also have descriptive data attached to it.

A Relationship is an object that contains a set of relations of the same 
type, meaning that each relation is based on the same set of roles.
It provides methods for searching and modifying this set. The 
Relationship also imposes constraints on and among the relations that it 
contains.

A role does not impose class or other restrictions on the entities that 
may fill it, although the Relationship may.

It is possible to derive a "view" of a Relationship, such that it 
contains the subset of relations in which one or more roles contain 
specified entities.  Any relation added to the view must map these roles 
to these entities.

In concrete terms:

 >>> r = Relationship(roles=[['group'], ['member']])
 >>> r.add(group='men', member='fred')
<relation group='men' member='fred'>
 >>> r.add('men', 'barney') # implicit use of role order?
<relation group='men' member='barney'>
 >>> fred_groups = r.view(member='fred')
 >>> fred_groups.add(group='water buffalo')
 >>> for rel in fred_groups.get(): print rel.group
'men'
'water buffalo'
 >>> print fred_groups.get(group='men')
<relation group='men' member='fred'>
 >>> len(r), len(fred_groups)
3, 2
 >>> r.remove(member='fred')
 >>> len(r), len(fred_groups)
1, 0

The various common sorts of cardinality (one-to-one, etc) constraints 
can be expressed by list nesting.  The example above creates a 
many-to-many relationship.

# one-to-one
Relationship(['person', 'ssn'])
# one-to-many
Relationship(['boss', ['employee']])
# one-to-many-to-many
Relationship(['book', ['edition'], ['editor']])
# one-to-one-to-many
Relationship(['mother', 'father', ['child']])
# one-to-(one-to-many)
Relationship(['superclass', ['class', ['instance']]])

Notice the subtle but important difference between the last two 
examples.  A child must have exactly one (mother, father) pair, and an 
instance must have exactly one (superclass, class) pair.  Also, a class 
must have exactly one superclass (we're talking single-inheritance, 
here), but a father may have children with more than one mother and 
vice-versa.

Attaching descriptive data might look like this:

r = Relationship(['invoice', 'payment'])
link = r.add(invoice=inv1, payment=p1)
link['amount'] = 50
link = r.add(invoice=inv2, payment=p1)
link['amount'] = 75

This facility could also be used to "annotate" objects, with a 
single-role relation:

member_data = Relationship(['member'])
member_data.add(current_user)['email'] = 'guy@example.com'

It would probably be valuable to allow arbitrary constraint objects that 
are notified of attempts to add and remove relations.

Cheers,

Evan @ 4-am