Relationships¶
Establishing an undirected relationship between two entities is done via the Relationship class. This requires the class of the connected entity as well as the type of the relationship.:
class Person(StructuredNode):
friends = Relationship('Person', 'FRIEND')
When defining relationships, you may refer to classes in other modules. This avoids cyclic imports:
class Garage(StructuredNode):
cars = RelationshipTo('transport.models.Car', 'CAR')
vans = RelationshipTo('.models.Van', 'VAN')
Cardinality¶
It is possible to (softly) enforce cardinality constraints on your relationships. Remember this needs to be declared on both sides of the relationship definition:
class Person(StructuredNode):
car = RelationshipTo('Car', 'OWNS', cardinality=One)
class Car(StructuredNode):
owner = RelationshipFrom('Person', 'OWNS', cardinality=One)
The following cardinality constraints are available:
|
|
|
|
If a cardinality constraint is violated by existing data a CardinalityViolation
exception is raised.
On attempting to violate a cardinality constraint a
AttemptedCardinalityViolation
is raised.
Properties¶
Neomodel uses relationship
models to define the properties stored on relations:
class FriendRel(StructuredRel):
since = DateTimeProperty(
default=lambda: datetime.now(pytz.utc),
index=True
)
met = StringProperty()
# Uniqueness constraints for relationship properties
# are only available from Neo4j version 5.7 onwards
meeting_id = StringProperty(unique_index=True)
class Person(StructuredNode):
name = StringProperty()
friends = RelationshipTo('Person', 'FRIEND', model=FriendRel)
rel = jim.friends.connect(bob)
rel.since # datetime object
The data to populate these properties when establishing a connection can be supplied
to the connect
method:
rel = jim.friends.connect(bob,
{'since': yesterday, 'met': 'Paris'})
print(rel.start_node().name) # jim
print(rel.end_node().name) # bob
rel.met = "Amsterdam"
rel.save()
You can retrieve relationships between two nodes using the ‘relationship’ method. This is only available for relationships with a defined relationship model:
rel = jim.friends.relationship(bob)
Relationship Uniqueness¶
By default neomodel applies only one relationship instance between two node instances and
this is achieved via use of MERGE
. (This used to be CREATE UNIQUE
until Cypher deprecated this command.)
Relationships and Inheritance¶
Relationships are established between Nodes of different types within a Neo4J Data Base Management System (DBMS) and this section contains more details about how nodes of different types at the two endpoints of a relationship are resolved by neomodel as wel as how extending relationship classes themselves works.
Node Inheritance¶
Neomodel is capable of understanding and resolving derived nodes at the endpoints of a relationships properly.
The following model establishes a BasePerson
that can be friends_with any class derived
from BasePerson
. Two concrete classes of BasePerson
(TechnicalPerson
and PilotPerson
) are
further defined.
class PersonalRelationship(neomodel.StructuredRel):
"""
A very simple relationship between two BasePersons that simply
records the date at which an acquaintance was established.
"""
on_date = neomodel.DateProperty(default_now = True)
class BasePerson(neomodel.StructuredNode):
"""
Base class for defining some basic sort of an actor in a system.
The base actor is defined by its name and a `friends_with`
relationship.
"""
name = neomodel.StringProperty(required = True, unique_index = True)
friends_with = neomodel.RelationshipTo("BasePerson", "FRIENDS_WITH", model = PersonalRelationship)
class TechnicalPerson(BasePerson):
"""
A Technical person specialises BasePerson by adding their
expertise.
"""
expertise = neomodel.StringProperty(required = True)
class PilotPerson(BasePerson):
"""
A pilot person specialises BasePerson by adding the type of
airplane they can operate.
"""
airplane = neomodel.StringProperty(required = True)
This means that either of these concrete objects can appear at the end
of a friends_with
relationship and be instantiated to the right object.
Here is a minimal example to demonstrate that:
# Create some technical persons
A = TechnicalPerson(name = "Grumpy", expertise = "Grumpiness").save()
B = TechnicalPerson(name = "Happy", expertise = "Unicorns"}).save()
C = TechnicalPerson(name = "Sleepy", expertise = "Pillows"}).save()
# Create some Pilot Persons
D = PilotPerson(name = "Porco Rosso", airplane = "Savoia-Marchetti").save()
E = PilotPerson(name = "Jack Dalton", airplane = "Beechcraft Model 18").save()
# TechnicalPersons befriend Technical Persons
A.friends_with.connect(B)
B.friends_with.connect(C)
C.friends_with.connect(A)
# Pilot Persons befriend Pilot Persons
D.friends_with.connect(E)
# Technical Persons befriend Pilot Persons
A.friends_with.connect(D)
E.friends_with.connect(C)
for some_friend in A.friends_with:
print(some_friend)
This will show two friends connected with node “Grumpy”, one of which is a TechnicalPerson
and the other a PilotPerson
.
Relationship Inheritance¶
Neomodel uses StructuredRel
to create classes that describe relationship objects. When the time comes to store this
relationship with the DBMS, neomodel creates a Neo4J Relationship that is characterised by a single label along with
the data members of the relationship class. Therefore, there is a direct correspondence between the relationship label
and the relationship class.
Continuing with the example that is defined in section Node Inheritance, it is possible to extend
PersonalRelationship
to describe extended (or enriched) versions of the same class, in this way:
class PersonalRelationshipWithStrength(PersonalRelationship):
"""
An extended relationship between two BasePersons that in addition to the date on which the acquaintance was
established, it also maintains an abstract `strength` value.
"""
on_date = neomodel.DateProperty(default_now = True)
strength = neomodel.FloatProperty(default = 1.0)
There is nothing too special here about the way PersonalRelationshipWithStrength
is established, except perhaps
noticing that it inherits from PersonalRelationship
rather than neomodel.StructuredRel
.
The special bit however comes when the extended relationship is attempted to be declared between two nodes. To
demonstrate this here, we will extend BasePerson
and constrain its friends_with
attribute to be of type
PersonalRelationshipWithStrength
:
class ExtendedBasePerson(BasePerson):
"""
An additional actor in a system, characterised further by a `role` attribute and having relationships with a
`strength` attribute.
"""
name = neomodel.StringProperty(required = True, unique_index = True)
role = neomodel.StringProperty(required = True)
friends_with = neomodel.RelationshipTo("BasePerson", "FRIENDS_WITH", model = PersonalRelationshipWithStrength)
In this case, ExtendedBasePerson
entities are expected to have relationships with a strength
attribute. At the
moment, PersonalRelationshipWithStrength
substitutes PersonalRelationship
entirely everywhere within the data
model. This is in-line with Neo4Js current capabilities of supporting only one label per relationship.
Since relationship classes are “tied” to their label definition, derived relationships can only be attached to the same
label. In the above example, FRIENDS_WITH
is already “tied” to relationships of type PersonalRelationship
and
PersonalRelationshipWithStrength
derives from PersonalRelationship
and this kind of relationship class extension
is permissible.
If a relationship label is already “tied” with a relationship model and an attempt is made to re-associate it with an
entirely alien relationship class, an exception of type neomodel.exceptions.RelationshipClassRedefined
is raised
that contains full information about the current data model state and the re-definition attempt.
This now enables queries returning Relationship
objects to be instantiated to their proper models. Continuing with
the above example, a representative query to demonstrate this capability would be:
Z = neomodel.db.cypher_query("MATCH (:BasePerson)-[r:FRIENDS_WITH]->(:BasePerson) RETURN r", resolve_objects=True)
Notice here that resolve_objects
is set to True
, which enables this automatic resolution of returned objects
to their “local” data model counterparts.
Now, elements of Z
contain properly instantiated relationship objects. And because of this, it is now possible to
access the nodes at their end points directly. For example:
u = Z[0][0][0].start_node()
v = Z[0][0][0].end_node()
Here, u,v
will be instantiated to whatever type nodes are expected to be found at the end points of the
relationship.
It is worth mentioning at this point that attempting to instantiate a relationship that has not been made known to
neomodel leads to an exception. For example, suppose that the DBMS contains relationships with label BUDDIES_WITH
in addition to what has already been defined earlier as FRIENDS_WITH
. If that relationship is attempted to be
“ingested” by neomodel, then exception RelationshipClassNotDefined
would be raised:
Z = neomodel.db.cypher_query("MATCH (:BasePerson)-[r:BUDDIES_WITH]->(:BasePerson) RETURN r", resolve_objects=True)
Explicit Traversal¶
It is possible to specify a node traversal by creating a
Traversal
object. This will get all Person
entities
that are directly related to another Person
, through all relationships:
definition = dict(node_class=Person, direction=OUTGOING,
relation_type=None, model=None)
relations_traversal = Traversal(jim, Person.__label__,
definition)
all_jims_relations = relations_traversal.all()
The definition
argument is a mapping with these items:
|
The class of the traversal target node. |
|
|
|
Can be |
|
The class of the relation model, |
Note
The RelationshipTo
and RelationshipFrom
objects are simply a proxy
for defining Traversal objects at the class level.