How to Use Relationship Managers and Relationship Definitions in neomodel
Relationship managers and relationship definitions in neomodel provide a high-level API to define graph edges between nodes, enforce cardinality constraints, and execute Cypher queries through methods like connect(), disconnect(), and all().
The neo4j-contrib/neomodel library abstracts Neo4j graph operations into Pythonic object-graph mapping. Understanding relationship managers and relationship definitions is essential for modeling complex schemas, as these components handle everything from simple friendships to strict one-to-one mappings with properties on the edge itself.
Core Concepts
Relationship Definitions
Relationship definitions are class attributes declared on a StructuredNode subclass. They describe the target node class, the Neo4j relationship type, its direction, and optional cardinality or relationship model. The three concrete definition helpers are:
RelationshipTo– Outgoing relationships (default direction).RelationshipFrom– Incoming relationships.Relationship– Bidirectional (EITHER) relationships.
These are implemented in neomodel/sync_/relationship_manager.py within the RelationshipDefinition class hierarchy.
Relationship Managers
Relationship managers are runtime objects instantiated for each defined relationship on a node. They provide the actual API for graph manipulation and are subclasses of RelationshipManager defined in neomodel/sync_/relationship_manager.py. The specific manager class used depends on the cardinality constraint (e.g., ZeroOrMore, One, ZeroOrOne).
How Definitions Become Managers
When a StructuredNode is instantiated, neomodel calls RelationshipDefinition.build_manager(source, name) to construct the appropriate manager.
# From neomodel/sync_/relationship_manager.py (lines 45-48)
def build_manager(self, source: "StructuredNode", name: str) -> RelationshipManager:
self.lookup_node_class()
return self.manager(source, name, self.definition)
The process involves three steps:
lookup_node_class()resolves the target class (whether passed as a string or type reference).self.managerreferences the cardinality class (e.g.,ZeroOrMore,One).- The manager receives the source node instance, the attribute name, and a low-level
definitiondictionary containingrelation_type,direction, optionalmodel, and other metadata.
All subsequent calls such as user.friends.all() or user.friends.connect(other) are delegated to this manager instance.
Defining Relationships in Your Models
Here is a practical schema demonstrating the three definition types:
from neomodel import StructuredNode, StringProperty, RelationshipTo, RelationshipFrom, Relationship, One
class Person(StructuredNode):
name = StringProperty()
# Zero-or-more outgoing FRIEND relationships (default cardinality)
friends = RelationshipTo('Person', 'FRIEND')
# Incoming KNOWS relationships
known_by = RelationshipFrom('Person', 'KNOWS')
# Exactly one spouse relationship using One cardinality
spouse = Relationship('Person', 'SPOUSE', cardinality=One)
RelationshipTo('Person', 'FRIEND')creates a zero-or-more outgoing relationship using theZeroOrMoremanager.RelationshipFrom('Person', 'KNOWS')creates an incoming relationship with the same cardinality.Relationship('Person', 'SPOUSE', cardinality=One)enforces exactly one spouse; attempting to connect a second spouse raisesAttemptedCardinalityViolation.
Enforcing Cardinality Constraints
Cardinality constraints are implemented in neomodel/sync_/cardinality.py and determine which manager subclass is instantiated:
| Class | File | Constraint |
|---|---|---|
ZeroOrMore |
relationship_manager.py |
Default. Allows any number of related nodes. |
ZeroOrOne |
cardinality.py |
At most one related node; raises AttemptedCardinalityViolation on excess. |
One |
cardinality.py |
Exactly one related node; disconnect and disconnect_all are prohibited. |
OneOrMore |
cardinality.py |
At least one related node; disconnect_all is prohibited. |
When cardinality is violated, neomodel raises specific exceptions:
from neomodel import AttemptedCardinalityViolation
try:
alice.spouse.connect(bob)
alice.spouse.connect(carol) # Raises exception
except AttemptedCardinalityViolation:
print("Cannot connect more than one spouse")
Working with Relationship Models (Edge Properties)
Relationships can carry properties by defining a StructuredRel subclass and passing it to the model parameter:
from neomodel import StructuredRel, IntegerProperty
class WorksAt(StructuredRel):
since = IntegerProperty()
class Person(StructuredNode):
name = StringProperty()
works_at = RelationshipTo('Company', 'WORKS_AT', model=WorksAt)
When a model is supplied, connect returns an instance of the relationship class populated with the supplied properties:
alice = Person(name='Alice').save()
acme = Company(name='Acme').save()
rel = alice.works_at.connect(acme, {'since': 2020})
print(rel.since) # → 2020
The Relationship Manager API
The RelationshipManager class in neomodel/sync_/relationship_manager.py provides a rich interface for graph manipulation. All mutation methods are wrapped with the @check_source decorator (lines 27-36) to ensure the source node is persisted before execution.
Connection Management
connect(node, properties=None)– Creates a relationship tonode. Handles inverse-side cardinality checks and returns aStructuredRelinstance if a model is defined.disconnect(node)– Deletes the relationship tonode. May be blocked by cardinality constraints (e.g.,Onecardinality prohibits disconnection).disconnect_all()– Deletes all related nodes. Not allowed forOneorOneOrMorecardinalities.reconnect(old_node, new_node)– Replacesold_nodewithnew_nodewhile preserving relationship properties. Useful when cardinality would otherwise forbid a directconnect.
Querying Related Nodes
all()– Returns a list of all related nodes, or relationship objects if a model is defined.single()– Returns the first related node orNone. For cardinalities expecting a single node, raisesCardinalityViolationif the actual count differs.is_connected(node)– Boolean test for relationship existence.
Advanced Querying
The manager proxies the underlying NodeSet API for advanced filtering:
# Filter friends named Bob
alice.friends.filter(name="Bob")
# Order by name descending
alice.friends.order_by("-name")
# Exclude specific nodes
alice.friends.exclude(name="Charlie")
Internals: How Managers Build Cypher
The connect method (lines 99-180 in relationship_manager.py) constructs Cypher queries using _rel_merge_helper from neomodel/sync_/relationship.py:
new_rel = _rel_merge_helper(
lhs="us",
rhs="them",
ident="r",
relation_properties=rel_prop,
**self.definition
)
q = f"MATCH (them), (us) WHERE {db.get_id_method()}(them)=$them and {db.get_id_method()}(us)=$self MERGE" + new_rel
If a relationship model is present, the method inflates the returned Neo4j relationship into a StructuredRel instance and wires its start/end node classes via _set_start_end_cls.
Summary
- Relationship definitions (
RelationshipTo,RelationshipFrom,Relationship) are declared onStructuredNodeclasses to describe the target node, relationship type, direction, and cardinality. - Relationship managers are instantiated at runtime via
RelationshipDefinition.build_manager()and provide the API for connecting, disconnecting, and querying nodes. - Cardinality classes (
ZeroOrMore,ZeroOrOne,One,OneOrMore) enforce constraints and determine available manager methods. - Relationship models (
StructuredRel) allow properties on edges and are passed via themodelparameter in definitions. - All mutation methods check node persistence via
@check_sourceand generate optimized Cypher through_rel_merge_helper.
Frequently Asked Questions
What is the difference between RelationshipTo and RelationshipFrom?
RelationshipTo defines an outgoing relationship from the current node to the target class, while RelationshipFrom defines an incoming relationship from the target class to the current node. Both use the ZeroOrMore manager by default, but RelationshipTo is the most common choice for standard graph modeling where you traverse from subject to object.
How do I enforce a one-to-one relationship in neomodel?
Import the One cardinality class from neomodel and pass it to the cardinality parameter in your relationship definition: spouse = Relationship('Person', 'SPOUSE', cardinality=One). This prevents disconnecting the relationship without reconnecting and raises AttemptedCardinalityViolation if you attempt to connect a second node while one already exists.
Can I store properties on relationships between nodes?
Yes, by defining a subclass of StructuredRel with properties such as since = IntegerProperty(), then passing it as the model parameter in your relationship definition: works_at = RelationshipTo('Company', 'WORKS_AT', model=WorksAt). When calling connect(), pass a dictionary of properties as the second argument to populate these fields.
What happens if I try to disconnect a node when cardinality is set to One?
The One cardinality manager overrides the disconnect and disconnect_all methods to raise an exception, as removing the relationship would violate the constraint that exactly one relationship must exist. To replace the related node, use the reconnect(old_node, new_node) method instead, which atomically swaps the connection while preserving any relationship properties.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →