Oracle 10 g/Trigger

Hallo,
ich bräuchte einen Trigger, der feuert, wenn ein Datensatz mit der gleichen ID und dem Merker ‚T‘ (für TRUE)gefunden wird. Ein insert soll dann nicht zugelassen werden. Der Versuch soll in einer Protokolltabelle abgespeichert werden.
Also: Prüfe, ob ID bereits vorhanden ist, wenn ja, prüfe ob Merker auf ‚T‘ gesetzt ist, wenn ja lasse keinen insert zu. Schreibe Versuch in Protokolltabelle.

create or replace mein_trigger_br
before insert
of id on meine_tabelle
for each row
when …
declare
id number;
merker bool;
.
.
.
end if;
insert into meine_tabelle_protokoll
values (id, merker, sysdate, user)
end;

Bei der Tabelle handelt es sich um eine Supplement-Tabelle, die mehrere gleiche ID’s zuläßt. Der Primärschlüssel ist aus der ID und zwei weiteren Nummern zusammengesetzt.

Gruß

Stefan

Hallo Stefan,

ich würde vorschlagen die Datenbank umzumodellieren. Klingt radikal, hat aber einen Grund: Der Trigger, den du haben willst, müsste bei jedem INSERT oder UPDATE (da natürlich nur, wenn deine id betroffen ist) die gesamte Tabelle sperren, und zwar deshalb:

Session 1 Session 2 ohne Lock mit Lock
INSERT id 4711 OK OK
 INSERT id 4711 OK WARTEN
COMMIT OK KEY VIOLATION
 COMMIT OK FEHLER

Du musst also faktisch dafür sorgen, dass der Trigger erst ausgeführt wird, wenn auch tatsächlich keine Transaktion auf der Tabelle offen ist, und das geht nunmal nur mit Table Locking. Wenn du das Ganze über einen Constraint löst, dann hast du auch wieder ein Mehrplatzsystem.
Wenn die Struktur denn unbedingt so bleiben muss, dann würde ich das mit einem Function Based Index lösen, also in etwa so:

CREATE UNIQUE INDEX my\_idx ON my\_ttab(DECODE(merker, 'T', id, NULL));

Du kriegst dann beim INSERT einen ORA-00001, den du programmatisch abfangen und verarbeiten kannst. Das Ganze noch hübsch in eine PL/SQL-Prozedur verpackt, und du hast ein wasserdichtes Interface.

Gruß
Martin

Moin zusammen,

bezüglich Ummodellierung geb´ ich Dir absolut recht; eine Lösung via Trigger ist immer gefährlich - zumal mann irgendwann vergessen hat, dass ein solchiger existiert.

Nichts desto trotz hier vielleicht ein Ansatz, wobei hier aufgrund mangelndem PK oder UK evtl. zuviele Zeilen gelöscht werden. Des weiteren wird der bestehende Datensatz gelöscht und der neue dennoch eingefügt. Ein after Insert-Trigger hat bei mir garnicht funktioniert.

Vielleicht hilft Dir der Ansatz ein wenig weiter.

Gruß
Andreas

CREATE TABLE testtabelle
(tst_id NUMBER(9,0),
tst_flag VARCHAR2(1),
tst_irgendwas VARCHAR2(100))
/
– Triggers for TESTTABELLE

CREATE OR REPLACE TRIGGER tst_instrg
BEFORE
INSERT
ON testtabelle
REFERENCING NEW AS NEW OLD AS OLD
FOR EACH ROW
DECLARE
v_zaehler NUMBER(9,0):=0;

BEGIN

SELECT COUNT(1)
INTO v_zaehler
FROM testtabelle tst
WHERE tst.tst_id = :NEW.tst_id
AND tst.tst_flag = ‚T‘;

IF v_zaehler = 0 THEN
NULL;
ELSIF v_zaehler = 1 THEN
DELETE FROM testtabelle tst
WHERE tst.tst_id = :NEW.tst_id
AND tst.tst_flag = ‚T‘;
ELSE
DELETE FROM testtabelle tst
WHERE tst.tst_id = :NEW.tst_id
AND tst.tst_flag = ‚T‘;
RAISE TOO_MANY_ROWS;
END IF;

EXCEPTION
WHEN TOO_MANY_ROWS THEN
DBMS_OUTPUT.Put_Line(SUBSTR(SQLCODE||’ - ‚||SQLERRM ,1,255));
DBMS_OUTPUT.Put_Line(SUBSTR(‚Trigger TST_INSTRG hat zuviele Einträge in Testtabelle gefunden‘,1,255));
WHEN others THEN
DBMS_OUTPUT.Put_Line(SUBSTR(SQLCODE||‘ - '||SQLERRM ,1,255));
END;
/

Hallo Martin,

erst mal danke für deine Hilfe. Ich muss vorausschicken, daß ich ein Einsteiger bzgl. Datenbankprogrammierung bin. Deshalb habe ich wahrscheinlich noch ein Verständnisproblem. Nochmal von vorne: Wenn ich dich richtig verstanden habe, sperren sich die zwei Sessions gegenseitig bei einem insert oder update, weil eine Transaktion aufgrund der bereits vorhandenen id offen ist (Gibt es einen KEY VIOLATION obwohl die ID bei mir kein Key ist?).

Übrigens: Die Datenbank ummodellieren scheidet leider aus.

Gruß

Stefan

[Bei dieser Antwort wurde das Vollzitat nachträglich automatisiert entfernt]

Hallo Andreas,

auch dir schon mal danke für die Hilfe. Ich habe zwei Kommentare in den Code eingefügt, vielleicht kannst du mir sagen, ob ich richtig liege. Den Rest habe ich kapiert.
Das Ergebnis wäre O.K… Der bestehende Datensatz kann gelöscht und der neue eingefügt werden. Ich werde mal einen Test starten.

Gruß

Stefan

Moin zusammen,

bezüglich Ummodellierung geb´ ich Dir absolut recht; eine
Lösung via Trigger ist immer gefährlich - zumal mann
irgendwann vergessen hat, dass ein solchiger existiert.

Nichts desto trotz hier vielleicht ein Ansatz, wobei hier
aufgrund mangelndem PK oder UK evtl. zuviele Zeilen gelöscht
werden. Des weiteren wird der bestehende Datensatz gelöscht
und der neue dennoch eingefügt. Ein after Insert-Trigger hat
bei mir garnicht funktioniert.

Vielleicht hilft Dir der Ansatz ein wenig weiter.

Gruß
Andreas

CREATE TABLE testtabelle
(tst_id NUMBER(9,0),
tst_flag VARCHAR2(1),
tst_irgendwas VARCHAR2(100))
/
– Triggers for TESTTABELLE

CREATE OR REPLACE TRIGGER tst_instrg
BEFORE
INSERT
ON testtabelle
REFERENCING NEW AS NEW OLD AS OLD
FOR EACH ROW
DECLARE
v_zaehler NUMBER(9,0):=0;

– Zähler deklarieren und mit 0 beginnen

BEGIN

SELECT COUNT(1)
INTO v_zaehler
FROM testtabelle tst
WHERE tst.tst_id = :NEW.tst_id
AND tst.tst_flag = ‚T‘;

IF v_zaehler = 0 THEN
NULL;
ELSIF v_zaehler = 1 THEN

– warum nicht > 0

DELETE FROM testtabelle tst
WHERE tst.tst_id = :NEW.tst_id
AND tst.tst_flag = ‚T‘;
ELSE
DELETE FROM testtabelle tst
WHERE tst.tst_id = :NEW.tst_id
AND tst.tst_flag = ‚T‘;
RAISE TOO_MANY_ROWS;
END IF;

EXCEPTION
WHEN TOO_MANY_ROWS THEN
DBMS_OUTPUT.Put_Line(SUBSTR(SQLCODE||’ - ‚||SQLERRM
,1,255));
DBMS_OUTPUT.Put_Line(SUBSTR(‚Trigger TST_INSTRG hat
zuviele Einträge in Testtabelle gefunden‘,1,255));
WHEN others THEN
DBMS_OUTPUT.Put_Line(SUBSTR(SQLCODE||‘ - '||SQLERRM
,1,255));
END;
/

v_zaehler NUMBER(9,0):=0;

– Zähler deklarieren und mit 0 beginnen

Da der Codeblock je Zeile eines statements aufgerufen wird, reicht die Deklaration im Rumpf und muss nicht nach „Begin“ neu ausgeführt werden (wenn das deine Frage war).

IF v_zaehler = 0 THEN
NULL;
ELSIF v_zaehler = 1 THEN

– warum nicht > 0

–> hab´ ich hier gemacht um den Fall gesondert behandeln zu können, wenn bereits in der Tabelle mehr Datensätze als nur 1 mit der tst_id bestehen sollte; insbesondere kann dies empfehlenswert sein, wenn ein Trigger auf eine bestehende, gefüllte Tabelle nachträglich aufgesetzt wird. Darüber hinaus gibt es ja noch das Problem mit den konkurierenden Zugriffen, die bei einem solchen Trigger auftreten können.

Gruß
Andreas

v_zaehler NUMBER(9,0):=0;

– Zähler deklarieren und mit 0 beginnen

Da der Codeblock je Zeile eines statements aufgerufen wird,
reicht die Deklaration im Rumpf und muss nicht nach „Begin“
neu ausgeführt werden (wenn das deine Frage war).

IF v_zaehler = 0 THEN
NULL;
ELSIF v_zaehler = 1 THEN

– warum nicht > 0

–> hab´ ich hier gemacht um den Fall gesondert behandeln
zu können, wenn bereits in der Tabelle mehr Datensätze als nur
1 mit der tst_id bestehen sollte;

insbesondere kann dies

empfehlenswert sein, wenn ein Trigger auf eine bestehende,
gefüllte Tabelle nachträglich aufgesetzt wird. Darüber hinaus
gibt es ja noch das Problem mit den konkurierenden Zugriffen,
die bei einem solchen Trigger auftreten können.

Gruß
Andreas

wenn also z.B., wie es bei mir der Fall ist, drei Datensätze mit der gleichen id vorhanden sind - werden dann alle drei nacheinander gelöscht (nach jedem delete wird ein neuer v_zaehler zugewiesen, bis v_zaehler = 0) oder nur einer der drei oder gar keiner und nur die Fehlermeldung wird ausgegeben?

Sorry der nochmaligen Nachfrage

Stefan

Hallo Andreas,

bezüglich Ummodellierung geb´ ich Dir absolut recht; eine
Lösung via Trigger ist immer gefährlich - zumal mann
irgendwann vergessen hat, dass ein solchiger existiert.

Tut mir leid, aber die ist nicht gefährlich sondern falsch - zumindest solange du keinen Table Lock machst.

Es geht um das Szenario, das ich schon im ersten Post beschrieben hatte:

  • Session A fügt einen neuen Satz ein

  • Trigger wird ausgeführt --> Satz existiert noch nicht, alles ok

  • Die Transaktion von User A wird aber noch nicht abgeschlossen, d.h. der Satz ist für andere Sessions nicht sichtbar

  • Session B fügt den gleichen Satz ein

  • Trigger wird ausgeführt wobei der Satz aus Session A nicht gefunden wird und der Satz wird ebenfalls eingefügt

  • Session A macht den commit

  • Session B macht den commit

Genau jetzt hast du den Satz doppelt ohne es bemerkt zu haben. Folglich MUSS hier ein Table Lock von Session A gemacht werden, dann müsste Session B eben bis zum commit von A warten, danach findet er den Satz auch. Man hat jetzt halt ein Einzelplatzsystem gebaut, wenn das nicht stört…

Ich würde es trotzdem so machen:

CREATE TABLE tmp\_tst1 (id NUMBER(9) NOT NULL,
 flag VARCHAR2(1) NOT NULL
 CHECK (flag IN ('T','F')),
 data VARCHAR2(1000));
CREATE TABLE tmp\_tst2 (id NUMBER(9) NOT NULL,
 data VARCHAR2(1000));
CREATE UNIQUE INDEX uk\_tst1 ON tmp\_tst1(DECODE(flag,'T',id,NULL));

CREATE OR REPLACE PROCEDURE tmp\_proc1 (id IN NUMBER,
 flag IN VARCHAR2,
 data IN VARCHAR2) AS
BEGIN
 INSERT INTO tmp\_tst1 (id, flag, data) VALUES (id, flag, data);
EXCEPTION WHEN DUP\_VAL\_ON\_INDEX THEN
 INSERT INTO tmp\_tst2 (id, data) VALUES (id, data);
END tmp\_proc1;
/

EXEC tmp\_proc1(1,'T','1: first insert');
EXEC tmp\_proc1(1,'F','1: second insert');
EXEC tmp\_proc1(1,'T','1: third insert');
EXEC tmp\_proc1(2,'F','2: first insert');
EXEC tmp\_proc1(2,'T','2: second insert');

Das ist dann auch mehrplatzfähig.

Falls man keinen Zugriff auf den Programmcode hat (und daher die INSERTs nicht durch Prozeduraufrufe ersetzen kann), dann könnte man z.B. die Tabelle umbenennen, einen View erstellen, der gleich wie zuvor die Tabelle heisst und auf diesen View einen INSTEAD OF Trigger anlegen (evtl. geht das mittlerweile auch schon auf Tabellen, dann spart man sich noch den Umweg mit dem View).

Gruß
Martin

Huhu,

wenn also z.B., wie es bei mir der Fall ist, drei Datensätze
mit der gleichen id vorhanden sind - werden dann alle drei
nacheinander gelöscht (nach jedem delete wird ein neuer
v_zaehler zugewiesen, bis v_zaehler = 0) oder nur einer der
drei oder gar keiner und nur die Fehlermeldung wird
ausgegeben?

Der Block löscht alle Datensätze mit der ID und dem flag=‚T‘ aus der Tabelle raus. Da es sich nicht um eine Schleife handelt (was man nat. auch machen kann) wird v_zaehler keinen neuen Wert zugewiesen sondern alles in einem Rutsch gelöscht.

Was man z.B. auch machen kann ist eine zweite Tabelle, welche die gleichen Spalten wie diese hat und die Werte, welche gelöscht werden, dann in diese Tabelle zuvor wegsichert.

Allerdings denke ich, sollte man dann den Zweck deiner Datenbank mal betrachten, der bisher unerwähnt geblieben ist.

Hier solltest Du Dir auf jeden fall die Sachen durchlesen die TheBeast aka Martin geschrieben hat.

Irgendwo sagst Du, dass ein ummodellieren der Datenbank (was wohl erforderlich wäre) nicht möglich wäre. Vielleicht solltest Du erklären was für eine Datenbank es ist, wozu diese dient und warum eine solche Konstellation der Daten vorkommt, dass Du einen Datensatz löschen willst.

Auch wichtig zu wissen wäre, wieviele Benutzer gleichzeitig auf der Datenbank arbeiten (also nur einer, eine überschaubare Anzahl z.B. 10 oder unbegrenzt z.B. über Webformularen durch Internetnutzer)

Also dieser Lösungsansatz gefällt mir auch gut:

http://www.wer-weiss-was.de/cgi-bin/forum/showarticl…

Sorry der nochmaligen Nachfrage

Stefan

Dafür ist doch ein Forum da oder?

Gruß
Andreas

Hallo,

versuchs doch mal damit:

CREATE TABLE "PROTTAB" 
 ( "ID" NUMBER(10,0), 
 "MERKER" CHAR(1), 
 "DATUM" DATE, 
 "BENUTZER" VARCHAR2(30), 
 CONSTRAINT "PROTTAB\_PK" PRIMARY KEY ("ID") ENABLE
 )
/
CREATE TABLE "MEINE\_TAB" 
 ( "ID" NUMBER(10,0), 
 "ID2" NUMBER(10,0), 
 "MERKER" CHAR(1), 
 CONSTRAINT "MEINE\_TAB\_PK" PRIMARY KEY ("ID", "ID2") ENABLE
 )
/
CREATE UNIQUE INDEX "MEINE\_TAB\_IDX1" ON "MEINE\_TAB" ("ID", "MERKER")
/
CREATE OR REPLACE TRIGGER "MEINE\_TAB\_T1" 
BEFORE
insert on "MEINE\_TAB"
for each row
DECLARE
 PRAGMA AUTONOMOUS\_TRANSACTION;
 temp NUMBER;
BEGIN
 SELECT count(\*) INTO temp FROM meine\_tab WHERE id = :NEW.id AND merker = :NEW.merker;
IF (temp \> 0) THEN
 INSERT INTO prottab VALUES (:NEW.id, :NEW.merker, SYSDATE, USER);
 COMMIT;
END IF;
END;
/
ALTER TRIGGER "MEINE\_TAB\_T1" ENABLE
/

Die Problematik mit dem PHANTOM READ/NON REPEATABLE READ tritt ja unabhängig davon auf und kann durch geeignete Wahl des Isolationslevelsoder organisatorisch verhindert werden.

Ein Fehler kann hier noch auftreten:
Der Trigger liefert IMMER ein Ergebnis, auch wenn das INSERT an einer anderen Ecke als dem ‚T‘ scheitert.

Gruß

Peter

[Bei dieser Antwort wurde das Vollzitat nachträglich automatisiert entfernt]