Package: relationship-manager
Relationship Manager - a lightweight Object Database classA central mediating class which records all the one-to-one, one-to-many and many-to-many relationships between a group of classes.What is it?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 aRelationship Managerobject 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 likeLINQ.In a sense, anObject Databaseis an elaborate implementation of the Relationship Manager pattern. However theintentof 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.Here are various implementations of the Relationship Manager Pattern in this GitHub repository:Python: Uses Python 3, there are no dependencies.JavaC#: Visual Studio 2005 project with unit test. Very fast implementation used in at least one commercial product.PythonInstallationpipinstallrelationship-managerUsagefromrelmgrimportRelationshipManagerrm=RelationshipManager()rm.enforce("xtoy","onetoone","directional")x=object()y=object()rm.add_rel(x,y,"xtoy")assertrm.find_target(x,"xtoy")==yRead the unit tests to see all functionality being exercised, incl. backpointer queries.See the examples below and in therelmgr/examples/directory of this repository.See fullAPI documentation.See the Relationship Manager pattern referred to above for lots more documentation.Python APIQuick summary of the v2 API:defadd_rel(self,source,target,rel_id:Union[int,str]=1)->None:defremove_rel(self,source,target,rel_id=1)->None:defenforce(self,rel_id,cardinality,directionality="directional"):defclear(self)->None:# query APIdeffind_targets(self,source,rel_id)->List:deffind_target(self,source,rel_id)->object:deffind_sources(self,target,rel_id)->List:# Back pointer querydeffind_source(self,target,rel_id)->object:# Back pointer querydeffind_rels(self,source,target)->List:defis_rel(self,source,target,rel_id=1)->bool:# persistence relatedobjects:Namespacerelationships=property(_get_relationships,_set_relationships)# flat list of rel. tuplesdefdumps(self)->bytes:defloads(asbytes:bytes)->RelationshipManager:# @staticmethodSee fullAPI documentation.Hiding the use of Relationship ManagerIts probably best practice to hide the use of Relationship Manager and simply use it as
an implementation underneath traditional wiring methods like.add()andsetY()or properties like.subjectetc.For example, to implement:______________        ______________
        |       X      |      |       Y      |
        |______________|      |______________|
        |              |      |              |
        |void  setY(y) |1    1|              |
        |Y     getY()  |----->|              |
        |void  clearY()|      |              |
        |______________|      |______________|write the Python code like this:fromrelmgrimportRelMgrRM=RelMgr()classX:def__init__(self):rm.enforce("xtoy","onetoone","directional")defsetY(self,y):rm.add_rel(self,y,"xtoy")defgetY(self):returnrm.find_target(source=self,rel_id="xtoy")defclearY(self):rm.remove_rel(self,self.getY(),"xtoy")classY:passNote the use of the abbreviated Relationship Manager API in this example.Another exampleHere is another example of hiding the use of Relationship Manager,
found in the examples folder asrelmgr/examples/observer.py- the
classic Subject/Observer pattern:fromrelmgrimportRelationshipManagerrm=RelationshipManager()classObserver:@propertydefsubject(self):returnrm.find_target(self)@subject.setterdefsubject(self,_subject):rm.add_rel(self,_subject)defnotify(self,subject,notification_type):pass# implementations override this and do somethingclassSubject:defnotify_all(self,notification_type:str):observers=rm.find_sources(self)# all things pointing at meforoinobservers:o.Notify(self,notification_type)defadd_observer(self,observer):rm.add_rel(observer,self)defremove_observer(self,observer):rm.remove_rel(source=observer,target=self)PersistenceThe easiest approach to persistence is to use the built indumpsandloadsmethods ofRelationshipManager. Relationship Manager also provides an attribute
object called.objectswhere 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.obj1at 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:importpprintimportrandomfromdataclassesimportdataclassfromrelmgrimportRelationshipManager@dataclassclassEntity:strength:int=0wise:bool=Falseexperience:int=0def__hash__(self):hash_value=hash(self.strength)^hash(self.wise)^hash(self.experience)returnhash_valuerm=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.add_rel(obj1,obj2)rm.add_rel(obj1,obj3)assertrm.find_targets(obj1)==[obj2,obj3]# persistasbytes=rm.dumps()# resurrectrm2=RelationshipManager.loads(asbytes)# check things workednewobj1=rm2.objects.obj1newobj2=rm2.objects.obj2newobj3=rm2.objects.obj3assertrm2.find_targets(newobj1)==[newobj2,newobj3]assertrm2.find_target(newobj1)isnewobj2print('done, all OK')Persistence APIAs a reminder, the persistence API ofRelationshipManageris:objects:Namespacedefdumps(self)->bytes:@staticmethoddefloads(asbytes:bytes)->RelationshipManager:Please create attributes on theobjectsproperty 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 thisobjectsproperty 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 nodump()orload()methods implemented, which would pickle
to and from afile. These can easily be added in a subclass or just write and
read the results of the existingdumps()andloads()methods to a file
yourself, as bytes.Manual Control of PersistencePersistence 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.For code examples of custom persistence, seeresearch/python persistence research/persist_pickle_simple.pyas 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 testsPython testsCheck our this project from GitHub, open the project directory in vscode and there is a localsettings.jsonandlaunch.jsonalready populated which means you can choose the launch profileRun all tests: using -m unittestor use the vscode built in GUI test runner (hit theDiscover Testsbutton then theRun all testsbutton).Or simply:python-munittestdiscover-p'test*'-vtestsC# and Java testsOpen the projects and run the tests from your IDE.Appendix: Installing into a new virtual environmentEither usepipenvto manage a new virtual environment or use Python's built invenv:mkdirproj1cdproj1
python-mvenvenv

env/bin/pipinstallrelationship-manager
env/bin/python
>fromrelmgrimportRelationshipManagerYou can activate the virtual environment after you create it, which makes runningpipandpythonetc. easiermkdir proj1
cd proj1
python -m venv env

source env/bin/activate
pip install relationship-manager
python
> from relmgr import RelationshipManagerFinal Thoughts on the Python ImplementationReferences and memoryBe 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.PerformanceBe 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 professionalObject 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 ZODBhttps://tinydb.readthedocs.io/en/stable/usage.html#queries- Welcome to TinyDB, your tiny, document oriented database optimized for your happinesshttps://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 databasehttp://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 ManagerIn this Github repository there are several other implementations of Relationship Manager. Their APIs are not the latest however - the methods names have evolved - the Python implementation is the gold standard API and implementation.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.BooTheboo languagefor .NET is now dead, however this implementation created a .net.dllthat was usable by other .NET languages.  This dll is still in the project and perfectly usable, however the C# implementation is much faster.JavaA Java implementation.  Needs a bit of dusting off, but should run.Note that the C# and Java implementations have a slightly cleaner set of methods and a few extra methods - and also use a nice interface to talk to.  The method names are substantially the same though.C# and Java APINote: C# and Java implementations use the original older v1 API, not the new v2 API implemented in Python, above.publicenumCardinality{OneToOne,OneToMany,ManyToOne,ManyToMany}publicenumDirectionality{UniDirectional,DirectionalWithBackPointer,DoubleDirectional}interfaceIRelationshipManager{voidAddRelationship(objectfromObj,objecttoObj,stringrelId);voidAddRelationship(objectfromObj,objecttoObj);voidEnforceRelationship(stringrelId,Cardinalitycardinality);voidEnforceRelationship(stringrelId,Cardinalitycardinality,Directionalitydirectionality);IListFindObjectsPointedToByMe(objectfromObj,stringrelId);objectFindObjectPointedToByMe(objectfromObj,stringrelId);IListFindObjectsPointingToMe(objecttoObj,stringrelId);objectFindObjectPointingToMe(objecttoObj,stringrelId);voidRemoveRelationship(objectfromObj,objecttoObj,stringrelId);voidRemoveAllRelationshipsInvolving(objectobj,stringrelId);intCount();intCountRelationships(stringrelId);voidClear();boolDoesRelIdExistBetween(objectfromObj,objecttoObj,stringrelId);IListFindRelIdsBetween(objectfromObj,objecttoObj);}C# and Java abbreviated APIThe abbreviated API is more succinct, but is only recommended for unit tests.Return TypeFunction NameShort-handvoidaddRelationship(from, to, id)R(f,t)voidremoveRelationship(from, to, id)NR(f,t)ListfindObjectsPointedToByMe(from, id)PS(f)ListfindObjectsPointingToMe(to, id)BS(t)voidEnforceRelationship(id, cardinality, bidirectionality)ER(id, c, bi)ObjectfindObjectPointedToByMe(fromMe, id, cast)P(f)ObjectfindObjectPointingToMe(toMe, id, cast)B(t)voidremoveAllRelationshipsInvolving(object, id)NRS(o)For exampleObjectis just one ofyourobjects which you added withaddRelationship().Recastthat's just in case you need to cast to a type. This might have been possible in theboo languagefor .NET (which is now dead). Please adapt to your language as needed. Dynamic languages don't need casting.C# and Java - Finding just one objectThe pair of find methodsFindObjectPointedToByMe()andFindObjectPointedToByMe()only findoneobject (even though there may be more), and cast it to the appropriate type.  This is a commonly used convenience method - the more painful way would be to useFindObjectsPointingToMe()and just grab the first object from the returned list.
Exactly which object is found is undefined, but would typically be the first one added.C# and Java - Relationship IdWhat to use as the Relationship Id?This is traditionally either an integer or a string in the Python implementation.  I have chosen to use a string in the C# and Java implementations, since you can describe relationships easily in this way rather than having to map from an integer back to some meaningful description.rm.addRelationship(fromObject,toObject,relationshipId)will raise an exception if relationshipId is an empty string.All other functions (except foraddRelationship) can pass either an empty string or "*" as therelationshipId, which means you are searching for any relationship at all.  You would usually only want to do this if there is onlyonerelationship between class X and class Y, then your P and NR calls can specify "*" as therelationshipIdin order to match any relationship between these two objects.  Alternatively, you can use relationship manager's overloaded versions of all its routines (except foraddRelationship) which don't take arelationshipIdwhererelationshipIddefaults to "*".ResourcesFullAPI documentation.OfficialRelationship Manager Patternpage incl. academic paper by Andy Bulka.Python ImplementationREADME(this page) andGitHub project.