[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