Triggers¶
Lógica de negocio declarada como métodos de la tabla. El generador los transpila e inlinea en los DAOs: no son triggers de la base de datos y no tienen coste en runtime.
class Post(Table):
__tablename__ = 'post'
id: col[bigint] = column(primary_key=True, autoincrement=True)
titulo: col[str]
estado: col[Estado] = column(default=Estado.BORRADOR)
publicado_en: col[datetime | None]
autor_id: col[int]
autor: manytoone[Usuario] = relation(
fields=['autor_id'], references=['id'], backref='posts'
)
@on_update(timing='before', fields=['estado'],
when=lambda t: t.new.estado == 'publicado')
def sellar_publicacion(self, t: TriggerAPI[Post]):
"""Al pasar a publicado, sella la fecha."""
t.new.publicado_en = datetime.now()
@on_create(timing='after')
def actualizar_autor(self, t: TriggerAPI[Post]):
"""Deja constancia en el autor de su último post."""
t.update(Usuario, self.autor_id, ultimo_post=datetime.now())
Los tres decoradores¶
@on_create(timing='before', priority=1, when=None)
@on_update(timing='before', priority=1, fields=None, when=None)
@on_delete(timing='before', priority=1, when=None)
| Parámetro | Qué hace |
|---|---|
timing |
'before' (antes de escribir; puede modificar t.new o abortar) o 'after' (después, con la fila ya persistida y su id asignado). |
priority |
Orden cuando hay varios triggers del mismo evento. Menor = primero. |
fields |
Solo en @on_update: lista de campos que activan el trigger. Sin ella, se ejecuta en cualquier update. |
when |
Condición. Recibe la TriggerAPI y devuelve bool; si es False, el trigger no se ejecuta. |
t.new y t.old¶
@on_create |
@on_update |
@on_delete |
|
|---|---|---|---|
t.new |
la fila que se va a crear | los valores nuevos | ❌ |
t.old |
❌ | los valores anteriores | la fila que se va a borrar |
Usar el lado que no existe es un error de importación
t.old en un @on_create, o t.new en un @on_delete, es un SyntaxError al importar el
schema. Es intencional: en esos eventos ese lado no existe, y descubrirlo en producción es
peor.
La API dentro del trigger¶
Todo lo que hace un trigger va en la misma transacción que la operación que lo disparó.
t.create(Modelo, **datos) # → instancia creada
t.create_many(Modelo, records) # → int
t.update(Modelo, id, **datos) # → int
t.update_many(Modelo, filters, **datos) # → int
t.upsert(Modelo, match_fields, **datos) # → instancia
t.upsert_many(Modelo, records, match_fields) # → int
t.delete(Modelo, id) # → int
t.delete_many(Modelo, **filtros) # → int
Validar y abortar¶
class Pedido(Table):
__tablename__ = 'pedido'
@on_create(timing='before')
def comprobar_stock(self, t: TriggerAPI[Pedido]):
producto = t.find(Producto, self.producto_id)
if producto is None or producto.stock < self.unidades:
t.abort('No hay stock suficiente')
t.abort() lanza una excepción: la operación no se aplica y la transacción se deshace entera.
Cascadas en la aplicación¶
class Post(Table):
@on_delete(timing='before')
def borrar_comentarios(self, t: TriggerAPI[Post]):
t.delete_many(Comment, post_id=t.old.id)
¿Trigger o ON DELETE CASCADE?
Si lo único que quieres es que se borren las filas hijas, declara onDelete='cascade' en la
relación: lo impone la base de datos y es más barato. Un trigger es para lo
que la base de datos no sabe hacer: escribir en otra tabla, recalcular un agregado, avisar.
Cómo afectan a las operaciones masivas¶
Una tabla con triggers no puede usar los caminos rápidos:
| Método | Sin triggers | Con triggers |
|---|---|---|
create_many |
inserción masiva | inserta y ejecuta los hooks fila a fila |
upsert_many |
INSERT ... ON CONFLICT nativo |
separa altas y modificaciones, y delega en create_many() y update() |
Es el precio de que los triggers se ejecuten siempre. Si una tabla recibe cargas masivas y su trigger no es imprescindible, piénsalo dos veces.
tai-sql pull no los puede reconstruir
Un trigger de tai-sql no existe en la base de datos: vive en el schema y en el cliente generado.