How to use pre/post hooks in Services
Execute business logic, compute fields, coordinate side-effects, and handle soft-delete restoration across mutation lifecycles.
ZCore's BaseService divides database operations into three structured layers: Pre-hooks (intercept/merge payload), On-methods (core repository execution), and Post-hooks (side-effects, external notifications & cleanup).
1. Using pre_* and post_* Hooks
Return a dict from any pre_* mutation hook to automatically merge that data into the database payload:
# services.py
from typing import Any
from zcore import BaseService
from zcore.utils.helpers import slugify
from .models import Task
from .schemas import TaskCreate, TaskUpdate
from .repositories import TaskRepo
class TaskService(BaseService[Task]):
def __init__(self, repo: TaskRepo):
super().__init__(model=Task, repository=repo)
async def pre_create(self, schema: TaskCreate) -> dict[str, Any] | None:
# Merges automatically into the insert payload
return {"slug": slugify(schema.title)}
async def post_delete(self, model: Task, force: bool = False) -> None:
# Executes after deletion (e.g., delete files from storage if hard-deleted)
if force and getattr(model, "file_path", None):
# await storage.delete(model.file_path)
pass
async def post_restore(self, model: Task) -> None:
# Executes after a soft-deleted task is successfully restored
# logger.info(f"Task {model.id} restored")
pass2. Overriding Core Execution (on_* Methods)
If you need to change how an entity is persisted in the repository (e.g., routing to a custom repository method or decorating execution), override the on_* methods directly:
class TaskService(BaseService[Task]):
def __init__(self, repo: TaskRepo):
super().__init__(model=Task, repository=repo)
async def on_create(self, schema: TaskCreate, **extra_data) -> Task:
# Custom persistence delegation (surrounded safely by pre_create and post_create)
return await self.repository.create(schema, **extra_data)
async def on_delete(self, target: Task | Any, force: bool = False) -> Task | None:
# Core delete delegation (delegates soft vs hard delete to repository)
return await self.repository.delete(target, force=force)3. Complete Lifecycle Hook Reference
| Hook | Category | Return Type | Purpose / Description |
|---|---|---|---|
post_get(model) | Read (Single) | Model | Modify or append computed runtime fields after fetching. |
post_get_multi(models) | Read (Batch) | Sequence[Model] | Intercept and process a batch list of retrieved entities. |
pre_create(schema) | Create (Single) | dict | None | Compute/merge data (e.g., slugs, hashes) before insert. |
post_create(model) | Create (Single) | None | Trigger post-insert events (e.g., send emails, webhooks). |
pre_create_multi(schemas) | Create (Batch) | None | Pre-validation check before inserting multiple records. |
post_create_multi(models) | Create (Batch) | None | Run notifications or logging after bulk insertions. |
pre_update(target, schema, partial) | Update (Single) | dict | None | Compute/merge data into the update payload. |
post_update(model) | Update (Single) | None | Clear cache keys or log modification audit trails. |
pre_update_multi(data, partial) | Update (Batch) | None | Intercept batch dictionary updates before execution. |
post_update_multi(models) | Update (Batch) | None | Perform side-effects after bulk updates finish. |
pre_delete(id, force=False) | Delete (Single) | None | Verify dependencies or cascade rules before row removal. |
post_delete(model, force=False) | Delete (Single) | None | Purge associated files from storage after deletion. |
pre_delete_multi(ids, force=False) | Delete (Batch) | None | Validate or check permissions for a list of target IDs. |
post_delete_multi(models, force=False) | Delete (Batch) | None | Batch cleanup of external resources after deletions. |
pre_restore(id) | Restore (Single) | None | Pre-validation checks before restoring a soft-deleted record. |
post_restore(model) | Restore (Single) | None | Trigger side-effects or logging after a record is restored. |
pre_restore_multi(ids) | Restore (Batch) | None | Validate or check permissions before restoring batch records. |
post_restore_multi(models) | Restore (Batch) | None | Run notifications or audit logs after batch restorations. |
pre_search(search_in) | Search | None | Inspect or mutate SearchRequest filters dynamically. |
post_search(models) | Search | None | Post-process or enrich dynamic search query results. |
Atomic Transaction Safety:
All pre_*, on_*, and post_* operations execute within the active transaction before _safe_commit() is invoked. If any hook raises an error, the entire operation is automatically rolled back.
How to build complex search queries
Construct nested AND/OR/NOT filters, eager-load relations, use inverted/range operators, and execute secure dynamic searches.
How to use UnitOfWork for atomic transactions
Coordinate multi-repository writes and post-commit domain events within an atomic all-or-nothing boundary.