Ir al contenido

Eventos y observers

Los eventos permiten normalizar atributos, rechazar operaciones o reaccionar al ciclo de una entidad. Se registran por clase, con callbacks síncronos o asíncronos. Esta guía usa Document de scopes y su tabla migrada.

El catálogo público MODEL_EVENTS: tuple[str, ...], definido en orionis.orm.metaclass, contiene los once nombres de la tabla. Es el conjunto de eventos reconocidos; no implica que todos se emitan en una sola operación ni sustituye los flujos de creación, actualización, borrado y restauración.

EventoMomento¿Permite veto?
retrievedTras hidratar cada fila, antes de cargar relaciones eagerNo
savingAntes de distinguir INSERT/UPDATESí
creatingAntes del INSERTSí
createdDespués del INSERT y sincronización del estadoNo
updatingAntes de evaluar/escribir diferencias del modelo existenteSí
updatedDespués del intento UPDATE o camino sin cambiosNo
savedDespués de created/updatedNo
deletingAntes de borrar físicamente o estampar borrado lógicoSí
deletedDespués del intento de borradoNo
restoringAntes de limpiar marca de borradoSí
restoredDespués del intento de restauraciónNo
flowchart TD
    S["save()"] --> A["saving"]
    A --> N{"New instance?"}
    N -->|Yes| C["creating → INSERT → created"]
    N -->|No| U["updating → dirty UPDATE or no SQL → updated"]
    C --> E["saved"]
    U --> E
    A -. "False / exception" .-> X["Stop"]
    C -. "before-event False / exception" .-> X
    U -. "before-event False / exception" .-> X

El veto requiere exactamente False; None, 0 y una cadena vacía no son veto por identidad. Un veto previo evita SQL y los eventos posteriores. save, delete y restore devuelven False; create conserva su retorno de modelo incluso si no se insertó.

def registerEvent(cls, event: str, listener: Callable[..., Any]) -> type

Registra listener al final del evento, devuelve la clase y acepta función sync o async que recibe el modelo. Un evento desconocido produce OrmException. Registrar dos veces el mismo callable crea dos registros: no deduplica.

def require_title(document: Document):
return bool((document.title or "").strip())
Document.registerEvent("saving", require_title)

def observe(cls, observer: Any) -> type

Registra los métodos nombrados como eventos del objeto observer y devuelve la clase. Si pasas una clase, la instancia sin argumentos: no usa DI para construirla. Para dependencias, crea/resuelve el objeto tú y pasa la instancia. Métodos ausentes se omiten.

class DocumentObserver:
async def saving(self, document: Document):
document.title = (document.title or "").strip()
def deleting(self, document: Document):
return document.status != "locked"
Document.observe(DocumentObserver())

def flushEvents(cls, event: str | None=None) -> None

Elimina los listeners de event o todos si None; retorna None. Un nombre ausente no falla. Úsalo al aislar tests; cambia el registro de toda esa clase.

Document.flushEvents("saving")
Document.flushEvents()

async def fireEvent(self, event: str) -> bool

Despacha secuencialmente listeners en orden de registro, esperando resultados awaitable. Devuelve False al primer veto exacto False de un evento cancelable; en los demás casos True. No realiza SQL ni convierte un nombre desconocido en error: sin registro retorna True. Las excepciones se propagan y detienen el resto.

async def check_registered_policy(document: Document) -> bool:
return await document.fireEvent("saving")

Registra listeners una vez al arrancar, por ejemplo en el boot() de un provider. No vuelvas a registrarlos en cada acción HTTP. Las subclases copian listeners al crearse; los cambios posteriores del padre no reconstruyen el registro del hijo. Las identidades de registro evitan duplicación heredada por diamantes, pero dos registros explícitos del mismo callback se ejecutan dos veces.

Un save de una instancia existente sin cambios dispara saving → updating → updated → saved sin UPDATE. Un UPDATE que afecte cero filas devuelve False, pero los eventos updated y saved igualmente se emiten. Lo mismo ocurre con los eventos posteriores de borrado/restauración tras el intento. No interpretes un evento posterior como prueba independiente de filas afectadas.

Los listeners se ejecutan dentro de la llamada, sin colas automáticas ni fase afterCommit. created ocurre tras INSERT, antes del commit de una transacción exterior. Si un listener posterior falla fuera de una transacción, la escritura puede haber quedado persistida aunque la acción lance excepción. Dentro de una transacción, propaga la excepción para que el contexto pueda revertir; el objeto en memoria no se revierte.

Para correos, notificaciones u otros efectos externos, decide cuándo publicarlos después del commit y cómo reintentarlos. Las escrituras masivas (query().update/delete/restore/forceDelete, destroy) y las operaciones pivot no ejecutan eventos por entidad. Los métodos hasOne/hasMany.create sí llegan al create de modelo. No se implementa aquí una API separada de observers transaccionales.

Prueba por separado éxito, veto, excepción de listener, y rollback; consulta pruebas de ORM.