Search code examples
sqltriggersoracle-xe

Cancel insertion inside a trigger without a rollback


For a certain assignment, I'm tasked with creating a very simple Q/A site, complete with tagged questions and commented answers.

One of the requirements is to have a trigger to detect the insertion of duplicate tags and, if such is the case, increment the already-existing tag's usage counter.
Problem is, I can't cancel the trigger with rolling back the entire transaction, including the UPDATE, which defeats the purpose of the trigger.

Any ideas on how to pull it off?

UPDATE

The requirement's phrased like follows:

"Create the trigger that checks if any tag to be added doesn't previously exist in the database. In case it exists, the use column in the corresponding row must be incremented by 1"
(Original language: "Crear el trigger tg_insertar_tag que revise que cualquier nuevo tag que se agregue no exista antes en la base de datos; en caso de existir, se debe incrementar en 1 la columna “usos” de la tabla tag del registro que corresponda")

This requirement can't be changed or avoided, although loopholes would be welcome.


For reference, my current trigger code:

CREATE OR REPLACE TRIGGER tg_insertar_tag BEFORE INSERT ON Tag
FOR EACH ROW
DECLARE
    tagCount integer;
    v_usos integer;
BEGIN
    SELECT COUNT(*) INTO tagCount FROM Tag WHERE nombre = :new.nombre;
    SELECT Usos INTO v_usos FROM Tag WHERE nombre = :new.nombre;
    IF tagCount > 0 THEN
        UPDATE Tag SET usos = v_usos + 1 WHERE nombre = :new.nombre;
    ELSE
        :new.usos := 1;
    END IF;
END;

Solution

  • That's not what the triggers on a table are for.

    You should use a MERGE statement from a stored procedure, an INSTEAD OF trigger or just from the client:

    MERGE
    INTO    tag tc
    USING   (
            SELECT  :NEW.nombre
            FROM    dual
            ) t
    ON      (tc.nombre = t.nombre)
    WHEN MATCHED THEN
    UPDATE
    SET     usos = usos + 1
    WHEN NOT MATCHED THEN
    INSERT  (nombre, usos)
    VALUES  (nombre, 1)
    

    Performance-wise, it would be best to pass all tags in a collection from the client and perform this query:

    MERGE
    INTO    tag tc
    USING   (
            SELECT  tag, COUNT(*) AS cnt
            FROM    TABLE(:mycollection)
            GROUP BY
                    nombre
            ) t
    ON      (tc.nombre = t.nombre)
    WHEN MATCHED THEN
    UPDATE
    SET     usos = usos + cnt
    WHEN NOT MATCHED THEN
    INSERT  (nombre, usos)
    VALUES  (nombre, cnt)
    

    in the stored procedure which would accept the collection as a parameter.