Session D-TRIGRI und Trigger für FortgeschritteneNathalie Mengel
|
Regel | Aktion |
---|---|
CASCADE | Weitergabe von Löschungen/Modifikationen |
RESTRICT | Verhindern von Löschungen/Modifikationen |
NULLIFY | Nullsetzen abhängiger Daten |
Ignore | Nichtbeachten der RI (VFP-eigene Regel) |
Set To Default | Auf Default setzen abhängiger Daten |
Nicht alle dieser Regeln sind Standard oder im VFP RI-Designer einstellbar.
Trigger und Stored Procedures sind Prozeduren, die in der Datenbank gespeichert werden. Der Unterschied zwischen ihnen besteht grundsätzlich darin, daß Trigger durch ein bestimmtes Datenbankereignis ausgelöst werden, während Stored Procedures explizit aufgerufen werden müssen. Trigger benutzen häufig Stored Procedures, um Prüfungen durchzuführen oder Updates auszuführen. Trigger werden standardmäßig vor Insert-/Update-/Delete-Anforderungen angestoßen. Einige Datenbanksysteme können auch Trigger nach Insert-/Update-/Delete-Aktionen ausführen.
Vergleichen wir doch mal die Datenbank (bzw. den VFP-Datenbankcontainer) mit einer Container-Klasse. Dann entsprechen die Datenbank-Trigger den Events, die für den Container ausgelöst werden. Die Stored Procedures entsprechen den Methoden, die für den Container aufgerufen werden. Die Tabellen einer Datenbank entsprechen den Objekten, die sich im Container befinden.
Um die RI wahren zu können, müssen Schutzmechanismen vorhanden sein, die Änderungen des Datenbankinhalts bei Verletzung der RI abweisen. Visual Foxpro realisiert diese Schutzmechanismen ausschließlich über Trigger. Das heißt, daß bei jedem Datenbankupdate, für das ein Trigger vorhanden ist, der entsprechende Triggercode aufgerufen wird.
Auch in Client/Server Datenbanken kann man sämtliche RI über Trigger abbilden. Allerdings verfügen diese Datenbanken meistens über einen datenbankeigenen Schutzmechanismus, der RI-Verletzungen abfängt. Diesen Mechanismus nennt man Deklarative RI (declarative integrity constraint). Deklarative RI hat den Vorteil, daß sie durch datenbankeigene Optimierungsmechanismen schneller ist und nicht gewartet werden muß. Allerdings werden durch sie natürlich nur Standardfehlermeldungen erzeugt, die im Zweifelsfalle vom Anwendungsprogramm nachbearbeitet werden müssen, um für den Benutzer aussagekräftig sein zu können.
Je nach Datenbanksystem kann die Deklarative RI mehr oder weniger selbst Regeln zur Wahrung der Integrität ausführen (z.B. CASCADE DELETE). Informieren Sie sich bei der Planung von Client/Server Datenbanken über deren Möglichkeiten, um sie optimal nutzen zu können.
An Tabellen, für die ein Trigger gefeuert wurde, können bei Ausführung des Triggers keinerlei Änderungen vorgenommen werden! |
Nehmen wir an, wir wollen einen Satz aus einer Tabelle löschen, während in einer abhängigen Tabelle noch ein Satz vorhanden ist, dessen Fremdschlüssel dem zu löschenden Primärschlüssel entspricht. Um die Integrität der Daten zu wahren, stellen wir hier zwei Möglichkeiten vor: entweder die Löschung wird zurückgewiesen, oder der Fremdschlüssel des Datensatzes in der abhängigen Tabelle wird auf einen Standardwert gesetzt. Ersteres entspricht der RI-Regel "Restrict", letzeres der RI-Regel "Set To Default".
Restrict Delete (Trigger-RI)
Restrict Delete (Deklarative RI)
Delete Set To Default (Trigger-RI)
Delete Set To Default (Trigger/Deklarative RI)
Persistente Beziehungen werden in Visual Foxpro entweder wie eingangs beschrieben visuell erstellt, oder indem man den Fremdschlüssel in der abhängigen Tabelle deklariert. In VFP hat dies tatsächlich nur die Erstellung der Beziehung zur Folge, was für die RI noch gar nichts bedeutet, weil RI hier eben ausschließlich über Trigger realisiert wird. Man könnte auch (beschönigend) sagen, daß VFP standardmäßig IGNORE als Insert-, Update- und Delete-Regeln einsetzt.
Beispiel:
ALTER TABLE bestell;In Visual Foxpro können Sie Trigger durch den RI-Assistenten generieren lassen. Zum Ändern von Triggern oder Hinzufügen von Code öffnen Sie die Datenbankprozeduren mit MODIFY PROCEDURE. Sie können über die Bearbeitung von Datenbankprozeduren auch neue Trigger erstellen, müssen diese dann aber im Tabellendesigner mit den gewünschten Aktionen verbinden.
Natürlich können Sie auch Trigger wie bei Client/Server Datenbanken über den CREATE TRIGGER Befehl erstellen.
Bei Client/Server Datenbanken erzeugt die Deklaration eines Fremdschlüssels automatisch die entsprechende Deklarative RI, weil dort standardmäßig RESTRICT als Regel eingesetzt wird. Hier ein Beispiel zur Erstellung Deklarativer RI unter Oracle.
ALTER TABLE bestell
In VFP können Sie Stored Procedures erstellen, indem Sie im Datenbankdesigner über das Kontexmenü Gespeicherte Prozeduren bearbeiten auswählen oder im Befehlsfenster MODIFY PROCEDURE eingeben.
Beachten Sie folgendes bei der Planung von Triggern:
Die folgenden Beispiele basieren auf einer stark vereinfachten Beispiel-VFP Datenbank, die aus vier Tabellen besteht: kunde, bestell, posten und protokol. Die ersten drei sind durch persistente Beziehungen miteinander verbunden und unterliegen Referentieller Integrität. Die Tabelle protokol dient zum Protokollieren von Änderungen.
Wie zu vermuten war, sind in der Tabelle kunde Kundendaten gespeichert. Sie verfügt über den Primärschlüssel idkunde. Die Tabelle bestell soll Bestellungen unserer Kunden speichern. Sie verfügt über den Primärschlüssel idbestell und den Fremdschlüssel idkunde. Damit ist sie abhängig von der Tabelle kunde. Die Tabelle posten soll die Bestellposten speichern. Sie verfügt über den Primärschlüssel idposten und den Fremdschlüssel idbestell. Somit ist sie abhängig von der Tabelle bestell.
Beim Löschen und Aktualisieren von Kundendaten sollen die Änderungen an die Bestelltabelle weitergegeben werden (DELETE CASCADE und UPDATE CASCADE). (Wem DELETE CASCADE in diesem Beispiel nicht besonders sinnvoll scheint: Sie werden später sehen, warum wir diese Regel hier verwenden!). Das Löschen von Bestelldaten ist untersagt, wenn zu der Bestellung bereits Posten existieren (DELETE RESTRICT). Änderungen sollen jedoch weitergegeben werden (UPDATE CASCADE). Das Einfügen von Bestelldaten, bzw. Postendaten wird untersagt, wenn der entsprechende Fremdschlüssel nicht in der Parent-Tabelle vorhanden ist (INSERT RESTRICT).
Alle Primärschlüssel sind vom Typ INTEGER. Dies ist nicht nur die schnellste Möglichkeit, sie erleichtert auch in diesem Beispiel den Eintrag in die Protokolltabelle.
Beim Löschen in der Tabelle kunde wird zunächst der dazugehörige Trigger aufgerufen
** "Referentielle Integrität-Delete-Trigger für" kunde PROCEDURE __RI_DELETE_kunde LOCAL llRetVal && den Return-Value für diesen Trigger deklarieren und initialisieren llRetVal = .t. PRIVATE pcParentDBF,pnParentRec,pcChildDBF,pnChildRec,pcParentID,pcChildID PRIVATE pcParentExpr,pcChildExpr STORE "" TO pcParentDBF,pcChildDBF,pcParentID,pcChildID,pcParentExpr,pcChildExpr STORE 0 TO pnParentRec,pnChildRec
Wenn _triggerlevel=1 ist, wurde diese Funktion direkt durch eine DELETE-Anforderung aufgerufen und nicht etwa durch andere Trigger. In diesem Fall werden zunächst einige Initialisierungsmaßnahmen durchgeführt.
IF _triggerlevel=1
Sämtliche Änderungen werden in die Transaktion eingeschlossen. Im Fehlerfall wird ein ROLLBACK aufzurufen, um die Änderungen rückgängig zu machen
BEGIN TRANSACTION PRIVATE pcRIcursors,pcRIwkareas,pcRIolderror,pnerror,; pcOldDele,pcOldExact,pcOldTalk,pcOldCompat,PcOldDBC pcOldTalk=SET("TALK") SET TALK OFF pcOldDele=SET("DELETED") pcOldExact=SET("EXACT") pcOldCompat=SET("COMPATIBLE") SET COMPATIBLE OFF SET DELETED ON SET EXACT OFF pcRIcursors="" pcRIwkareas="" pcRIolderror=ON("error")
Das Fehlerflag pnerror wird als PRIVATE-Variable von den aufgerufenen Funktionen weiter benutzt.
pnerror=0Fehler automatisch in pnerror speichern lassen
ON ERROR pnerror=rierror(ERROR(),message(),message(1),program()) IF TYPE('gaErrors(1)')<>"U" release gaErrors ENDIF PUBLIC gaErrors(1,12) pcOldDBC=DBC() SET DATA TO ("DTRIG") ENDIF && first trigger LOCAL lcParentID && parent's value to be sought in child LOCAL lcChildWkArea && child work area handle returned by riopen LOCAL lcParentWkArea LOCAL llDelHeaderarea Hier beginnt die eigentliche Verarbeitung lcStartArea=select() && den aktuellen Arbeitsbereich sichern llRetVal=.t. lcParentWkArea=select() SELECT (lcParentWkArea) pcParentDBF=dbf() pnParentRec=recno() STORE IDKUNDE TO lcParentID,pcParentID && die gelöschte idkunde pcParentExpr="IDKUNDE" Durch die Funktion riopen wird die bestell- Tabelle geöffnet (s.u.) lcChildWkArea=riopen("bestell","idkunde") IF lcChildWkArea<=0 && Abbruch, wenn das Öffnen fehlschlug IF _triggerlevel=1 DO riend WITH .F. ENDIF at the end of the highest trigger level RETURN .F. ENDIF not able to open the child work area pcChildDBF=dbf(lcChildWkArea) SELECT (lcChildWkArea) && die bestell-Tabelle selektieren SEEK lcParentID && die erste Bestellung zu unserem Kunden suchen SCAN WHILE IDKUNDE=lcParentID AND llRetVal pnChildRec=recno() pcChildID=IDKUNDE pcChildExpr="IDKUNDE" Zum Löschen anhängender Bestellungen wird die Stored Procedure ridelete aufgerufen (s.u.) llRetVal=ridelete() ENDSCAN && get all of the bestell recordsrireuse gibt die Tabelle bestell zur weiteren Bearbeitung frei =rireuse("bestell",lcChildWkArea)
Die Stored Procedure riopen
riopen wird von Triggern zum Öffnen von Tabellen benutzt
PROCEDURE riopen PARAMETERS tcTable,tcOrder local lcCurWkArea,lcNewWkArea,lnInUseSpot
Die Variable pcRIcursors enthält die geöffneten Tabellen in folgender Struktur: Tabellenname, In-Use-Flag, Arbeitsbereich. Die Tabelle wird nur dann geöffnet, wenn sie nicht offen, bzw. offen aber in Bearbeitung ist.
lnInUseSpot=atc(tcTable+"*",pcRIcursors) IF lnInUseSpot=0 && Wir müssen die Tabelle neu öffnen lcCurWkArea=select() SELECT 0 lcNewWkArea=select() IF NOT EMPTY(tcOrder) && Index angegeben? USE (tcTable) AGAIN ORDER (tcOrder) ; ALIAS ("__ri"+LTRIM(STR(SELECT()))) share ELSE USE (tcTable) AGAIN ALIAS ("__ri"+LTRIM(STR(SELECT()))) share ENDIF if pnerror=0 pcRIcursors=pcRIcursors+upper(tcTable)+"?"+STR(SELECT(),5) else lcNewWkArea=0 endif && something bad happened while attempting to open the file ELSE Tabelle mußte nicht neu geöffnet werden lcNewWkArea=val(substr(pcRIcursors,lnInUseSpot+len(tcTable)+1,5)) Also müssen wir nur das In-Use-Flag setzen... pcRIcursors = strtran(pcRIcursors,upper(tcTable)+"*"+str(lcNewWkArea,5),; upper(tcTable)+"?"+str(lcNewWkArea,5)) ... und natürlich den richtigen Index aktivieren IF NOT EMPTY(tcOrder) SET ORDER TO (tcOrder) IN (lcNewWkArea) ENDIF sent an order if pnerror<>0 lcNewWkArea=0 endif && something bad happened while setting order ENDIF lcNewWkArea enthält im Erfolgsfall den Arbeitsbereich der geöffneten Tabelle und wird in der aufrufenden Prozedur abgefragt. RETURN (lcNewWkArea)riopen wird zum Zurücksetzen des In-Use-Flags benutzt
PROCEDURE rireuse * rireuse.prg PARAMETERS tcTableName,tcWkArea pcRIcursors = strtran(pcRIcursors,upper(tcTableName)+; "?"+str(tcWkArea,5),; upper(tcTableName)+"*"+str(tcWkArea,5)) RETURN .t.riend wird von Triggern zum Beenden der Verarbeitung benutzt
PROCEDURE riend PARAMETERS tlSuccess local lnXX,lnSpot,lcWorkAreaÄnderungen im Erfolgsfall wegschreiben
IF tlSuccess END TRANSACTION ELSE
Änderungen im Fehlerfall zurücknehmen
SET DELETED OFF ROLLBACK SET DELETED ON ENDIF IF EMPTY(pcRIolderror) ON ERROR ELSE ON ERROR &pcRIolderror. ENDIF
Die geöffneten Tabellen wieder schließen
FOR lnXX=1 TO occurs("*",pcRIcursors) lnSpot=atc("*",pcRIcursors,lnXX)+1 USE IN (VAL(substr(pcRIcursors,lnSpot,5))) ENDFOR IF pcOldCompat = "ON" SET COMPATIBLE ON ENDIF IF pcOldDele="OFF" SET DELETED OFF ENDIF IF pcOldExact="ON" SET EXACT ON ENDIF IF pcOldTalk="ON" SET TALK ON ENDIF do case case empty(pcOldDBC) set data to case pcOldDBC<>DBC() set data to (pcOldDBC) endcase RETURN .T.ridelete wird von Triggern zum Löschen von Child-Daten benutzt
procedure RIDELETE local llRetVal llRetVal=.t. IF (ISRLOCKED() and !deleted()) OR !RLOCK() && den Datensatz sperren llRetVal=.F. ELSE IF !deleted() DELETE && löschen IF CURSORGETPROP('BUFFERING') > 1 =TABLEUPDATE() ENDIFAchtung: Warum diese Zeile ausgesternt ist, unbedingt unten nachlesen
*** llRetVal=pnerror=0 ENDIF not already deleted ENDIF UNLOCK RECORD (RECNO())
Die Zeile von oben wurde manuell hierher verschoben
llRetVal=pnerror=0 RETURN llRetValDie vom RI-Assistenten generierte Prozedur RIDELETE hat einen kleinen, aber unter Umständen verheerenden Haken. Wenn - wie in unserem Beispielfall - der DELETE einen weiteren Trigger auslöst, der unter Umständen fehlschlagen kann (Beachte: Bestellungen werden nur gelöscht, wenn keine Bestellpositionen mehr vorhanden sind), dann muß der Abbruch hier erkannt werden. Da aber die Routine riopen die Tabellen im ungepufferten Modus öffnet, wird kein nächster Level des Triggers vor der Anweisung unlock ausgeführt. Wenn der unlock also erst nach dem Setzen des Returnwertes ausgeführt wird, bekommen wir nichts von einem eventuellen Fehler mit. Die Lösung liegt darin, den Returnwert erst nach dem unlock zu füllen.
Ansonsten können unter unseren beschriebenen Bedingungen Zombie-Datensätze entstehen, wie hier zu sehen ist. Der Parent kunde und die erste Bestellung wurden erfolgreich gelöscht, bevor der DELETE-Trigger der zweiten Bestellung weiteres verhindern konnte.
Nun soll die Protokoll-Tabelle aus unserem Beispiel bei jeder Änderung von Bestelldaten gefüllt werden. Man kann dies jedesmal von Hand im Programm (bzw. in der entsprechenden Klasse) erledigen. Aber was ist, wenn Benutzer auch ohne Anwendungsprogramm, z.B. über das Browse-Fenster, Zugriff auf die Daten haben. Oder was passiert, wenn man das Schreiben der Protokolltabelle irgendwo im Programmcode vergißt?
In unserem Beispiel werden Timestamp, Benutzerid, Tabellenname, ausgeführte Aktion und der betroffene Primärschlüssel über einen Trigger in die Protokolltabelle geschrieben. Wir demonstrieren dies an dem Update-Trigger der Tabelle bestell, aber man könnte so alle wichtigen Tabellen der Datenbank verwalten und sogar Löschungen von Daten problemlos nachvollziehen.
procedure __RI_UPDATE_bestell ** "Referentielle Integrität-Update-Trigger für" bestell. . . wir sparen uns hier die Erklärung des gesamten Triggers . . .
LOCAL llRetVal . . . IF _triggerlevel=1 . . . unsere benötigten Variablen LOCAL lcUser, ldTst, lcTable, lcAction, lnPk, lcProtWorkArea . . . ENDIF first trigger . . .
Durchführen der Änderungen
. . .
Jetzt gehts los: IF llRetVal && nur nach erfolgreichem Update ldTst = DATETIME() && Timestamp holen lcUser = UPPER(ALLTRIM(GETENV('Username'))) && Netzwerkuser holen lcTable = "bestell" && die betroffene Tabelle lcAction = "UPDATE" && die durchgeführte Aktion lnPk = idbestell && der betroffene Primärschlüssel lcProtWorkArea = riopen("protokol","") && Protokolltabelle öffnen lassen IF lcProtWorkArea<=0 && Abbrechen, wenn das Öffnen nicht geklappt hat IF _triggerlevel=1 DO riend WITH .F. ENDIF SELECT (lcStartArea) RETURN .F. ENDIF && not able to open prot
Einfügen des Protokollsatzes
INSERT INTO protokol VALUES(ldTst,lcUser,lcTable,lcAction,lnPk) =rireuse("protokol",lcProtWorkArea) && Prot-Tabelle wieder freigeben ENDIF und so weiter SELECT (lcStartArea) RETURN llRetVal ** "Ende des Referentielle Integrität-Update-Triggers für" bestell ********************************************************************Zum Abschluß betrachten wir noch einen Trigger, der aus einer Oracle-Datenbank stammt. Dieser Trigger feuert bei Einträgen in die Mitarbeitertabelle mitarbeiter. Er prüft in der Gehaltsstufen-Tabelle stufe, ob sich das angegebene Gehalt gehalt innerhalb des Rahmens befindet, der für die Qualifikation quali üblich ist.
Vielleicht zahlt es sich ja mal für Sie aus, wenn Sie solche Mechanismen kennen...
CREATE TRIGGER gehalts-check BEFORE INSERT OR UPDATE OF gehalt, quali ON mitarbeiter FOR EACH ROW DECLARE minsal NUMBER; maxsal NUMBER; zuviel_oder_zuwenig EXCEPTION; BEGIN
Das minimale und maximale Gehalt für die entsprechende Qualifikation aus der Tabelle stufe holen
SELECT minsal, maxsal INTO minsal, maxsal FROM stufe WHERE stufe.quali = :new.quali;
Wenn das eingegebene Gehalt größer oder kleiner als das Limit ist, wird eine Exception angestoßen. Damit wird eine Fehlermeldungzurückgegeben und das INSERT, bzw. UPDATE, das den Trigger ausgelösthat, wieder zurückgedreht.
IF (:new.gehalt < minsal OR :new.gehalt > maxsal) THEN (hier wäre noch Platz für Ihre Änderungen) RAISE zuviel_oder_zuwenig; END IF;
WHEN NO_DATA_FOUND THEN raise_application_error(–20322,’Falsche Qualifikation ’||:new.quali); END;
From Here...
An dieser Stelle vielen Dank an Doug Hennig für die Fehlerbereinigung in der RIDELETE-Prozedur und Jürgen Wondzinski dafür, daß er diese auf der DEVCON97 vorgestellt hat.
Die im Text angesproche Beispieldatenbank finden Sie auf der Begleit-CD zur DEVCON.
Für Fragen stehe ich Ihnen jederzeit unter Mengel@indisoft-gmbh.de zur Verfügung.
Sie erreichen mich auch auf unserer Website www.indisoft-gmbh.de.