# How to Use Relationship Managers and Relationship Definitions in neomodel

> Learn to use neomodel relationship managers and definitions to define graph edges enforce constraints and query relationships with connect disconnect and all methods

- Repository: [Neo4j Contrib/neomodel](https://github.com/neo4j-contrib/neomodel)
- Tags: how-to-guide
- Published: 2026-03-08

---

**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`](https://github.com/neo4j-contrib/neomodel/blob/main/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`](https://github.com/neo4j-contrib/neomodel/blob/main/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.

```python

# 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:

```python
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`](https://github.com/neo4j-contrib/neomodel/blob/main/neomodel/sync_/cardinality.py) and determine which manager subclass is instantiated:

| Class | File | Constraint |
|-------|------|------------|
| `ZeroOrMore` | [`relationship_manager.py`](https://github.com/neo4j-contrib/neomodel/blob/main/relationship_manager.py) | Default. Allows any number of related nodes. |
| `ZeroOrOne` | [`cardinality.py`](https://github.com/neo4j-contrib/neomodel/blob/main/cardinality.py) | At most one related node; raises `AttemptedCardinalityViolation` on excess. |
| `One` | [`cardinality.py`](https://github.com/neo4j-contrib/neomodel/blob/main/cardinality.py) | Exactly one related node; `disconnect` and `disconnect_all` are prohibited. |
| `OneOrMore` | [`cardinality.py`](https://github.com/neo4j-contrib/neomodel/blob/main/cardinality.py) | At least one related node; `disconnect_all` is prohibited. |

When cardinality is violated, neomodel raises specific exceptions:

```python
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:

```python
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:

```python
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`](https://github.com/neo4j-contrib/neomodel/blob/main/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`.

### 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 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:

```python

# 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`](https://github.com/neo4j-contrib/neomodel/blob/main/relationship_manager.py)) constructs Cypher queries using `_rel_merge_helper` from [`neomodel/sync_/relationship.py`](https://github.com/neo4j-contrib/neomodel/blob/main/neomodel/sync_/relationship.py):

```python
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.