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:

  1. lookup_node_class() resolves the target class (whether passed as a string or type reference).
  2. self.manager references the cardinality class (e.g., ZeroOrMore, One).
  3. The manager receives the source node instance, the attribute name, and a low-level definition dictionary containing relation_type, direction, optional model, 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 the ZeroOrMore manager.
  • 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 raises AttemptedCardinalityViolation.

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 to node. Handles inverse-side cardinality checks and returns a StructuredRel instance if a model is defined.
  • disconnect(node) – Deletes the relationship to node. May be blocked by cardinality constraints (e.g., One cardinality prohibits disconnection).
  • disconnect_all() – Deletes all related nodes. Not allowed for One or OneOrMore cardinalities.
  • reconnect(old_node, new_node) – Replaces old_node with new_node while preserving relationship properties. Useful when cardinality would otherwise forbid a direct connect.
  • all() – Returns a list of all related nodes, or relationship objects if a model is defined.
  • single() – Returns the first related node or None. For cardinalities expecting a single node, raises CardinalityViolation if 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 on StructuredNode classes 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 the model parameter in definitions.
  • All mutation methods check node persistence via @check_source and 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →