Disparadors (Triggers)
Què és un trigger?
Un trigger (disparador) és un objecte de la base de dades que s'executa automàticament en resposta a un esdeveniment concret sobre una taula o vista: una inserció (INSERT), una modificació (UPDATE) o una eliminació (DELETE). A diferència dels procediments, no es criden explícitament; el SGBD els invoca de manera transparent sempre que es produeix l'esdeveniment que els ha activat.
Els triggers permeten implementar lògica que ha de ser garantida independentment de quin client o aplicació executi l'operació. No importa si la modificació arriba des d'una aplicació web, d'una importació massiva de dades o d'un DBA treballant des de la consola: el trigger s'executa sempre.
sequenceDiagram
participant APP as Aplicació
participant SGBD as SGBD
participant T as Trigger
participant Tbl as Taula principal
participant Audit as Taula d'auditoria
APP->>SGBD: UPDATE productes SET preu=150 WHERE id=7
SGBD->>T: Activa trigger AFTER UPDATE
T->>Audit: INSERT INTO audit_productes (...)
T->>SGBD: Trigger completat
SGBD->>Tbl: Aplica el UPDATE
SGBD->>APP: Operació completada
Tipus de triggers
Per moment d'execució: BEFORE i AFTER
-
BEFORE: s'executa abans que l'operació modifiqui les dades. Ideal per a:
- Validar o modificar els valors nous (
NEW) abans que s'escriguin. - Rebutjar l'operació si no compleix les regles de negoci.
- Calcular valors derivats que han de formar part del registre inserit.
- Validar o modificar els valors nous (
-
AFTER: s'executa després que l'operació hagi modificat les dades. Ideal per a:
- Registrar l'operació en una taula d'auditoria.
- Actualitzar taules relacionades (totals, estoc...).
- Enviar notificacions o disparar altres processos.
Per granularitat: FOR EACH ROW i FOR EACH STATEMENT
-
FOR EACH ROW: el trigger s'executa una vegada per cada fila afectada per l'operació. Té accés a
OLDiNEW, els valors de la fila concreta. (Tots els motors el suporten.) -
FOR EACH STATEMENT: el trigger s'executa una vegada per tota l'operació, independentment de quantes files s'hagin modificat. No té accés als valors individuals de cada fila. (PostgreSQL i SQL Server.)
Resum de combinacions
graph TD
TR[Trigger]
TR --> MOMENT{Moment}
TR --> GRAN{Granularitat}
MOMENT --> BEF[BEFORE]
MOMENT --> AFT[AFTER]
MOMENT --> INS[INSTEAD OF\nSQL Server / Oracle / PostgreSQL vistes]
GRAN --> ROW[FOR EACH ROW\nTots els motors]
GRAN --> STMT[FOR EACH STATEMENT\nPostgreSQL, SQL Server]
BEF --> BEF_US[Validació, modificació\ndels valors nous]
AFT --> AFT_US[Auditoria, actualització\nde taules relacionades]
INS --> INS_US[Operacions sobre vistes\nnot updatable]
Creació de triggers
Sintaxi bàsica
En PostgreSQL, els triggers criden una funció de trigger separada. En els altres motors, el codi va directament dins del CREATE TRIGGER.
-- Pas 1: Crear la funció de trigger (retorna TRIGGER)
CREATE OR REPLACE FUNCTION trg_fn_exemple()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
-- Lògica del trigger aquí
-- NEW conté els nous valors (INSERT / UPDATE)
-- OLD conté els valors anteriors (UPDATE / DELETE)
RETURN NEW; -- Obligatori per a BEFORE (retorna la fila a escriure)
-- Per a AFTER: RETURN NULL (s'ignora)
END;
$$;
-- Pas 2: Crear el trigger i associar-lo a la taula i funció
CREATE OR REPLACE TRIGGER trg_exemple
BEFORE INSERT OR UPDATE ON productes
FOR EACH ROW
EXECUTE FUNCTION trg_fn_exemple();
-- En SQL Server, el codi va dins del CREATE TRIGGER
-- Les taules especials 'inserted' i 'deleted' contenen les files afectades
CREATE OR ALTER TRIGGER trg_exemple
ON productes
AFTER INSERT, UPDATE
AS
BEGIN
SET NOCOUNT ON;
-- 'inserted' conté les noves files (INSERT / UPDATE)
-- 'deleted' conté les files anteriors (UPDATE / DELETE)
SELECT * FROM inserted; -- exemple
END;
GO
-- Oracle: el codi va dins del CREATE TRIGGER
-- :NEW i :OLD (amb dos punts) accedeixen als valors
CREATE OR REPLACE TRIGGER trg_exemple
BEFORE INSERT OR UPDATE ON productes
FOR EACH ROW
BEGIN
-- :NEW.columna per als nous valors
-- :OLD.columna per als valors anteriors
:NEW.nom := UPPER(:NEW.nom);
END trg_exemple;
/
Accés als valors OLD i NEW
-- NEW: valors nous (INSERT i UPDATE)
-- OLD: valors anteriors (UPDATE i DELETE)
-- En un INSERT: OLD no existeix (és NULL)
-- En un DELETE: NEW no existeix (és NULL)
CREATE OR REPLACE FUNCTION trg_fn_mostra_canvis()
RETURNS TRIGGER AS $$
BEGIN
IF TG_OP = 'INSERT' THEN
RAISE NOTICE 'INSERT: nou preu = %', NEW.preu;
ELSIF TG_OP = 'UPDATE' THEN
RAISE NOTICE 'UPDATE: preu % → %', OLD.preu, NEW.preu;
ELSIF TG_OP = 'DELETE' THEN
RAISE NOTICE 'DELETE: preu eliminat = %', OLD.preu;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- NEW.columna: valor nou (INSERT / UPDATE)
-- OLD.columna: valor anterior (UPDATE / DELETE)
DELIMITER //
CREATE TRIGGER trg_mostra_canvis
AFTER UPDATE ON productes
FOR EACH ROW
BEGIN
IF OLD.preu <> NEW.preu THEN
INSERT INTO log_preus(producte_id, preu_anterior, preu_nou, data_canvi)
VALUES (OLD.id, OLD.preu, NEW.preu, NOW());
END IF;
END //
DELIMITER ;
-- 'inserted': taula virtual amb les files noves (INSERT / UPDATE)
-- 'deleted': taula virtual amb les files anteriors (UPDATE / DELETE)
-- Poden contenir MÚLTIPLES files si l'operació afecta N files alhora
CREATE OR ALTER TRIGGER trg_mostra_canvis
ON productes
AFTER UPDATE
AS
BEGIN
SET NOCOUNT ON;
INSERT INTO log_preus(producte_id, preu_anterior, preu_nou, data_canvi)
SELECT
d.id,
d.preu,
i.preu,
GETDATE()
FROM deleted d
JOIN inserted i ON i.id = d.id
WHERE d.preu <> i.preu;
END;
GO
-- :NEW.columna: valor nou (INSERT / UPDATE)
-- :OLD.columna: valor anterior (UPDATE / DELETE)
-- Nota: :NEW és NULL en DELETE, :OLD és NULL en INSERT
CREATE OR REPLACE TRIGGER trg_mostra_canvis
AFTER UPDATE ON productes
FOR EACH ROW
WHEN (OLD.preu <> NEW.preu) -- Condició WHEN (sense els dos punts en WHEN)
BEGIN
INSERT INTO log_preus(producte_id, preu_anterior, preu_nou, data_canvi)
VALUES (:OLD.id, :OLD.preu, :NEW.preu, SYSDATE);
END trg_mostra_canvis;
/
Exemple 1: Trigger d'auditoria
Volem registrar tots els canvis (INSERT, UPDATE, DELETE) sobre la taula productes en una taula d'auditoria audit_productes.
Creació de la taula d'auditoria
-- Estructura comuna (adaptar tipus de dades al motor)
CREATE TABLE audit_productes (
id SERIAL PRIMARY KEY, -- AUTO_INCREMENT en MySQL; IDENTITY en SQL Server
operacio VARCHAR(10), -- 'INSERT', 'UPDATE', 'DELETE'
producte_id INT,
nom_anterior VARCHAR(200),
nom_nou VARCHAR(200),
preu_anterior DECIMAL(10,2),
preu_nou DECIMAL(10,2),
usuari VARCHAR(100),
data_hora TIMESTAMP
);
Trigger d'auditoria
CREATE OR REPLACE FUNCTION trg_fn_audit_productes()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
INSERT INTO audit_productes (
operacio, producte_id,
nom_anterior, nom_nou,
preu_anterior, preu_nou,
usuari, data_hora
)
VALUES (
TG_OP,
COALESCE(NEW.id, OLD.id),
OLD.nom, NEW.nom,
OLD.preu, NEW.preu,
current_user, NOW()
);
RETURN NEW;
END;
$$;
CREATE OR REPLACE TRIGGER trg_audit_productes
AFTER INSERT OR UPDATE OR DELETE ON productes
FOR EACH ROW
EXECUTE FUNCTION trg_fn_audit_productes();
-- MySQL necessita un trigger separat per cada operació DML
DELIMITER //
CREATE TRIGGER trg_audit_productes_insert
AFTER INSERT ON productes
FOR EACH ROW
BEGIN
INSERT INTO audit_productes (
operacio, producte_id, nom_nou, preu_nou, usuari, data_hora
) VALUES (
'INSERT', NEW.id, NEW.nom, NEW.preu, USER(), NOW()
);
END //
CREATE TRIGGER trg_audit_productes_update
AFTER UPDATE ON productes
FOR EACH ROW
BEGIN
INSERT INTO audit_productes (
operacio, producte_id,
nom_anterior, nom_nou,
preu_anterior, preu_nou,
usuari, data_hora
) VALUES (
'UPDATE', OLD.id,
OLD.nom, NEW.nom,
OLD.preu, NEW.preu,
USER(), NOW()
);
END //
CREATE TRIGGER trg_audit_productes_delete
AFTER DELETE ON productes
FOR EACH ROW
BEGIN
INSERT INTO audit_productes (
operacio, producte_id, nom_anterior, preu_anterior, usuari, data_hora
) VALUES (
'DELETE', OLD.id, OLD.nom, OLD.preu, USER(), NOW()
);
END //
DELIMITER ;
CREATE OR ALTER TRIGGER trg_audit_productes
ON productes
AFTER INSERT, UPDATE, DELETE
AS
BEGIN
SET NOCOUNT ON;
-- Determinar l'operació
DECLARE @op NVARCHAR(10);
IF EXISTS (SELECT 1 FROM inserted) AND EXISTS (SELECT 1 FROM deleted)
SET @op = 'UPDATE';
ELSE IF EXISTS (SELECT 1 FROM inserted)
SET @op = 'INSERT';
ELSE
SET @op = 'DELETE';
-- Insertar un registre per cada fila afectada
INSERT INTO audit_productes (
operacio, producte_id,
nom_anterior, nom_nou,
preu_anterior, preu_nou,
usuari, data_hora
)
SELECT
@op,
COALESCE(i.id, d.id),
d.nom, i.nom,
d.preu, i.preu,
SYSTEM_USER, GETDATE()
FROM inserted i
FULL OUTER JOIN deleted d ON i.id = d.id;
END;
GO
CREATE OR REPLACE TRIGGER trg_audit_productes
AFTER INSERT OR UPDATE OR DELETE ON productes
FOR EACH ROW
BEGIN
INSERT INTO audit_productes (
operacio, producte_id,
nom_anterior, nom_nou,
preu_anterior, preu_nou,
usuari, data_hora
) VALUES (
CASE
WHEN INSERTING THEN 'INSERT'
WHEN UPDATING THEN 'UPDATE'
WHEN DELETING THEN 'DELETE'
END,
COALESCE(:NEW.id, :OLD.id),
:OLD.nom, :NEW.nom,
:OLD.preu, :NEW.preu,
SYS_CONTEXT('USERENV', 'SESSION_USER'),
SYSTIMESTAMP
);
END trg_audit_productes;
/
Exemple 2: Trigger de validació (prevenir preus negatius)
CREATE OR REPLACE FUNCTION trg_fn_valida_preu()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
IF NEW.preu < 0 THEN
RAISE EXCEPTION 'El preu no pot ser negatiu (valor: %)', NEW.preu
USING ERRCODE = 'check_violation';
END IF;
IF NEW.preu = 0 THEN
RAISE WARNING 'S''ha inserit un producte amb preu zero: %', NEW.nom;
END IF;
RETURN NEW;
END;
$$;
CREATE OR REPLACE TRIGGER trg_valida_preu
BEFORE INSERT OR UPDATE OF preu ON productes
FOR EACH ROW
EXECUTE FUNCTION trg_fn_valida_preu();
DELIMITER //
CREATE TRIGGER trg_valida_preu_insert
BEFORE INSERT ON productes
FOR EACH ROW
BEGIN
IF NEW.preu < 0 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'El preu no pot ser negatiu';
END IF;
END //
CREATE TRIGGER trg_valida_preu_update
BEFORE UPDATE ON productes
FOR EACH ROW
BEGIN
IF NEW.preu < 0 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'El preu no pot ser negatiu';
END IF;
END //
DELIMITER ;
CREATE OR ALTER TRIGGER trg_valida_preu
ON productes
INSTEAD OF INSERT, UPDATE
AS
BEGIN
SET NOCOUNT ON;
IF EXISTS (SELECT 1 FROM inserted WHERE preu < 0)
BEGIN
RAISERROR ('El preu no pot ser negatiu.', 16, 1);
ROLLBACK TRANSACTION;
RETURN;
END;
-- Si la validació passa, executar l'operació real
IF EXISTS (SELECT 1 FROM deleted)
-- Era un UPDATE
UPDATE p SET preu = i.preu, nom = i.nom
FROM productes p JOIN inserted i ON i.id = p.id;
ELSE
-- Era un INSERT
INSERT INTO productes SELECT * FROM inserted;
END;
GO
Exemple 3: Trigger de camp calculat (actualitzar total de comanda)
Cada vegada que s'insereix o modifica una línia de comanda, actualitzem automàticament el total de la comanda pare.
CREATE OR REPLACE FUNCTION trg_fn_actualitza_total_comanda()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
DECLARE
v_comanda_id INT;
BEGIN
-- Determinar quina comanda s'ha vist afectada
v_comanda_id := COALESCE(NEW.comanda_id, OLD.comanda_id);
-- Recalcular el total sumant totes les línies
UPDATE comandes
SET total = (
SELECT COALESCE(SUM(quantitat * preu_unitari), 0)
FROM linies_comanda
WHERE comanda_id = v_comanda_id
)
WHERE id = v_comanda_id;
RETURN NEW;
END;
$$;
CREATE OR REPLACE TRIGGER trg_actualitza_total
AFTER INSERT OR UPDATE OR DELETE ON linies_comanda
FOR EACH ROW
EXECUTE FUNCTION trg_fn_actualitza_total_comanda();
DELIMITER //
CREATE TRIGGER trg_actualitza_total_insert
AFTER INSERT ON linies_comanda
FOR EACH ROW
BEGIN
UPDATE comandes
SET total = (
SELECT COALESCE(SUM(quantitat * preu_unitari), 0)
FROM linies_comanda
WHERE comanda_id = NEW.comanda_id
)
WHERE id = NEW.comanda_id;
END //
CREATE TRIGGER trg_actualitza_total_update
AFTER UPDATE ON linies_comanda
FOR EACH ROW
BEGIN
UPDATE comandes
SET total = (
SELECT COALESCE(SUM(quantitat * preu_unitari), 0)
FROM linies_comanda
WHERE comanda_id = NEW.comanda_id
)
WHERE id = NEW.comanda_id;
END //
CREATE TRIGGER trg_actualitza_total_delete
AFTER DELETE ON linies_comanda
FOR EACH ROW
BEGIN
UPDATE comandes
SET total = (
SELECT COALESCE(SUM(quantitat * preu_unitari), 0)
FROM linies_comanda
WHERE comanda_id = OLD.comanda_id
)
WHERE id = OLD.comanda_id;
END //
DELIMITER ;
CREATE OR ALTER TRIGGER trg_actualitza_total
ON linies_comanda
AFTER INSERT, UPDATE, DELETE
AS
BEGIN
SET NOCOUNT ON;
-- Actualitzar totes les comandes afectades de cop
UPDATE c
SET c.total = COALESCE((
SELECT SUM(lc.quantitat * lc.preu_unitari)
FROM linies_comanda lc
WHERE lc.comanda_id = c.id
), 0)
FROM comandes c
WHERE c.id IN (
SELECT comanda_id FROM inserted
UNION
SELECT comanda_id FROM deleted
);
END;
GO
CREATE OR REPLACE TRIGGER trg_actualitza_total
AFTER INSERT OR UPDATE OR DELETE ON linies_comanda
FOR EACH ROW
DECLARE
v_comanda_id NUMBER;
BEGIN
v_comanda_id := COALESCE(:NEW.comanda_id, :OLD.comanda_id);
UPDATE comandes
SET total = (
SELECT NVL(SUM(quantitat * preu_unitari), 0)
FROM linies_comanda
WHERE comanda_id = v_comanda_id
)
WHERE id = v_comanda_id;
END trg_actualitza_total;
/
INSTEAD OF triggers (per a vistes)
Els triggers INSTEAD OF s'executen en lloc de l'operació original. Són especialment útils per fer que les vistes complexes (joins de múltiples taules, vistes amb agregats) siguin actualitzables.
-- PostgreSQL suporta INSTEAD OF en vistes
CREATE VIEW v_productes_complets AS
SELECT p.id, p.nom, p.preu, c.nom AS categoria
FROM productes p
JOIN categories c ON c.id = p.categoria_id;
CREATE OR REPLACE FUNCTION trg_fn_update_vista_productes()
RETURNS TRIGGER LANGUAGE plpgsql AS $$
BEGIN
UPDATE productes
SET nom = NEW.nom, preu = NEW.preu
WHERE id = NEW.id;
RETURN NEW;
END;
$$;
CREATE TRIGGER trg_upd_vista_productes
INSTEAD OF UPDATE ON v_productes_complets
FOR EACH ROW
EXECUTE FUNCTION trg_fn_update_vista_productes();
-- MySQL no suporta INSTEAD OF triggers directament.
-- Per fer vistes actualitzables, cal que la vista sigui 'simple'
-- (una sola taula, sense DISTINCT, GROUP BY, UNION, etc.)
-- o usar procediments emmagatzemats com a alternativa.
-- Alternativa: procediment que gestiona la lògica manualment
DELIMITER //
CREATE PROCEDURE actualitza_vista_producte(
IN p_id INT,
IN p_nom VARCHAR(200),
IN p_preu DECIMAL(10,2)
)
BEGIN
UPDATE productes SET nom = p_nom, preu = p_preu WHERE id = p_id;
END //
DELIMITER ;
-- SQL Server: INSTEAD OF molt usat per a vistes amb JOINs
CREATE VIEW v_productes_complets AS
SELECT p.id, p.nom, p.preu, c.nom AS categoria
FROM productes p
JOIN categories c ON c.id = p.categoria_id;
GO
CREATE OR ALTER TRIGGER trg_io_upd_productes
ON v_productes_complets
INSTEAD OF UPDATE
AS
BEGIN
SET NOCOUNT ON;
UPDATE p
SET p.nom = i.nom, p.preu = i.preu
FROM productes p
JOIN inserted i ON i.id = p.id;
END;
GO
-- Oracle: INSTEAD OF disponible per a vistes no actualitzables
CREATE OR REPLACE VIEW v_productes_complets AS
SELECT p.id, p.nom, p.preu, c.nom AS categoria
FROM productes p
JOIN categories c ON c.id = p.categoria_id;
/
CREATE OR REPLACE TRIGGER trg_io_upd_productes
INSTEAD OF UPDATE ON v_productes_complets
FOR EACH ROW
BEGIN
UPDATE productes
SET nom = :NEW.nom, preu = :NEW.preu
WHERE id = :OLD.id;
END trg_io_upd_productes;
/
Habilitar i deshabilitar triggers
-- Deshabilitar un trigger concret
ALTER TABLE productes DISABLE TRIGGER trg_audit_productes;
-- Deshabilitar tots els triggers d'una taula
ALTER TABLE productes DISABLE TRIGGER ALL;
-- Tornar a habilitar
ALTER TABLE productes ENABLE TRIGGER trg_audit_productes;
ALTER TABLE productes ENABLE TRIGGER ALL;
-- MySQL no té ALTER TRIGGER per habilitar/deshabilitar.
-- Cal eliminar-lo i tornar-lo a crear.
DROP TRIGGER IF EXISTS trg_audit_productes_update;
-- Alternativa: usar una variable de sessió com a flag
DELIMITER //
CREATE TRIGGER trg_audit_productes_update
AFTER UPDATE ON productes FOR EACH ROW
BEGIN
IF @disable_audit IS NULL OR @disable_audit = 0 THEN
INSERT INTO audit_productes (...) VALUES (...);
END IF;
END //
DELIMITER ;
-- Per deshabilitar temporalment:
SET @disable_audit = 1;
-- ... operacions ...
SET @disable_audit = 0;
Advertències importants
Rendiment dels triggers
Els triggers s'executen sincrònament dins de la transacció de l'operació que els ha disparat. Qualsevol temps que tardi el trigger s'afegeix al temps de resposta de l'operació original:
- Eviteu consultes costoses (sense índexs, fulls table scans) dins dels triggers.
- En operacions massives (imports de milers de files), considereu deshabilitar temporalment els triggers no crítics.
- Feu el trigger tan curt com sigui possible. Si la lògica és complexa, delegeu-la a un procediment emmagatzemat.
- Monitoritzeu el temps d'execució dels triggers en entorns de producció.
Bucles de triggers (trigger cascades)
Un trigger que modifica una taula pot disparar altres triggers sobre aquella taula, que al seu torn en disparin d'altres... i crear un bucle infinit o una cascada de canvis inesperats.
Exemple perillós:
-- Trigger A: AFTER UPDATE ON taula_1 → UPDATE taula_2
-- Trigger B: AFTER UPDATE ON taula_2 → UPDATE taula_1 ← BUCLE!
Com evitar-ho:
- Documenteu clarament quins triggers existeixen i quines taules toquen.
- Useu flags de sessió o context per detectar si ja esteu dins d'un trigger.
- PostgreSQL i SQL Server limiten la profunditat de la recursió de triggers.
- A SQL Server,
@@NESTLEVELindica el nivell d'imbricació actual. - Preferiu dissenys on els triggers no modifiquen les taules que els disparen.
Inspecció de triggers existents
-- Llistar triggers amb la taula associada
SELECT
t.name AS trigger_nom,
OBJECT_NAME(t.parent_id) AS taula,
t.type_desc,
t.is_disabled
FROM sys.triggers t
WHERE t.parent_class = 1; -- 1 = object/column (triggers de taula)
-- Veure el codi font
SELECT OBJECT_DEFINITION(OBJECT_ID('trg_audit_productes'));
Miniactivitat — AC0405 · AC0406
Miniactivitat — AC0405 · AC0406 · Triggers de gestió d'inventari
Teniu les taules:
productes(id, nom, preu, estoc, estoc_minim)comandes(id, client_id, data, estat, total)linies_comanda(id, comanda_id, producte_id, quantitat, preu_unitari)alertes_estoc(id, producte_id, estoc_actual, data_alerta, gestionada)
Tasca 1 — Trigger de validació d'estoc:
Creeu un trigger BEFORE INSERT sobre linies_comanda que comprovi que hi ha prou estoc del producte sol·licitat. Si l'estoc és insuficient, ha de cancel·lar la inserció amb un missatge d'error clar que indiqui el producte i l'estoc disponible.
Tasca 2 — Trigger d'actualització d'estoc:
Creeu un trigger AFTER INSERT sobre linies_comanda que descompti del camp estoc de productes la quantitat inserida.
Tasca 3 — Trigger d'alerta d'estoc mínim:
Creeu un trigger AFTER UPDATE OF estoc sobre productes que detecti quan l'estoc cau per sota de estoc_minim i insereixi un registre a alertes_estoc.
Tasca 4 — Prova de cascada: Comproveu que els triggers de les tasques 1, 2 i 3 no provoquen cap conflicte entre ells quan s'insereix una línia de comanda que porta l'estoc per sota del mínim.
Entrega: Arxiu SQL amb els triggers, les taules de prova, les dades de prova (INSERT) i les consultes de verificació que demostrin que els tres triggers funcionen conjuntament de manera correcta.