Saltar a contenido

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.find(Modelo, id, includes=None)                    # → instancia | None
t.find_many(Modelo, limit=None, offset=None,
            order_by=None, order='ASC',
            includes=None, **filtros)                # → List[instancia]
t.count(Modelo, **filtros)                           # → int
t.exists(Modelo, **filtros)                          # → bool
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
t.sum(Modelo, agg_fields, **filtros)
t.mean(Modelo, agg_fields, **filtros)
t.max(Modelo, agg_fields, **filtros)
t.min(Modelo, agg_fields, **filtros)
t.agg(Modelo, request, **filtros)
t.log('mensaje', level='info')     # al logger del cliente generado
t.abort('motivo')                  # lanza excepción y deshace la transacción

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.