Relationship Manager - Design Pattern
Abstract
Basically describes a lightweight, in-memory Object Database.
Classes that use a Relationship Manager to implement their relationship properties and methods have a consistent metaphor and trivial implementation code (one line calls). In contrast - traditional “pointer” and “arraylist” techniques of implementing relationships are fully flexible but often require a reasonable amount of non-trivial code which can be tricky to get working correctly and are almost always a pain to maintain due to the detailed coding and coupling between classes involved, especially when back-pointers are involved.
Using a Relationship Manager
object to manage the relationships can mitigate these problems and make managing relationships straightforward. It also opens up the possibility of powerful querying of relationships, a very simple version of something like LINQ.
In a sense, an Object Database is an elaborate implementation of the Relationship Manager pattern. However the intent of the Relationship Manager pattern is lighter weight, to replace the wirings between objects rather than acting as a huge central database on disk - though persistence is built into Relationship Manager too.
The Official Pattern
Note this pattern was written and presented in 2001 - quite a while ago! In 2020 the API of the Python Relationship Manager implementation was revised and improved - see full API documentation.
Download as pdf.
Quick Example
The examples on this page use the modern v2. Python implementation.
Relationship Manager has also been implemented in Python, C# (.net4 and .net core) and Java - see the Relationship Manager GitHub project for all implementation source code.
Installation
pip install relationship-manager
Usage
from relmgr import RelationshipManager
rm = RelationshipManager()
rm.enforce("xtoy", "onetoone", "directional")
x = object()
y = object()
rm.add_rel(x, y, "xtoy")
assert rm.find_target(x, "xtoy") == y
Benefits
- Modelling relationships is easy and consistent
- Back pointer are free
- Using a query language (think LINQ) is possible over your objects
- Optional constraints ensure wrong wirings are not made
Traditional object oriented programmers wire up their objects manually using pointers and arrays, whereas database programmers save their objects in a database and let the database model the relationships.
Programmers often use ORM mappers to get the best of both worlds - objects which also have a representation in a database. Relationship Manager is another solution to being able to more easily model and query your objects - without needing a database.
Queries
You can make queries on the Relationship Manager instance:
# query API
def find_targets(self, source, rel_id) -> List:
def find_target(self, source, rel_id) -> object:
def find_sources(self, target, rel_id) -> List: # Back pointer query
def find_source(self, target, rel_id) -> object: # Back pointer query
def find_rels(self, source, target) -> List:
def is_rel(self, source, target, rel_id=1) -> bool:
Constrained Relationships
You can enforce relationships. For example enforce()
works like this:
rm.enforce("xtoy", "onetoone", "directional")
The relationship is registered as being one to one and directional, so that e.g. when you add a second relationship between the same two objects the first relationship is automatically removed - ensuring the relationship is always one to one. Alternatively, the implementation could raise an exception (go into the source and change it if this is what you need).
Modelling relationships
What methods do I put where when modelling relationships?
What are all the possibilities of relationships between two classes?
When looking at all the possibilities of relationships between two classes, you get
- one to one
- one to many
- many to one
- many to many
Then you have the variations generated by whether the relationships are either
- directional
- bi-directional
Finally, you have variations of whether you put pointer methods (e.g. set, get, add) on one class or the other, or both.
For example, assuming you have a two classes one on the lhs and one on the rhs - you could omit methods on e.g. the rhs. class, or you could go to the other extreme and provide a full range of methods on the rhs. class.
I recommend that you use the table of relationship scenarios table to figuring out what methods to put where for each type of classic relationship you want to model. For example, to implement a one to many relationship between two classes X and Y, you would use template 4 or 5 (use the latter if you want bidirectionality)
Note that some combinatorial possibilities do not make sense and are left out of the table below.
S
means singular API - this makes sense for one to one relationships, or the many side (ironically) of one to many relationships. It consists of methods like get, set, clear.P
means plural API- this makes sense where you are dealing with collections, a many concept. It consists of methods like add, remove, getall.-
means no methods relating to the relationship have been implemented on that class.
Blank cells mean “not applicable”.
Scenario # see below | directional | bi-directional | comments |
---|---|---|---|
one to one1 --> 1 |
one to one1 <--> 1 |
||
#1. | S - | ||
#2. | - S | ||
#3. | S S | using ‘bidirectional’ relationship, which creates two relationship entries | |
#3A. | S S | alternative implementation using a single ‘direction’ relationship - the bidirectionality is figured out using the magic of rm.find_source() |
|
one to many1 --> * |
one to many1 <--> * |
||
#4. | P - | ||
#5. | P S | using ‘bidirectional’ relationship, which creates two relationship entries | |
#5A. | P S | alternative implementation using a single ‘direction’ relationship - the bidirectionality is figured out using the magic of rm.find_source() |
|
many to one* --> 1 |
many to one* <--> 1 |
||
#6. | - P | ||
#7. | S P | ||
many to many* --> * |
many to many* <--> * |
||
#8. | P - | ||
#9. | - P | ||
#10. | P P |
An attempt at mapping the theoretical relationship possibilities
The above table shows all the possible relationship scenarios between two classes. It indicates various possibilities as to the methods you can add to either class. For example a one to many relationship where the “many” side has no need of any methods to see who is pointing at it, would use template 4.
Table of Relationship Scenarios
How to implement relationships using sets of Relationship Manager methods
Here is a list of classic “relationship scenarios” (e.g. one to one, one to many etc.) and how to implement them using the Relationship Manager API.
The right hand side of the below table shows python code using calls to RM (relationship manager) using the shorthand notation for the function names. For long hand names just substitute in the appropriate name e.g. instead of RM.R() you would call rm.AddRelationship().
Note: The method names below are just suggestions. Normally you would use better method names that pertain to your application domain.
- Instead of
.addY(y)
you might haveaddOrder(order)
. - Instead of
.getX()
you might havegetCustomer()
. - Instead of
getAllY()
you might have.getOrders()
etc.
Here is the table:
Look up the scenario you need to implement on the left, then use the template implementation on the right in your code.
Relationship Scenario | Example Python Implementation | |||
---|---|---|---|---|
Implementing one to one relationships between class X and Y |
||||
#1. |
1 → 1, directional, all methods on X Singular API No API ______________ ______________ | X | | Y | |______________| |______________| | | | | |void setY(y) |1 1| | |Y getY() |⎯⎯⎯⎯⎯⎯⎯⎯⎯→| | |void clearY()| | | |______________| |______________| Note: The |
class X: def __init__(self): rm.enforce("xtoy", "onetoone", "directional") def setY(self, y): rm.add_rel(self, y, "xtoy") def getY(self): rm.find_target(source=self, rel_id="xtoy") def clearY(self): rm.remove_rel(self, self.getY(), "xtoy") class Y: pass |
||
#2. |
1 → 1, directional, all methods on Y No API Singular API ______________ ______________ | X | | Y | |______________| |______________| | | | | | |1 1| setX(x) | | |⎯⎯⎯⎯⎯⎯⎯⎯→ | getX() | | | | clearX() | |______________| |______________| |
class X: pass class Y: def __init__(self): rm.enforce("xtoy", "onetoone", "directional") def setX(self, x): rm.add_rel(x, self, "xtoy") def getX(self): rm.find_source(target=self, rel_id="xtoy") def clearX(self): rm.remove_rel(self.getX(), self, "xtoy") |
||
#3. |
1 ←→ 1, bi-directional, methods on both X and Y Singular API Singular API ______________ ______________ | X | | Y | |______________| |______________| | | | | |void setY(y) |1 1| setX(x) | |Y getY() | ←⎯⎯⎯⎯→ | getX() | |void clearY()| | clearX() | |______________| |______________| |
class X: def __init__(self): rm.enforce("xy", "onetoone", "bidirectional") def setY(self, y): rm.add_rel(self, y, "xy") def getY(self): rm.find_target(self, "xy") def clearY(self): rm.remove_rel(self, self.getY(), "xy") class Y: def __init__(self): rm.enforce("xy", "onetoone", "bidirectional") def setX(self, x): rm.add_rel(self, x, "xy") def getX(self): rm.find_target(self, "xy") def clearX(self): rm.remove_rel(self, self.getX(), "xy") |
||
#3A. |
1 ←→ 1, bi-directional, methods on both X and Y Alternative implementation of scenario 3, using "directional" and a backpointer method diagram as above |
class X: def __init__(self): rm.enforce("xy", "onetoone", "directional") # different to 3. # uses 'directional' not 'bidirectional' def setY(self, y): rm.add_rel(self, y, "xy") # same as 3. def getY(self): rm.find_target(self, "xy") # same as 3. def clearY(self): rm.remove_rel(self, self.getY(), "xy") # same as 3. class Y: def __init__(self): rm.enforce("xy", "onetoone", "directional") # different to 3. # uses 'directional' not 'bidirectional' # redundant call since already called in X's constructor def setX(self, x): # different to 3. rm.add_rel(self, x, "xy") # source and target params swapped def getX(self): # different to 3. rm.find_source(self, "xy") # uses 'find_source' not 'find_target' def clearX(self): # different to 3. rm.remove_rel(self, self.getX(), "xy") # source and target params swapped |
||
Notes on Scenario 3 and 3A:
|
||||
Implementing one to many relationships between class X and Y |
||||
#4. |
1 → *, directional, all methods on X Plural API No API _____________ ______________ | X | | Y | |_____________| |______________| | | | | |addY(y) |1 *| | |getAllY() | ⎯⎯⎯⎯⎯⎯→ | | |removeY(y) | | | |_____________| |______________| |
class X: def __init__(self): rm.enforce def addY(self, y): rm.add_rel(self, y, "xtoy") def getAllY(self): rm.find_targets(self, "xtoy") def removeY(self, y): rm.remove_rel(self, y, "xtoy") class Y: # no methods on rhs pass |
||
#5. |
1 ←→ *, bi-directional, methods on both X and Y Plural API Singular API _____________ ______________ | X | | Y | |_____________| |______________| | | | | |addY(y) |1 *| setX(x) | |getAllY() | ←⎯⎯⎯⎯→ | getX() | |removeY(y) | | clearX() | |_____________| |______________|
Since there are two API's, one on each class, this makes it a bidirectional relationship. However - there still remains a sense of directionality because the one to many is directional i.e. the the lhs. 'one' side is the X and the rhs. 'many' side is the Y, not the other way around. |
class X: def __init__(self): rm.enforce("xtoy", "onetomany", "bidirectional") def addY(self, y): rm.add_rel(self, y, "xtoy") def getAllY(self): rm.find_targets(self, "xtoy") def removeY(self, y): rm.remove_rel(self, y, "xtoy") class Y: # though bi, there is still a direction! def setX(self, x): rm.add_rel(x, self, "xtoy") def getX(self): rm.find_target(self, "xtoy") def clearX(self): rm.remove_rel(self, self.getX(), "xtoy") |
||
#5A. |
1 ←→ *, bi-directional, methods on both X and Y Alternative implementation of scenario 5, using "directional" and a backpointer method diagram as above |
class X: def __init__(self): rm.enforce("xtoy", "onetomany", "directional") # different to 5 # uses 'directional' not 'bidirectional' def addY(self, y): rm.add_rel(self, y, "xtoy") # same as 5. def getAllY(self): rm.find_targets(self, "xtoy") # same as 5. def removeY(self, y): rm.remove_rel(self, y, "xtoy") # same as 5. class Y: def setX(self, x): rm.add_rel(x, self, "xtoy") # same as 5. def getX(self): rm.find_source(self, "xtoy") # different to 5 # uses 'find_source' not 'find_target' def clearX(self): rm.remove_rel(self.getX(), self, "xtoy") # different to 5 # source and target params swapped |
||
Implementing many to one relationships between class X and Y |
||||
#6. |
* → 1, directional, all methods on Y No API Plural API ______________ ______________ | X | | Y | |______________| |______________| | | | | | |* 1|addX(x) | | | ⎯⎯⎯⎯⎯⎯→ |getAllX() | | | |removeX(x) | |______________| |______________| |
DRAFT API (not tested) class X: pass class Y: def addX(x) -> None: rm.add_rel(x, this, "xtoy") def getAllX() -> List: return rm.find_sources(this, "xtoy") def removeX(x) -> None: rm.remove_rel(x, this, "xtoy") |
||
#7. |
* ←→ 1, bi-directional, methods on both X and Y Singular API Plural API ______________ ______________ | X | | Y | |______________| |______________| | | | | |void setY(y) |* 1|addX(x) | |Y getY() | ←⎯⎯⎯⎯→ |getAllX() | |void clearY()| |removeX(x) | |______________| |______________| |
DRAFT API (not tested) class X: def setY(y) -> None: rm.add_rel(this, y, "xtoy") def getY() -> Y: rm.find_target(this, "xtoy") def clearY() -> None: rm.remove_rel(this, getY(), "xtoy") class Y: def addX(x) -> None: rm.add_rel(x, this, "xtoy") def getAllX() -> List: rm.find_sources(this, "xtoy") def removeX(x) -> None: rm.remove_rel(x, this, "xtoy") |
||
Implementing many to many relationships between class X and Y |
||||
#8. |
* → *, directional, all methods on X Plural API No API _____________ ______________ | X | | Y | |_____________| |______________| | | | | |addY(y) |* *| | |getAllY() | ⎯⎯⎯⎯⎯⎯→ | | |removeY(y) | | | |_____________| |______________| |
DRAFT API (TODO, not tested) |
||
#9. |
* → *, directional, all methods on Y No API Plural API ______________ ______________ | X | | Y | |______________| |______________| | | | | | |* *|addX(x) | | | ⎯⎯⎯⎯⎯⎯→ |getAllX() | | | |removeX(x) | |______________| |______________| |
DRAFT API (TODO, not tested) |
||
#10. |
* ←→ *, bi-directional, methods on both X and Y Plural API Plural API ______________ ______________ | X | | Y | |______________| |______________| | | | | | addY(y) |* *| addX(x) | | getAllY() | ←⎯⎯⎯⎯→ | getAllX() | | removeY(y) | | removeX(x) | |______________| |______________| |
DRAFT API (TODO, not tested) |
These scenarios are all unit tested in tests/python/test_enforcing.py
in the GitHub project.
Back pointers
One of the benefits of the relationship manager pattern is that you don’t have to explicitly wire up and maintain back-pointers. Once you add a pointer relationship, you get the back pointer relationship available, for free. And once you delete the pointer relationship, the back-pointer relationship goes away automatically too.
The following code is a good example of how the use of RM saves you from having to explicitly maintain backpointers. P.S. To run the code you also need the support files found here. View the code below (requires the flash plugin) - showing an implementation of a Composite Pattern, with back pointer - or simply read the pdf directly.
Backpointers are pointers on the “target end” of a relationship, so that the target object knows who is pointing at it. For example when a Customer places an Order, it might be convenient for any particular order instance to know which customer ordered it. I think you can choose to conceive of the backpointer in a few different ways:
- as an extra, separate relationship or
- as part of the one bidirectional relationship or
- merely a convenience method in the implementation in the r.h.s. class
The easiest way of implementing this backpointer without using relationship manager is to follow the Martin Fowler refactoring technique - see Martin Fowler ‘Refactorings’ p. 197 “Change Unidirectional Association to Bidirectional” - this will ensure you get the wiring correct. In this refactoring, you decide which class is the master and which is the slave etc. See the before and after python pdf below for an example of the correct wiring.
The way of implementing a backpointer using relationship manager is simply to call the rm.find_source(target=self)
method. Since a rm holds all relationships, it can answer lots of questions for free - just like SQL queries to a database.
Bi-directional relationships
A bi-directional relationship between X and Y means both sides have pointers to each other.
or just
Within this seemingly obvious idea are a myriad of nuances:
We must distinguish between a relationship that in its domain meaning, goes both ways, and a relationship which goes one way only. And furthermore, implementationally, you can have RM methods on one class only, on the other class only, or on both classes. The meaning of the relationship and the implementation (methods to create and look up those relationships) are two different things!
- As the diagram above shows, one bi-bidirectional relationship is arguably shorthand for two directional relationships.
In fact in the Python rm implementation, when you create a bi-directional enforcement rule (e.g. Scenario 3) with a call to rm.enforce(“xy”, “onetoone”, “bidirectional”)
you are actually causing rm to create two relationship entries in the rm. This means you can reliably use a rm.find_target(source=self)
call from either side, knowing there is a relationship in both directions.
- The methods you implement on your classes to create and look up relationships can influence your perception of what is pointing to what.
When you put an API (relationship manager methods) on both classes this might seem to imply that you are implementing bi-directionality - however this does not mean that the “semantic relationship” points in both directions. The meaning of the relationship is often in one direction only, and the existence of methods on both classes merely gives you a convenient way of querying the directional relationship that exists.
A rm, like a database, allows you to ‘cheat’ and find out who is pointing to a class even though that class has no actual pointers implementing ‘am pointed to by’. This is accomplished by using rm.find_source(target=self)
. But just because a rm let’s you find out this knowledge doesn’t mean there is a official modelling of this back-relationship in your domain.
- Back-pointer relationships are not the same thing as official, semantic relationships.
However you may still want to declare a bidirectional relationship for its semantic value in your particular business logic domain, or for domain modelling accuracy - or even just for your own implementation preferences.
- A bi-directional relationship (pair) can be implemented more efficiently by a single directional relationship together with the magic rm back-pointer lookup call
rm.find_source(target=self)
.
When you create a directional enforcement rule (e.g. Scenario 3A) with a call to rm.enforce(“xy”, “onetoone”, “directional”)
or leave out this call altogether, you are causing rm to create only the relationships that you ask for. Thus classes on the ’target’ side of a relationship cannot call rm.find_target(source=self)
to find out who is pointing to them. They can however, thanks to the back-pointer lookup magic of rm, call rm.find_source(target=self)
to derive this information.
This means bidirectional relationships never actually need to be used or declared, 😲, since an implicit back-pointer (i.e. a back reference) is always deducible using rm.find_source()
, when using a Relationship Manager! In fact a bidirectional relationship creates extra entries in the rm datastructure, and slightly more overhead in performance (maintaining both relationships e.g. in the case of creation and removal).
- Name your relationships with direction in mind
- If you choose to implement relationship related methods on both classes use the same relationship id on both sides.
The same relationship id should be used in both classes e.g. "xtoy"
(notice the sense of directionality is built into the name of the relationship!). Even though there is an API on both classes allowing each class to find the other class, does not turn the relationship semantics to be bi-directional from the point of view of domain modelling, but only in a convenient implementation sense.
Some may frown on this ability of an implementation to cheat and betray the domain model. Perhaps a flag could be set in the rm to disallow use of the back-pointer lookup magic rm.find_source(target=self)
of rm,.
In the following implementation of a one to many relationship between class X and class Y, notice the same relationship id "xtoy"
must be used in both classes.
class X:
def __init__(self): rm.enforce("xtoy", "onetoone", "directional")
def setY(self, y): rm.add_rel(self, y, "xtoy")
def getY(self): return rm.find_target(self, "xtoy")
def clearY(self): rm.remove_rel(self, self.getY(), "xtoy")
class Y:
def __init__(self): rm.enforce("xtoy", "onetoone", "directional") # probably redundant
def setX(self, x): rm.add_rel(x, self, "xtoy")
def getX(self): return rm.find_source(self, "xtoy")
def clearX(self): rm.remove_rel(self.getX(), self, "xtoy")
Note that both classes calling rm.enforce
is possibly redundant, since its telling the rm the same information - depending on the order of initialisation of your classes.
Examples
Python Example - Observer pattern
Here is an example of hiding the use of Relationship Manager,
found in the examples folder as relmgr/examples/observer.py
- the
classic Subject/Observer pattern:
from relmgr import RelationshipManager
rm = RelationshipManager()
class Observer:
@property
def subject(self):
return rm.find_target(self)
@subject.setter
def subject(self, _subject):
rm.add_rel(self, _subject)
def notify(self, subject, notification_type):
pass # implementations override this and do something
class Subject:
def notify_all(self, notification_type: str):
observers = rm.find_sources(self) # all things pointing at me
for o in observers:
o.Notify(self, notification_type)
def add_observer(self, observer):
rm.add_rel(observer, self)
def remove_observer(self, observer):
rm.remove_rel(source=observer, target=self)
When using the Subject and Observer, you use their methods without realising their functionality has been implemented using rm. See tests/python/examples/test_observer.py
in the GitHub project for the unit tests for this code.
C# Example - modelling Person –>* Order
Say you want to model a Person class which has one or more Orders. The Order class needs to have a backpointer - back to the Person owning that order.
Instead of hand coding and reinventing techniques for doing all the AddOrder() methods and GetOrders() methods etc. using ArrayLists and whatever, we can do it using the relationship manager object instead, which turns out to be simpler and faster and less error prone.
The RM (relationship manager) is implemented in this particular example as a static member of the base BO (business object) class. Thus in this situation all business objects will be using the same relationship manager.
Note that the use of Relationship Manager is hidden, and is a mere implementation detail.
Here is the c# code to implement the above UML. This code uses the v1 API documented in the Relationship Manager GitHub project:
using System;
using System.Collections;
using System.Collections.Generic;
using RelationshipManager.Interfaces;
using RelationshipManager.Turbo;
namespace Example_Person_Order_Console_App
{
class Program
{
static void Main(string[] args)
{
var jane = new Person("Jane");
var order1 = new Order("Boots");
var order2 = new Order("Clothes");
jane.AddOrder(order1);
jane.AddOrder(order2);
// test forward pointer wiring
Console.WriteLine(jane + " has " + jane.GetOrders().Count + " orders");
// test the backpointer wiring
foreach (var order in jane.GetOrders())
{
Console.WriteLine("The person who ordered " + order + " is " + order.GetPerson());
}
Console.WriteLine("Done!");
}
///
/// BO is the base Business Object class which holds a single static reference
/// to a relationship manager. This one relationship manager is
/// used for managing all the relationships between Business Objects
/// like Person and Order.
///
public class BO // Base business object
{
static protected RelationshipMgrTurbo rm = new RelationshipMgrTurbo();
}
///
/// Person class points to one or more orders.
/// Implemented using a relationship manager rather
/// than via pointers and arraylists etc.
///
public class Person : BO
{
public string name;
static Person()
{
rm.EnforceRelationship("p->o", Cardinality.OneToMany, Directionality.DirectionalWithBackPointer);
}
public Person(string name)
{
this.name = name;
}
public override string ToString()
{
return "Person: " + this.name;
}
public void AddOrder(Order o)
{
rm.AddRelationship(this, o, "p->o");
}
public void RemoveOrder(Order o)
{
rm.RemoveRelationship(this, o, "p->o");
}
public List<Order> GetOrders()
{
IList list = rm.FindObjectsPointedToByMe(this, "p->o");
// cast from list of 'object' to list of 'Person'
var result = new List<Order>();
foreach (var order in list)
result.Add((Order)order);
// attempts at other simpler ways to cast a whole list
//result = list as List<Order>; // crash
//result = new List<Order>(list); // syntax error?
return result;
}
}
///
/// Order class points back to the person holding the order.
/// Implemented using a relationship manager rather
/// than via pointers and arraylists etc.
///
public class Order : BO
{
public string description;
public Order(string description)
{
this.description = description;
}
public override string ToString()
{
return "Order Description: " + this.description;
}
public void SetPerson(Person p)
{
// though mapping is bidirectional, there is still a primary relationship direction!
rm.AddRelationship(p, this, "p->o");
}
public Person GetPerson()
{
// cast from 'object' to 'Person'
return (Person)rm.FindObjectPointingToMe(this, "p->o");
}
public void ClearPerson()
{
rm.RemoveRelationship(this, this.GetPerson(), "p->o");
}
}
}
}
Output:
Person: Jane has 2 orders
The person who ordered Order Description: Clothes is Person: Jane
The person who ordered Order Description: Boots is Person: Jane
Done!
C# Future Directions
A generics version of relationship manager would be cool - that way no casting would be required. Presently all calls to relationship manager return objects or lists of objects - which you have to cast to the specific type you actually have stored. You can see this casting in the above example.
Resources
-
Python Implementation README and GitHub project.
-
Full Python Relationship Manager API documentation.
-
Official Relationship Manager Pattern page incl. academic paper by Andy Bulka (this page).
Feedback
Was this page helpful?
Glad to hear it! Please tell us how we can improve.
Sorry to hear that. Please tell us how we can improve.