Relationship Manager - Design Pattern

A central mediating class which records all the one-to-one, one-to-many and many-to-many relationships between a group of selected classes.

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 one
1 --> 1
one to one
1 <--> 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 many
1 --> *
one to many
1 <--> *
#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 have addOrder(order).
  • Instead of .getX() you might have getCustomer().
  • 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 clearY() implementation needs to get a reference to y in order to call remove_rel(x, y, ...) which is done by calling getY() on itself.

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:

  1. When you create a bi-directional enforcement rule (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.

  2. When you create a directional enforcement rule (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 magic of rm, call rm.find_source() to derive this information.

  3. 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).

    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.

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()     |
|_____________|      |______________|
        
  • X has the required plural API
  • Y has the reciprocal singular API

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.

svg your image

or just

svg your image

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


Last modified March 10, 2022: added tag cloud (9c1d0fd)