How to Create Relationships with Different Cardinality Types in Neomodel
Use the cardinality parameter in RelationshipTo or RelationshipFrom and pass one of four manager classes—ZeroOrMore, ZeroOrOne, One, or OneOrMore—to enforce exactly how many target nodes a relationship may connect.
Neomodel, the Python Object Graph Mapper for Neo4j, provides precise control over relationship constraints through cardinality managers. When you create relationships with different cardinality types in neomodel, you define exactly how many nodes can participate in a relationship, preventing invalid graph states at runtime according to the source code in neomodel/sync_/cardinality.py.
Understanding Relationship Cardinality Managers
The cardinality system is implemented in neomodel/sync_/cardinality.py. Each manager subclasses RelationshipManager and overrides connection and disconnection logic to enforce constraints:
- ZeroOrMore: The default manager allowing any number of relationships (0 to infinity), defined in
neomodel/sync_/relationship_manager.py(lines 616-622). - ZeroOrOne: Permits zero or exactly one target node (optional singular relationship), implemented in
neomodel/sync_/cardinality.py(lines 14-30). - One: Requires exactly one target node at all times (mandatory singular relationship), implemented in
neomodel/sync_/cardinality.py(lines 106-155). - OneOrMore: Requires at least one target node (mandatory collection), implemented in
neomodel/sync_/cardinality.py(lines 63-89).
Defining Cardinality in Model Definitions
To apply cardinality constraints, pass the desired manager class to the cardinality parameter when declaring RelationshipTo, RelationshipFrom, or Relationship (for bidirectional links). The constructors for these relationship definitions reside in neomodel/sync_/relationship_manager.py (lines 24-30).
from neomodel import StructuredNode, RelationshipTo, ZeroOrOne, One, OneOrMore
class Employee(StructuredNode):
# Optional single relationship
manager = RelationshipTo("Manager", "REPORTS_TO", cardinality=ZeroOrOne)
# Mandatory single relationship
company = RelationshipTo("Company", "WORKS_FOR", cardinality=One)
# Mandatory collection (at least one)
projects = RelationshipTo("Project", "WORKS_ON", cardinality=OneOrMore)
Cardinality Types in Practice
ZeroOrMore (Default Behavior)
When you omit the cardinality parameter, neomodel uses ZeroOrMore. This imposes no limits on relationship count.
from neomodel import StructuredNode, RelationshipTo
class Monkey(StructuredNode):
dryers = RelationshipTo("HairDryer", "OWNS_DRYER") # ZeroOrMore by default
# Usage
m = Monkey(name="tim").save()
h1 = HairDryer(version=1).save()
h2 = HairDryer(version=2).save()
m.dryers.connect(h1)
m.dryers.connect(h2) # Succeeds: no cardinality limit
assert len(m.dryers.all()) == 2
Source: test/sync_/test_cardinality.py (lines 83-94)
ZeroOrOne (Optional Singular Relationship)
The ZeroOrOne manager enforces at most one target node. Attempting to connect a second target raises AttemptedCardinalityViolation.
from neomodel import StructuredNode, RelationshipTo, ZeroOrOne
from neomodel.exceptions import AttemptedCardinalityViolation
class Monkey(StructuredNode):
driver = RelationshipTo("ScrewDriver", "HAS_SCREWDRIVER", cardinality=ZeroOrOne)
m = Monkey(name="bob").save()
s1 = ScrewDriver(version=1).save()
s2 = ScrewDriver(version=2).save()
m.driver.connect(s1) # First connection succeeds
# Attempting second connection raises exception
try:
m.driver.connect(s2)
except AttemptedCardinalityViolation:
print("Cannot connect more than one screwdriver")
# To replace, use reconnect
m.driver.reconnect(s1, s2) # s2 replaces s1
assert m.driver.single().version == 2
Source: test/sync_/test_cardinality.py (lines 110-128)
One (Mandatory Singular Relationship)
The One manager requires exactly one target node at all times. It raises AttemptedCardinalityViolation when disconnecting the sole relationship and CardinalityViolation when accessing a missing relationship.
from neomodel import StructuredNode, RelationshipTo, One
from neomodel.exceptions import AttemptedCardinalityViolation, CardinalityViolation
class Monkey(StructuredNode):
toothbrush = RelationshipTo("ToothBrush", "HAS_TOOTHBRUSH", cardinality=One)
m = Monkey(name="jerry").save()
tb = ToothBrush(name="bristle").save()
# Must create the relationship
m.toothbrush.connect(tb)
# Cannot disconnect the only relationship
try:
m.toothbrush.disconnect(tb)
except AttemptedCardinalityViolation:
print("Cannot remove the mandatory toothbrush")
# Accessing without relationship raises CardinalityViolation
fresh_monkey = Monkey(name="new").save()
try:
fresh_monkey.toothbrush.all()
except CardinalityViolation:
print("Mandatory relationship missing")
Source: test/sync_/test_cardinality.py (lines 152-166)
OneOrMore (Mandatory Collection)
The OneOrMore manager enforces at least one target node. You cannot disconnect the final relationship, and accessing an empty collection raises CardinalityViolation.
from neomodel import StructuredNode, RelationshipTo, OneOrMore
from neomodel.exceptions import AttemptedCardinalityViolation, CardinalityViolation
class Monkey(StructuredNode):
car = RelationshipTo("Car", "HAS_CAR", cardinality=OneOrMore)
m = Monkey(name="jerry").save()
c1 = Car(version=2).save()
c2 = Car(version=3).save()
# Empty collection raises CardinalityViolation
try:
m.car.all()
except CardinalityViolation:
print("Must have at least one car")
# Add first car (now valid)
m.car.connect(c1)
# Can add more
m.car.connect(c2)
assert len(m.car.all()) == 2
# Can disconnect one
m.car.disconnect(c2)
assert len(m.car.all()) == 1
# Cannot disconnect the last one
try:
m.car.disconnect(c1)
except AttemptedCardinalityViolation:
print("Cannot remove the last car")
Source: test/sync_/test_cardinality.py (lines 151-188)
Handling Cardinality Violations
When cardinality constraints are violated, neomodel raises specific exceptions defined in neomodel/exceptions.py (lines 12-28):
AttemptedCardinalityViolation: Raised when an operation would create an invalid state, such as adding a second target to aZeroOrOnerelationship or removing the last target from aOneOrMorerelationship.CardinalityViolation: Raised when the current graph state violates the constraint, such as accessing aOnerelationship that has no target nodes.
Both exceptions inherit from NeomodelException, allowing you to catch all neomodel-specific errors with a single exception handler.
Summary
- Use the
cardinalityparameter inRelationshipTo,RelationshipFrom, orRelationshipto enforce relationship constraints in neomodel. - ZeroOrMore is the default manager allowing unlimited relationships, implemented in
neomodel/sync_/relationship_manager.py(lines 616-622). - ZeroOrOne enforces at most one target node, raising
AttemptedCardinalityViolationon duplicate connections (lines 14-30). - One requires exactly one target node, forbidding disconnection and raising
CardinalityViolationwhen missing (lines 106-155). - OneOrMore requires at least one target node, preventing removal of the final relationship (lines 63-89).
- All cardinality managers reside in
neomodel/sync_/cardinality.pyand raise specific exceptions defined inneomodel/exceptions.py.
Frequently Asked Questions
What is the default cardinality if I don't specify one?
If you omit the cardinality parameter, neomodel uses ZeroOrMore, which imposes no limits on the number of target nodes. This manager is defined in neomodel/sync_/relationship_manager.py (lines 616-622) and allows you to connect any number of nodes without raising cardinality violations.
Can I change the cardinality of an existing relationship?
No, cardinality is defined at the model level and enforced by Python manager classes. Changing cardinality requires updating your model definition and migrating your data to comply with the new constraints. For example, changing from ZeroOrMore to One requires ensuring every source node has exactly one target node before the code enforces the new rule.
What happens if I violate a cardinality constraint?
Neomodel raises specific exceptions based on the violation type. AttemptedCardinalityViolation occurs when you try to create an invalid state, such as connecting a second node to a ZeroOrOne relationship or disconnecting the last node from a OneOrMore relationship. CardinalityViolation occurs when accessing a relationship that violates its constraint, such as calling all() on a One relationship with no targets. Both exceptions are defined in neomodel/exceptions.py.
How do I replace a node in a ZeroOrOne or One relationship?
Use the reconnect(old_node, new_node) method provided by the cardinality managers. This atomically disconnects the existing node and connects the new one, maintaining the cardinality constraint throughout the operation. For example, employee.manager.reconnect(old_manager, new_manager) replaces the manager while ensuring the relationship never violates the ZeroOrOne or One constraint.
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 →