2008-12-17 2 views
6

Ich arbeite an einer großen, komplexen Anwendung vor kurzem damit begonnen, und ich habe nur einen Fehler aufgrund dieser Fehler zugewiesen wurde:ORA-04091: Tabelle [bla] mutiert, Trigger/Funktion kann nicht sehen, es

ORA-04091: table SCMA.TBL1 is mutating, trigger/function may not see it 
ORA-06512: at "SCMA.TRG_T1_TBL1_COL1", line 4 
ORA-04088: error during execution of trigger 'SCMA.TRG_T1_TBL1_COL1' 

Der Auslöser in Frage sieht aus wie

create or replace TRIGGER TRG_T1_TBL1_COL1 
    BEFORE INSERT OR UPDATE OF t1_appnt_evnt_id ON TBL1 
    FOR EACH ROW 
    WHEN (NEW.t1_prnt_t1_pk is not null) 
    DECLARE 
     v_reassign_count number(20); 
    BEGIN 
     select count(t1_pk) INTO v_reassign_count from TBL1 
       where t1_appnt_evnt_id=:new.t1_appnt_evnt_id and t1_prnt_t1_pk is not null; 
     IF (v_reassign_count > 0) THEN 
      RAISE_APPLICATION_ERROR(-20013, 'Multiple reassignments not allowed'); 
     END IF; 
    END; 

Die Tabelle einen Primärschlüssel „t1_pk“ hat, eine „Termin Ereignis-ID“ t1_appnt_evnt_id und eine weitere Spalte „t1_prnt_t1_pk“, die enthalten ein können oder nicht andere Reihe .

Es scheint der Auslöser sicher zu machen versucht, dass niemand sonst mit dem gleichen t1_appnt_evnt_id hat auf den gleichen verwiesen wurde diese Reihe zu bezieht sich ein Verweis auf eine andere Zeile, wenn dieser zu einer anderen Zeile bezieht.

Der Kommentar zum Fehlerbericht von der DBA sagt "Entfernen Sie den Trigger, und führen Sie die Überprüfung in den Code", aber leider haben sie eine proprietäre Code Generation Framework über Hibernate geschichtet, so dass ich nicht einmal zu sehen heraus, wo es tatsächlich ausgeschrieben wird, also hoffe ich, dass es einen Weg gibt, diesen Auslöser zum Laufen zu bringen. Ist da?

+2

Regeln wie diese nur im Code zu erzwingen ist eine schlechte Idee - mehrere gleichzeitige Updates sind schwer zu handhaben. Wenn Sie in Ihrem Code synchronisieren, können Sie mit schmutzigen Deadlocks zwischen diesen und Datenbanksperren enden. –

+0

Fazit - Oracle löst saugen. Vermeide sie wie die Pest, abgesehen von so einfachen Dingen wie dem Aktualisieren von Sequenzwerten oder Feldern vom Typ "updated_by". Ihre Auslöser saugt in den 90er Jahren und sie saugen jetzt. –

Antwort

7

Ich glaube, ich stimme nicht mit Ihrer Beschreibung von dem, was der Auslöser versucht, tun. Es sieht für mich so aus, als ob es diese Geschäftsregel erzwingen soll: Für einen gegebenen Wert von t1_appnt_event kann immer nur eine Zeile einen Nicht-Nullwert von t1_prnt_t1_pk haben. (Es spielt keine Rolle, ob sie den gleichen Wert in der zweiten Spalte haben oder nicht.)

Interessanterweise ist es für UPDATE von t1_appnt_event definiert, aber nicht für die andere Spalte, also denke ich, jemand könnte die Regel durch Aktualisierung brechen die zweite Spalte, es sei denn, es gibt einen separaten Trigger für diese Spalte.

Möglicherweise gibt es eine Möglichkeit, einen funktionsbasierten Index zu erstellen, der diese Regel erzwingt, damit Sie den Auslöser vollständig loswerden können. Ich kam mit einer Art und Weise, aber es erfordert einige Annahmen:

  • Die Tabelle hat einen numerischen Primärschlüssel
  • den Primärschlüssel und die t1_prnt_t1_pk sind beide immer positive Zahlen

Wenn diese Annahmen zutreffen

dev> create or replace function f(a number, b number) return number deterministic as 
    2 begin 
    3 if a is null then return 0-b; else return a; end if; 
    4 end; 

und einen Index wie folgt aus::, könnten Sie eine Funktion wie diese erstellen

CREATE UNIQUE INDEX my_index ON my_table 
    (t1_appnt_event, f(t1_prnt_t1_pk, primary_key_column)); 

So erscheinen Zeilen, in denen die PMNT-Spalte NULL ist, im Index mit der Umkehrung des Primärschlüssels als zweiter Wert, sodass sie niemals miteinander in Konflikt geraten würden. Zeilen, bei denen es nicht NULL ist, würden den tatsächlichen (positiven) Wert der Spalte verwenden. Die einzige Möglichkeit, eine Constraint-Verletzung zu erhalten, wäre, wenn zwei Zeilen in beiden Spalten dieselben Nicht-NULL-Werte hätten.

Dies ist vielleicht übermäßig "clever", aber es könnte Ihnen helfen, Ihr Problem zu umgehen.

Update von Paul Tomblin: Ich mit dem Update ging auf die ursprüngliche Idee, die in den Kommentaren stellen IGOR:

CREATE UNIQUE INDEX cappec_ccip_uniq_idx 
ON tbl1 (t1_appnt_event, 
    CASE WHEN t1_prnt_t1_pk IS NOT NULL THEN 1 ELSE t1_pk END); 
+0

Na gut. Du hattest meine Hoffnungen für einen Moment.:-) –

+0

Dieser Ansatz kann funktionieren, Sie müssen nur die Funktion nehmen zwei Parameter. Erstellen Sie einen eindeutigen Index für F (CAPPEC_APPE_ID, CAPPEC_CAPPEC_ID_PMNT). Wenn Sie NULL von der Funktion zurückgeben, wird sie nicht indiziert. Ansonsten gib CAPPEC_CAPPEC_ID_PMNT zurück –

+0

@WW - würde das nicht zwei Zeilen verhindern, die unterschiedliche ID-Werte aber denselben PMNT-Wert haben? –

0

ich mit Dave einig, dass die via Modem gewünschte Ergebnis und erreicht werden soll mit Hilfe der integrierten in Einschränkungen wie eindeutige Indizes (oder eindeutige Einschränkungen).

Wenn Sie wirklich den mutierenden Tabellenfehler umgehen müssen, ist die übliche Vorgehensweise, ein Paket zu erstellen, das eine paketorientierte Variable enthält, die eine Tabelle darstellt, mit der die geänderten Zeilen identifiziert werden können (Ich denke, ROWID ist möglich, sonst muss man das PK benutzen, ich benutze Oracle momentan nicht, also kann ich es nicht testen). Der Trigger FOR JEDER ZEILE füllt dann diese Variable mit allen Zeilen, die von der Anweisung geändert werden, und dann gibt es einen AFTER-Befehl für jede Anweisung, der die Zeilen liest und validiert.

So etwas wie (Syntax wahrscheinlich falsch ist, habe ich nicht mit Oracle für ein paar Jahre gearbeitet)

CREATE OR REPLACE PACKAGE trigger_pkg; 
    PROCEDURE before_stmt_trigger; 
    PROCEDURE for_each_row_trigger(row IN ROWID); 
    PROCEDURE after_stmt_trigger; 
END trigger_pkg; 

CREATE OR REPLACE PACKAGE BODY trigger_pkg AS 
    TYPE rowid_tbl IS TABLE OF(ROWID); 
    modified_rows rowid_tbl; 

    PROCEDURE before_stmt_trigger IS 
    BEGIN 
     modified_rows := rowid_tbl(); 
    END before_each_stmt_trigger; 

    PROCEDURE for_each_row_trigger(row IN ROWID) IS 
    BEGIN 
     modified_rows(modified_rows.COUNT) = row; 
    END for_each_row_trigger; 

    PROCEDURE after_stmt_trigger IS 
    BEGIN 
     FOR i IN 1 .. modified_rows.COUNT LOOP 
     SELECT ... INTO ... FROM the_table WHERE rowid = modified_rows(i); 
     -- do whatever you want to 
     END LOOP; 
    END after_each_stmt_trigger; 
END trigger_pkg; 

CREATE OR REPLACE TRIGGER before_stmt_trigger BEFORE INSERT OR UPDATE ON mytable AS 
BEGIN 
    trigger_pkg.before_stmt_trigger; 
END; 

CREATE OR REPLACE TRIGGER after_stmt_trigger AFTER INSERT OR UPDATE ON mytable AS 
BEGIN 
    trigger_pkg.after_stmt_trigger; 
END; 

CREATE OR REPLACE TRIGGER for_each_row_trigger 
BEFORE INSERT OR UPDATE ON mytable 
WHEN (new.mycolumn IS NOT NULL) AS 
BEGIN 
    trigger_pkg.for_each_row_trigger(:new.rowid); 
END; 
0

Mit jedem Trigger-basierten (oder Anwendungscode-basiert) Lösung, die Sie zu setzen müssen in Sperren, um Datenkorruption in einer Umgebung mit mehreren Benutzern zu verhindern. Auch wenn Ihr Auslöser funktionierte oder neu geschrieben wurde, um das Problem der mutierenden Tabelle zu vermeiden, würde dies nicht verhindern, dass 2 Benutzer t1_appnt_evnt_id gleichzeitig auf den gleichen Wert in Zeilen aktualisieren, bei denen t1_appnt_evnt_id nicht null ist: angenommen, dass derzeit keine Zeilen vorhanden sind wo t1_appnt_evnt_id = 123 und t1_prnt_t1_pk nicht null ist:

Session 1> update tbl1 
      set t1_appnt_evnt_id=123 
      where t1_prnt_t1_pk =456; 
      /* OK, trigger sees count of 0 */ 

Session 2> update tbl1 
      set t1_appnt_evnt_id=123 
      where t1_prnt_t1_pk =789; 
      /* OK, trigger sees count of 0 because 
       session 1 hasn't committed yet */ 

Session 1> commit; 

Session 2> commit; 

Sie haben nun eine beschädigte Datenbank!

Die Art und Weise dies (in Trigger oder Anwendungscode) zu vermeiden, würde die übergeordnete Zeile in der Tabelle von t1_appnt_evnt_id = 123 verwiesen sperren, bevor die Prüfung durchführt:

select appe_id 
into v_app_id 
from parent_table 
where appe_id = :new.t1_appnt_evnt_id 
for update;  

Jetzt Session 2 des Trigger muss warten für Sitzung 1 zum Festschreiben oder Zurücksetzen, bevor die Überprüfung durchgeführt wird.

Es wäre viel einfacher und sicherer Dave Costas Index zu implementieren!

Endlich bin ich froh, dass niemand vorgeschlagen hat, PRAGMA AUTONOMOUS_TRANSACTION zu Ihrem Auslöser hinzuzufügen: Dies wird oft in Foren vorgeschlagen und funktioniert so gut wie das mutierende Tabellenproblem verschwindet - aber es macht das Datenintegritätsproblem noch schlimmer! Also einfach nicht ...

+0

Tatsächlich bekomme ich, dass "Tabelle mutiert" Fehler bei jedem Versuch, die capec_appe_id Spalte zu aktualisieren. –

+0

Entschuldigung, ich verstehe Ihren Kommentar nicht? –

0

Ich hatte einen ähnlichen Fehler mit Hibernate. Und Spülsitzung mit

getHibernateTemplate().saveOrUpdate(o); 
getHibernateTemplate().flush(); 

löste dieses Problem für mich. (Ich poste meinen Code-Block nicht, da ich sicher war, dass alles richtig geschrieben wurde und funktionieren sollte - aber erst, als ich die vorherige flush() - Anweisung hinzugefügt habe). Vielleicht kann das jemandem helfen.

Verwandte Themen