Lightweight Object Database, manages relationships between classes
Project description
Relationship Manager - Lightweight Object Database
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.
Official Relationship Manager Pattern page incl. academic paper by Andy Bulka.
What is it?
In a sense, an Object Database is an elaborate implementation of the RM pattern. The intent of the RM pattern is lighter weight, to replace the wirings between objects rather than acting as a huge central 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.
Using a Relationship Manager
object to manage the relationships can mitigate these problems and make managing relationships straightforward.
Here are various implementations of the Relationship Manager Pattern in this GitHub repository:
- Python: Uses Python 3, there are no dependencies.
- Java
- C#: Visual Studio 2005 project with unit test. Very fast implementation used in at least one commercial product.
Python
Installation
pip install -i relationship-manager
You can also simply copy the single file relationship_manager.py
into your project and import the RelationshipManager
class from that single file. However pip
is the preferred way and the following examples will assume you are importing from the rm_python
package created by pip
.
Usage
from rm_python.relationship_manager import RelationshipManager
rm = RelationshipManager()
rm.EnforceRelationship("xtoy", "onetoone", "directional")
x = object()
y = object()
rm.AddRelationship(x, y, "xtoy")
assert rm.FindObjectPointedToByMe(x, "xtoy") == y
- Read the unit tests to see all functionality being exercised, incl. backpointer queries.
- See the examples below and in the
rm_python/examples/
directory of this repository. - See API below.
- See the Relationship Manager pattern referred to above for lots more documentation.
Python API
The API is:
def AddRelationship(self, From, To, RelId: Union[int,str]=1) -> None: pass
def RemoveRelationships(self, From, To, RelId=1) -> None: pass
def FindObjects(self, From=None, To=None, RelId=1) -> Union[List[object], bool]: pass
def FindObject(self, From=None, To=None, RelId=1) -> object: pass
def Clear(self) -> None: pass
def FindObjectPointedToByMe(self, fromObj, relId) -> object: pass
def FindObjectPointingToMe(self, toObj, relId) -> object: pass # Back pointer query
Relationships = property(GetRelations, SetRelations)
def EnforceRelationship(self, relId, cardinality, directionality="directional"): pass
# persistence related
objects: Namespace
def dumps(self) -> bytes:
def loads(asbytes: bytes) -> RelationshipManagerPersistent: # @staticmethod
The abbreviated Relationship Manager API is typically only used in unit tests and some documentation:
def ER(self, relId, cardinality, directionality="directional"): # EnforceRelationship
def R(self, fromObj, toObj, relId=1): # AddRelationship
def P(self, fromObj, relId=1): # findObjectPointedToByMe
def PS(self, fromObj, relId=1): # findObjectsPointedToByMe
def B(self, toObj, relId=1): # findObjectPointingToMe
def NR(self, fromObj, toObj, relId=1): # RemoveRelationships
def CL(self): # Clear
# No abbreviated API for the following:
def FindObjects(self, From=None, To=None, RelId=1) -> Union[List[object], bool]: pass
def FindObject(self, From=None, To=None, RelId=1) -> object: pass
Relationships = property(GetRelations, SetRelations)
# No abbreviated API for the persistence API:
objects: Namespace
def dumps(self) -> bytes: # pickle persistence related
def loads(asbytes: bytes) -> RelationshipManagerPersistent:
All possible permutations of this abbreviate API approach can be found in
tests/python/test_enforcing_relationship_manager.py
Hiding the use of Relationship Manager
Its probably best to hide the use of Relationship Manager and simply use it as
an implementation underneath traditional wiring methods like .add()
and
setY()
or properties like .subject
etc.
For example, to implement:
______________ ______________
| X | | Y |
|______________| |______________|
| | | |
|void setY(y) |1 1| |
|Y getY() |----->| |
|void clearY()| | |
|______________| |______________|
write the Python code like this:
class X:
def __init__(self): RM.ER("xtoy", "onetoone", "directional")
def setY(self, y): RM.R(self, y, "xtoy")
def getY(self): return RM.P(self, "xtoy")
def clearY(self): RM.NR(self, self.getY(), "xtoy")
class Y:
pass
Note the use of the abbreviated Relationship Manager API in this example.
Another example of hiding the use of Relationship Manager
Here is another example of hiding the use of Relationship Manager,
found in the examples folder as rm_python/examples/observer.py
- the
classic Subject/Observer pattern:
from rm_python.relationship_manager import RelationshipManager
rm = RelationshipManager()
class Observer:
@property
def subject(self):
return rm.FindObjectPointedToByMe(self)
@subject.setter
def subject(self, _subject):
rm.AddRelationship(self, _subject)
def Notify(self, subject, notificationEventType):
pass # implementations override this and do something
class Subject:
def NotifyAll(self, notificationEventType):
observers = rm.FindObjects(None, self) # all things pointing at me
for o in observers:
o.Notify(self, notificationEventType)
def AddObserver(self, observer):
rm.AddRelationship(observer, self)
def RemoveObserver(self, observer):
rm.RemoveRelationships(From=observer, To=self)
Persistence
The easiest approach to persistence is to use the built in dumps
and loads
methods of RelationshipManager
. Relationship Manager also provides an attribute
object called .objects
where you should keep all the objects involved in
relationships e.g.
rm.objects.obj1 = Entity(strength=1, wise=True, experience=80)
Then when you persist the Relationship Manager both the objects and
relations are pickled and later restored. This means your objects are
accessible by attribute name e.g. rm.objects.obj1
at all times. You can
assign these references to local variables for convenience e.g. obj1 = rm.objects.obj1
.
Here is complete example of creating three entitys, wiring them up, persisting them then restoring them:
import pprint
import random
from rm_python.relationship_manager import RelationshipManagerPersistent as RelationshipManager
from dataclasses import dataclass
@dataclass
class Entity:
strength: int = 0
wise: bool = False
experience: int = 0
def __hash__(self):
hash_value = hash(self.strength) ^ hash(
self.wise) ^ hash(self.experience)
return hash_value
rm = RelationshipManager()
obj1 = rm.objects.obj1 = Entity(strength=1, wise=True, experience=80)
obj2 = rm.objects.obj2 = Entity(strength=2, wise=False, experience=20)
obj3 = rm.objects.obj3 = Entity(strength=3, wise=True, experience=100)
rm.AddRelationship(obj1, obj2)
rm.AddRelationship(obj1, obj3)
assert rm.FindObjects(obj1) == [obj2, obj3]
# persist
asbytes = rm.dumps()
# resurrect
rm2 = RelationshipManager.loads(asbytes)
# check things worked
newobj1 = rm2.objects.obj1
newobj2 = rm2.objects.obj2
newobj3 = rm2.objects.obj3
assert rm2.FindObjects(newobj1) == [newobj2, newobj3]
assert rm2.FindObjectPointedToByMe(newobj1) is newobj2
print('done, all OK')
Persistence API
As a reminder, the persistence API of RelationshipManager
is:
objects: Namespace
def dumps(self) -> bytes:
@staticmethod
def loads(asbytes: bytes) -> RelationshipManagerPersistent:
Please create attributes on the objects
property of the relationship manager, pointing to those objects involved in relationships. It is however optional, and only provides a guaranteed way of pickling and persisting the objects involved in the relationships along with the relationships themselves, when persisting the relationship manager.
You could attach your other application state to this objects
property of the relationship manager and thus save your entire application state into the same file. Alternively save the pickeled bytes into your own persistence file solution.
There are currently no dump()
or load()
methods implemented, which would pickle
to and from a file. These can easily be added in a subclass or just write and
read the results of the existing dumps()
and loads()
methods to a file
yourself, as bytes.
Manual Control of Persistence
Persistence can be a bit tricky because you need to persist both objects and relationships between those objects.
Other libraries that implement models, schemas, serializers/deserializers, ODM's/ORM's, Active Records or similar patterns will require you to define your classes in a particular way. Relationship Manager works with any Python objects like dataclass objects etc. without any special decoration or structure required.
Whilst it is possible to simply pickle a Relationship Manager instance and restore it, you won't have easy access to the objects involved. Sure, Relationship Manager will return objects which have been resurrected from persistence correctly but how, in such a unpickled situation, will you pass object instances to the Relationship Manager API? Thus its better to prepare your persitence properly and store all your objects in a dictionary or object and pickle that together with the Relationship Manager. E.g.
@dataclass
class Entity:
strength: int = 0
wise: bool = False
experience: int = 0
def __hash__(self):
hash_value = hash(self.strength) ^ hash(
self.wise) ^ hash(self.experience)
return hash_value
@dataclass
class Namespace:
"""Just want a namespace to store vars/attrs in. Could use a dictionary."""
@dataclass
class PersistenceWrapper:
"""Holds both objects and relationships. Could use a dictionary."""
objects: Namespace # Put all your objects involved in relationships as attributes of this object
relations: List # Relationship Manager relationship List will go here
objects = Namespace() # create a namespace for the variables
objects.id1 = Entity(strength=1, wise=True, experience=80)
objects.id2 = Entity(strength=2, wise=False, experience=20)
objects.id3 = Entity(strength=3, wise=True, experience=100)
rm = RelationshipManager()
rm.AddRelationship(objects.id1, objects.id2)
rm.AddRelationship(objects.id1, objects.id3)
assert rm.FindObjects(objects.id1) == [objects.id2, objects.id3]
# persist
asbytes = pickle.dumps(PersistenceWrapper(objects=objects, relations=rm.Relationships))
# resurrect
data: PersistenceWrapper = pickle.loads(asbytes)
rm2 = RelationshipManager()
objects2 = data.objects
rm2.Relationships = data.relations
# check things worked
assert rm2.FindObjects(objects2.id1) == [objects2.id2, objects2.id3]
For a more detailed example, see
rm_python/examples/persistence/persist_pickle.py
as well as other persistence approaches in that directory, including an approach which
stores objects in dictionaries with ids and uses the Relationship Manager to store relationships between those ids, rather than relationships between object references.
Running the tests
Check our this project from GitHub, open the project directory in vscode and there is a local settings.json
and launch.json
already populated which means you can choose the launch profile Run all tests: using -m unittest
or use the vscode built in GUI test runner (hit the Discover Tests
button then the Run all tests
button).
Or simply:
python -m unittest discover -p 'test*' -v tests
Appendix: Installing into a new virtual environment
Either use pipenv
to manage a new virtual environment or use Python's built in venv
:
mkdir proj1
cd proj1
python -m venv env
env/bin/pip install relationship-manager
env/bin/python
> from rm_python.relationship_manager import RelationshipManager
You can activate the virtual environment after you create it, which makes running pip
and python
etc. easier
mkdir proj1
cd proj1
python -m venv env
source env/bin/activate
pip install relationship-manager
python
> from rm_python.relationship_manager import RelationshipManager
Final Thoughts on the Python Implementation
References and memory
Be careful - the Relationship Manager will have references to your objects so garbage collection may not be able to kick in. If you remove all relationships for an object it should be removed from the Relationship Manager, but this needs to be verified in these implementations.
Performance
Be mindful that normal object to object wiring using references and lists of references is going to be much faster than a Relationship Manager.
You can have multiple relationship manager instances to manage different areas of your programming domain, which increases efficiency and comprehensibility.
You may want to google for other more professional Object Databases. For example, in the Python space we have:
- https://github.com/grundic/awesome-python-models - A curated list of awesome Python libraries, which implement models, schemas, serializers/deserializers, ODM's/ORM's, Active Records or similar patterns.
- https://www.opensourceforu.com/2017/05/three-python-databases-pickledb-tinydb-zodb/ - A peek at three Python databases: PickleDB, TinyDB and ZODB
- https://tinydb.readthedocs.io/en/stable/usage.html#queries - Welcome to TinyDB, your tiny, document oriented database optimized for your happiness
- https://divmod.readthedocs.io/en/latest/products/axiom/index.html - Axiom is an object database whose primary goal is to provide an object-oriented layer to an SQL database
- http://www.newtdb.org/en/latest/getting-started.html - Newt DB - You’ll need a Postgres Database server.
- http://www.zodb.org/en/latest/tutorial.html#tutorial-label - This tutorial is intended to guide developers with a step-by-step introduction of how to develop an application which stores its data in the ZODB.
However most of these need a backing SQL database - Relationship Manager does not, which may be a benefit - no databases to set up - just get on with coding.
Other implementations of Relationship Manager
In this Github repository there are several other implementations of Relationship Manager
C#
Very fast implementation for .NET - has been used in a commercial project. Note that the Visual Studio 2005 projects/solutions need updating to a more recent version of Visual Studio.
Boo
The boo language for .NET is now dead, however this implementation created a .net .dll
that was usable by other .NET languages. This dll is still in the project and perfectly usable, however the C# implementation is much faster.
Java
A java implementation.
Javascript
To be completed.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
File details
Details for the file relationship-manager-1.4.1.tar.gz
.
File metadata
- Download URL: relationship-manager-1.4.1.tar.gz
- Upload date:
- Size: 12.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/3.2.0 pkginfo/1.5.0.1 requests/2.24.0 setuptools/49.2.1 requests-toolbelt/0.9.1 tqdm/4.50.2 CPython/3.8.6
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | a333a0cc0b3d6c05b43f0503880b5aa1a64727fbf8105c8bbcd97e2b9679d7c2 |
|
MD5 | df4d09b023e5ecfbfea37455aa470ac2 |
|
BLAKE2b-256 | fee6c31cf4ae2b5b8da9fb622bddad5e700a1f94bf8974286191cde576167554 |
File details
Details for the file relationship_manager-1.4.1-py3-none-any.whl
.
File metadata
- Download URL: relationship_manager-1.4.1-py3-none-any.whl
- Upload date:
- Size: 10.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/3.2.0 pkginfo/1.5.0.1 requests/2.24.0 setuptools/49.2.1 requests-toolbelt/0.9.1 tqdm/4.50.2 CPython/3.8.6
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 4a36dddd0d95bd68a0096619ab09169b71e0e6f63d5389532e81c7908214e648 |
|
MD5 | a660a7c41a217cf196df2b930bb1dbeb |
|
BLAKE2b-256 | 71410224f586832c318f8a404935839488ca40a328f6c6443b9d78348bf8d889 |