Módosítás alatt álló táblák

Vannak bizonyos megszorítások arra vonatkozóan, hogy egy trigger törzsében mely táblák mely oszlopai érhetők el. Egy DML utasítás által éppen változtatott táblát módosítás alatt álló táblának hívunk. A DML utasításhoz rendelt trigger ezen a táblán van definiálva. Az adott táblához a DELETE CASCADE hivatkozási integritási megszorítás által hozzárendelt táblákat is módosítás alatt álló tábláknak tekintjük. Azokat a táblákat, amelyeket egy adott hivatkozási integritási megszorítás ellenőrzéséhez olvasni kell, megszorítással kapcsolt táblának hívjuk.

A trigger törzsében elhelyezett SQL utasítás nem olvashatja és módosíthatja a triggert aktivizáló utasítás által éppen módosítás alatt álló táblákat.

Ezek a megszorítások sor szintű triggerekre mindig érvényesek, utasítás szintűekre csak akkor, ha a trigger egy DELETE CASCADE eredményeként aktivizálódott.

Az INSERT INTO … SELECT utasítás kivételével az INSERT utasítás abban az esetben, ha csak egyetlen sort szúr be, a bővítendő táblát nem tekinti módosítás alatt állónak.

A következő példában egy fát tárolunk hierarchikusan az adatbázisban. Minden elem tartalmazza a szülő elem azonosítóját. A gyökér elemeket tartalmazó sorokban a szulo oszlop értéke NULL.

Példa

CREATE TABLE fa (
  id NUMBER PRIMARY KEY,
  szulo NUMBER,
  adat NUMBER,
  CONSTRAINT fa_fk FOREIGN KEY (szulo)
  REFERENCES fa(id)
);

INSERT INTO fa VALUES (1, NULL, 10);
INSERT INTO fa VALUES (2, 1, 20);
INSERT INTO fa VALUES (3, 2, 30);
INSERT INTO fa VALUES (4, 2, 40);
INSERT INTO fa VALUES (5, 3, 50);
INSERT INTO fa VALUES (6, 1, 60);
INSERT INTO fa VALUES (7, 1, 70);
INSERT INTO fa VALUES (8, NULL, 80);
INSERT INTO fa VALUES (9, 8, 90);

SELECT LPAD(' ', (LEVEL-1)*3, '| ') || '+--'
    || '(' || id || ', ' || adat || ')' AS elem
FROM fa
CONNECT BY PRIOR id = szulo
START WITH szulo IS NULL;

/*
ELEM
---------------------
+--(1, 10)
| +--(2, 20)
| | +--(3, 30)
| | | +--(5, 50)
| | +--(4, 40)
| +--(6, 60)
| +--(7, 70)
+--(8, 80)
| +--(9, 90)
9 sor kijelölve.
*/

Készítsünk triggert, ami lehetővé teszi DML segítségével elemek törlését a fából úgy, hogy minden törölt elem gyerekeinek szülőjét átállítja a törölt elem szülőjére. Ha a DML azt a szülőt is törli, akkor annak a szülőjére stb.

A következő trigger logikus megoldás lenne, használata mégsem megengedett, mert egy módosítás alatt álló táblát nem módosíthatunk.

CREATE OR REPLACE TRIGGER tr_fa
  BEFORE DELETE ON fa
  FOR EACH ROW
BEGIN
  /* Ezt kellene csinálni, ha lehetne: */
  UPDATE fa SET szulo = :OLD.szulo
  WHERE szulo = :OLD.id;
END;
/
show errors

DELETE FROM fa WHERE id = 2;

/*
Hiba a(z) 1. sorban:
DELETE FROM fa WHERE id = 2;
*
Hiba a(z) 1. sorban:
ORA-04091: PLSQL.FA tábla változtatás alatt áll, trigger/funkció számára nem látható
ORA-06512: a(z) "PLSQL.TR_FA", helyen a(z) 3. sornál
ORA-04088: hiba a(z) 'PLSQL.TR_FA' trigger futása közben
*/
/* oracle 12 verzió ORA-02292: integritás megszorítás (PLSQL.FA_FK) megsértés - gyermek rekord található meg */

A megoldás megkerüli ezt a megszorítást:

/* Egy lehetséges megoldás.
Úgy törlünk, hogy az azonosítót -1-re módosítjuk,
triggereken keresztül pedig elvégezzük a tényleges törlést.
Szükségünk van egy temporális táblára,
ezt egy csomagban tároljuk majd.
Első menetben ebben összegyűjtjük a törlendő elemeket.
Második menetben módosítjuk a gyerekeket,
aztán végül elvégezzük a tényleges törlést. */
CREATE OR REPLACE PACKAGE pkg_fa IS
  TYPE t_torlendo IS TABLE OF fa%ROWTYPE INDEX BY BINARY_INTEGER;
  v_Torlendo t_torlendo;
  v_Torles BOOLEAN := FALSE;
  /* Megadja egy törlésre kerülő elem végső szülőjét,
   hiszen az ő szülőjét is lehet, hogy törlik. */
  FUNCTION szulo_torles_utan(p_Id NUMBER) RETURN NUMBER;
END pkg_fa;
/

CREATE OR REPLACE PACKAGE BODY pkg_fa IS
  FUNCTION szulo_torles_utan(p_Id NUMBER) RETURN NUMBER IS
    v_Id NUMBER := p_Id;
    v_Szulo NUMBER;
  BEGIN
    WHILE v_Torlendo.EXISTS(v_Id) LOOP
      v_Id := v_Torlendo(v_Id).szulo;
    END LOOP;
    RETURN v_Id;
  END szulo_torles_utan;
END pkg_fa;
/

-- utasítás szintű BEFORE trigger inicializálja a csomagváltozót
CREATE OR REPLACE TRIGGER tr_fa1
  BEFORE UPDATE OF id ON fa
BEGIN
  IF NOT pkg_fa.v_Torles THEN
    pkg_fa.v_Torlendo.DELETE;
  END IF;
END tr_fa1;
/

-- sor szintű trigger tárolja a törlendő elemeket
CREATE OR REPLACE TRIGGER tr_fa
  BEFORE UPDATE OF id ON fa
  FOR EACH ROW
  WHEN (NEW.id = -1)
BEGIN
  IF NOT pkg_fa.v_Torles THEN
    pkg_fa.v_Torlendo(:OLD.id).id := :OLD.id;
    pkg_fa.v_Torlendo(:OLD.id).szulo := :OLD.szulo;
    pkg_fa.v_Torlendo(:OLD.id).adat := :OLD.adat;
    -- nem módosítunk, hogy az integritás rendben legyen
    :NEW.id := :OLD.id;
  END IF;
END tr_fa;
/
show errors

-- utasítás szintű AFTER trigger végzi el a munka oroszlánrészét
CREATE OR REPLACE TRIGGER tr_fa2
  AFTER UPDATE OF id ON fa
DECLARE
  v_Id NUMBER;
BEGIN
  IF NOT pkg_fa.v_Torles AND pkg_fa.v_Torlendo.COUNT > 0 THEN
    pkg_fa.v_Torles := TRUE;
    -- Gyerekek átállítása
    v_Id := pkg_fa.v_Torlendo.FIRST;
  WHILE v_Id IS NOT NULL LOOP
    UPDATE fa SET szulo = pkg_fa.szulo_torles_utan(v_Id)
    WHERE szulo = v_Id;
    v_Id := pkg_fa.v_Torlendo.NEXT(v_Id);
  END LOOP;
  -- Törlés
  v_Id := pkg_fa.v_Torlendo.FIRST;
  WHILE v_Id IS NOT NULL LOOP
    DELETE FROM fa
    WHERE id = v_Id;
    v_Id := pkg_fa.v_Torlendo.NEXT(v_Id);
  END LOOP;
  pkg_fa.v_Torles := FALSE;
  END IF;
END tr_fa2;
/

/*
Emlékeztetőül a fa:
ELEM
---------------------
+--(1, 10)
| +--(2, 20)
| | +--(3, 30)
| | | +--(5, 50)
| | +--(4, 40)
| +--(6, 60)
| +--(7, 70)
+--(8, 80)
| +--(9, 90)
*/

UPDATE fa SET id = -1
WHERE id IN (2, 3, 8);

/*
3 sor módosítva.
*/

SELECT LPAD(' ', (LEVEL-1)*3, '| ') || '+--'
  || '(' || id || ', ' || adat || ')' AS elem
FROM fa
CONNECT BY PRIOR id = szulo
START WITH szulo IS NULL;

/*
ELEM
--------------
+--(1, 10)
| +--(4, 40)
| +--(5, 50)
| +--(6, 60)
| +--(7, 70)
+--(9, 90)
6 sor kijelölve.
*/