How to Use Pre-Save and Post-Delete Hooks on a StructuredNode in Neomodel
Use pre_save, post_save, pre_delete, and post_delete methods on your StructuredNode subclass—the @hooks decorator in neomodel automatically invokes them before and after persistence operations.
The neo4j-contrib/neomodel library provides a built-in lifecycle hook system that lets you execute custom logic when nodes are saved to or removed from Neo4j. By implementing specific method signatures on a StructuredNode, you can run validation, auditing, or cascading operations without overriding core persistence logic.
Understanding the Hook Lifecycle in Neomodel
neomodel supports six distinct lifecycle hooks that map to the core persistence operations in StructuredNode. The hook mechanism is implemented via the @hooks decorator located in neomodel/hooks.py, which wraps the save() and delete() methods defined in neomodel/sync_/node.py.
When you define a method matching one of the supported patterns, the decorator automatically invokes it at the appropriate time:
| Operation | Hook Method | Execution Timing |
|---|---|---|
save() |
pre_save |
Immediately before the node is persisted or updated |
save() |
post_save |
Immediately after the node is written to the database |
delete() |
pre_delete |
Immediately before the node is removed from the graph |
delete() |
post_delete |
Immediately after the node is detached and marked deleted |
create() |
post_create |
After a new node is created via create() (internally used by save) |
create() |
pre_create |
Rarely used; only if calling create() directly |
How the Hook Mechanism Works Internally
The @hooks Decorator Implementation
The @hooks decorator in neomodel/hooks.py inspects the wrapped function name and constructs the corresponding pre- and post-hook method names. It then calls _exec_hook to check if the instance implements these methods and executes them if present:
# neomodel/hooks.py
def _exec_hook(hook_name: str, self: Any) -> None:
if hasattr(self, hook_name):
getattr(self, hook_name)()
Integration with StructuredNode Methods
Both save() and delete() in neomodel/sync_/node.py are decorated with @hooks:
# neomodel/sync_/node.py
@hooks
def save(self) -> "StructuredNode":
# Core persistence logic
...
@hooks
def delete(self) -> bool:
# Core deletion logic
...
When you call instance.save(), the decorator executes pre_save → core save logic → post_save. Similarly, instance.delete() triggers pre_delete → core delete logic → post_delete.
Implementing Pre-Save and Post-Delete Hooks
Basic Hook Implementation
Define methods with the exact names pre_save, post_save, pre_delete, or post_delete on your StructuredNode subclass:
from neomodel import StructuredNode, StringProperty
class Person(StructuredNode):
name = StringProperty()
def pre_save(self):
print(f"[pre_save] About to save {self}")
def post_save(self):
print(f"[post_save] Saved node with id {self.element_id}")
def pre_delete(self):
print(f"[pre_delete] Deleting node {self}")
def post_delete(self):
print("[post_delete] Node removed")
# Usage
bob = Person(name="Bob").save() # → triggers pre_save → post_save
bob.delete() # → triggers pre_delete → post_delete
Validation with Pre-Save Hooks
Use pre_save to enforce business rules before persistence occurs. Raising an exception prevents the save operation:
class Product(StructuredNode):
sku = StringProperty(unique_index=True)
name = StringProperty()
def pre_save(self):
if not self.sku:
raise ValueError("SKU must be set before saving")
If sku is missing, save() raises ValueError before executing any Cypher queries.
Auditing with Post-Delete Hooks
Implement post_delete to create audit trails or trigger cleanup operations after removal:
class AuditLog(StructuredNode):
action = StringProperty()
node_id = StringProperty()
class Order(StructuredNode):
number = StringProperty()
def post_delete(self):
AuditLog(action="order_deleted", node_id=self.element_id).save()
When an Order is deleted, post_delete automatically creates an AuditLog entry.
Testing Hook Behavior
The official test suite in test/sync_/test_hooks.py demonstrates the complete hook execution flow:
# test/sync_/test_hooks.py
class HookTest(StructuredNode):
name = StringProperty()
def post_create(self):
HOOKS_CALLED["post_create"] = 1
def pre_save(self):
HOOKS_CALLED["pre_save"] = 1
def post_save(self):
HOOKS_CALLED["post_save"] = 1
def pre_delete(self):
HOOKS_CALLED["pre_delete"] = 1
def post_delete(self):
HOOKS_CALLED["post_delete"] = 1
ht = HookTest(name="k").save() # → pre_save, post_save, post_create
ht.delete() # → pre_delete, post_delete
assert "pre_save" in HOOKS_CALLED
assert "post_save" in HOOKS_CALLED
assert "post_create" in HOOKS_CALLED
assert "pre_delete" in HOOKS_CALLED
assert "post_delete" in HOOKS_CALLED
The async implementation follows the same pattern; see test/async_/test_hooks.py for AsyncStructuredNode examples.
Summary
- Hook mechanism: The
@hooksdecorator inneomodel/hooks.pyautomatically triggerspre_save,post_save,pre_delete, andpost_deletemethods on yourStructuredNodesubclass. - Implementation: Define methods with exact names matching the hook pattern; no registration or configuration required.
- Execution order:
pre_saveruns before persistence,post_saveafter;pre_deleteruns before removal,post_deleteafter detachment. - Use cases: Validation (raise exceptions in
pre_save), auditing (create logs inpost_delete), cascading updates, and side-effect management. - Source locations: Core logic resides in
neomodel/hooks.pyandneomodel/sync_/node.py, with comprehensive tests intest/sync_/test_hooks.py.
Frequently Asked Questions
What is the difference between pre_save and post_create hooks?
The pre_save hook runs before every save operation, including both creating new nodes and updating existing ones. The post_create hook only fires after a new node is created via the create() method, which is called internally during the initial save. Use pre_save for validation that applies to both inserts and updates, and post_create for initialization logic specific to new instances.
Can I raise exceptions in pre_save hooks to prevent saving?
Yes. Because pre_save executes before any Cypher queries are sent to Neo4j, raising an exception inside the method aborts the save operation entirely. The database remains unchanged, and the exception propagates to your application code. This pattern is ideal for enforcing business rules or validating required fields before persistence.
Do hooks work with async StructuredNode in neomodel?
Yes. The hook mechanism works identically for AsyncStructuredNode in the async API. The @hooks decorator is applied to the async versions of save() and delete() in neomodel/async_/node.py. You define pre_save, post_save, pre_delete, and post_delete methods exactly as you would in the sync version, and they will be awaited appropriately during the async lifecycle.
How do I implement cascading deletes using post_delete hooks?
Implement the post_delete method on your parent node to manually remove related nodes or perform cleanup operations. Because post_delete runs after the node has been detached from the graph and marked as deleted, you can safely access related nodes or properties to determine what additional cleanup is required. For example, you can query for related children and delete them, or create audit records referencing the deleted node's ID before it becomes unavailable.
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 →