PDA

Archiv verlassen und diese Seite im Standarddesign anzeigen : SQL: Referenz auf Foreignkey?


AtTheDriveIn
2008-02-20, 02:53:52
Hallo

Ich habe ein kleines Problem beim Datenbankdesign. Ich will mir möglichst das Löschen "per Hand" ersparen.

btw: ich nutze Oracle 10g


Und zwar habe ich im Moment 3 Tabellen.

table Student
Matnr(prim key)
...
...

table Belegung
pMatnr (foreign key references Student(Matnr)
...
...

table Anwesenheit
pMatnr (foreign key references Student(Matnr)
...
...

Jetzt will ich, das wenn ich einen Datensatz aus Belegung lösche, er aus Anwesenheit ebenfalls verschwindet. Im Moment wird, wenn ich einen Datensatz aus Student lösche er (on delete cascade ist gesetzt) aus Belegung und Anwesenheit ebenfalls gelöscht, das ist ok. Ich möchte aber auch ersteres.

Eine Lösung wäre es, wenn pMatnr aus Anwesenheit auf pMatnr aus Belegung referenzieren würde, aber das scheint nicht zu funktionieren, jedenfalls beschwert sich da Oracle. Geht das überhaupt, eine Referenz auf einen Foreign Key?

Ideen? Oder bleibt nur das umständliche Löschen über delete from.... where... ??

Danke

daflow
2008-02-20, 14:02:36
Wenn denn dein Datenmodell so aussieht, das in Anwesenheit nur eine Martikelnummer stehen darf, wenn sie auch in Belegung vorhanden ist, ist so ein Foreignkey Konstrukt durchaus denkbar bzw notwendig aka

Anwesenheit (matnr) ref-> Belegung(matnr) ref->Student(matnr)

Wie sehen denn die PKs von Belegung und Anwesenheit aus, wei dein fehlgeschlagenes FK-Statement und wie die Fehlermeldung? ;)

Gast
2008-02-20, 14:48:30
Anwesenheit und Belegung haben untereinander keinen Bezug, und soweit ich das aus deinen Angaben ersehen kann, ist das auch richtig so. Wäre das nicht eine klassische Aufgabe für einen Trigger?

nefu
2008-02-20, 15:01:16
Anwesenheit und Belegung haben untereinander keinen Bezug, und soweit ich das aus deinen Angaben ersehen kann, ist das auch richtig so. Wäre das nicht eine klassische Aufgabe für einen Trigger?


Das sehe ich auch so wie der Gast ("on delete" trigger).

Gruß!

nefu

AtTheDriveIn
2008-02-22, 19:49:48
Ja gut Trigger. :)

Vom Prinzip weiß ich wie das funktionieren soll, aber ich bekomm das nicht hin. Hat jemand vielleicht mal einen Beispielcode wo sowas mit einem Trigger gemacht wird?

ich mache beispielsweise ein: delete from belegung where pMatnr=11111 and Veranstaltungskey=123123;

(Veranstaltungskey ist der Key einer Veranstaltung zum dem ich die Anwesenheit des Studenten in der Tabelle Anwesenheit speichere)

so, dann wird automatisch der Trigger aufgerufen, aber wie gebe ich ihm die die pMatnr und den Veranstaltungskey mit? Er soll ja schließlich die richtigen Datensätze in Anwesenheit löschen....

Berni
2008-02-22, 20:10:03
Bin leider in PL/SQL und Triggern nicht mehr so fit. So in der Art müsste das aussehen:
CREATE OR REPLACE TRIGGER belegung_after_delete AFTER DELETE ON belegung FOR EACH ROW
BEGIN
DELETE FROM anwesenheit WHERE Matnr = :OLD.Matnr
END;
Man kann da auch was komplizierteres machen (vorherige Abfrage in anderer Tabelle und Einlesen in Variable) aber ich weiß grad nicht, wie die Datenbankstruktur genau aussieht...

AtTheDriveIn
2008-02-22, 20:53:58
Bin leider in PL/SQL und Triggern nicht mehr so fit. So in der Art müsste das aussehen:
CREATE OR REPLACE TRIGGER belegung_after_delete AFTER DELETE ON belegung FOR EACH ROW
BEGIN
DELETE FROM anwesenheit WHERE Matnr = :OLD.Matnr
END;

Das sieht doch schonmal nach was aus. :)
Obwohl dieser Trigger löscht alle Datensätze mit besagter Matnr in Anwesenheit oder?

Berni
2008-02-22, 21:44:42
Ja. Ich dachte das wolltest du? Ansonsten müsstest du mal genauer die Struktur erklären und was du löschen willst.

nefu
2008-02-22, 22:01:17
Zum Thema Trigger gibt es in der Online Doku eine ganz gute Zusammenfassung:

http://download.oracle.com/docs/cd/B19306_01/appdev.102/b14251/adfns_triggers.htm#sthref1139

(Oracle Database Application Developer's Guide - Fundamentals - Chapter 9 : Coding triggers)

Die Anforderung hatte ich auch so verstanden, als sollten alle Sätze mit der betreffenden Matrnr gelöscht werden.
Falls nicht - die WHERE Klausel beim delete entsprechend anpassen/erweitern.

Gruß!

nefu

AtTheDriveIn
2008-02-22, 23:38:01
Ja. Ich dachte das wolltest du? Ansonsten müsstest du mal genauer die Struktur erklären und was du löschen willst.

Ich wollte die Sache hier anfangs nicht zu komlpliziert machen, da ich dachte dass das Prinzip gleich seien müßte. Eigentlich ist die Sache aber komplizierter (siehe vorletztes Posting von mir), da in Anwesenheit eben auch Anwesenheiten des Studenten in Veranstaltungen eingetragen sind, die er weiterhin belegt.

Ich habe es aber nun mit einem Cursor einem Join und einer For-Schleife hinbekommen. danke für die Hilfe :)

AtTheDriveIn
2008-02-24, 19:10:57
Hmm jetzt hat sich ein weiteres Problem aufgetan:

Ich habe ja nun einen Trigger auf Belegung, der beim löschen die entsprechenden Datensätze in Anwesenheit löscht.

create or replace TRIGGER t_refresh_Anwesenheit
BEFORE DELETE ON Belegung
FOR EACH ROW
DECLARE
CURSOR hisVeranstaltungen IS
SELECT PVerankey,PMatnr,Praktikumsgruppe FROM Anwesenheit INNER JOIN Veranstaltung ON(Anwesenheit.PVerankey=Veranstaltung.Veranstaltungskey)
WHERE Pmatnr=:OLD.PMatnr AND Praktikumsgruppe=:OLD.PPRakgrkey;
BEGIN
FOR i IN hisVeranstaltungen
LOOP
DELETE FROM Anwesenheit WHERE PMatnr=:OLD.PMatnr AND PVerankey=i.PVerankey;
END LOOP;
END t_refresh_Abgabe;

Das klappt auch wenn ich direkt in Belegung etwas lösche. Wird aber ein Datensatz aus Student gelöscht werden ja mittels 'on delete cascade' alle Datensätze in Belegung mit seiner Matnr automatisch mit gelöscht. Da gibts dann einen ORA-04091 Error, d.h. der Trigger schaltet sich ein und meckert das er irgendwas nicht mitbekommen könnte, oder so....

Was heißt das genau? Was muß ich machen? Kann ich den Trigger bei einem 'on delete cascade' Ereigniss irgendwie deaktivieren??

nefu
2008-02-24, 21:27:43
In diesem Fall ist die Ferndiagnose etwas schwierig, falls Du Zugriff auf metalink hast (https://metalink.oracle.com) solltest Du mit der Fehlernummer nach Einträgen suchen.
Für den Fall, dass Du gerade keinen account zur Hand hast helfen die folgenden Infos vielleicht weiter:


Note:74859.1
Subject: ORA-4091 Mutating Table Explanation and Workarounds
Applies to:
PL/SQL - Version:
Information in this document applies to any platform.
Purpose
The following bulletin discusses concepts of cascade update, delete, and insert and how to avoid the mutating table error.
Scope and Application
For users requiring the ability to cascade update, delete or insert whilst being able to maintain referential integrity between objects.
ORA-4091 Mutating Table Explanation and Workarounds
Overview
The purpose of this paper is to illustrate to those customers who require one of the following functional capabilities whilst being able to maintain referential integrity among objects:

* Cascade Update
* Cascade Delete
* Cascade Insert

For cascade Update and Insert functions, using stored triggers and procedures will result in an ORA-04091 - "Table <table_name> is mutating" error.

ORA-04091: "table %s.%s is mutating, trigger/function may not see it"
Cause: A trigger (or a user defined plsql function that is referenced in this statement) attempted to look at (or modify) a table that was in the middle of being modified by the statement which fired it.
Action: Rewrite the trigger (or function) so it does not read that table.

ORA-04091 is a very common error that occurs with triggers if triggers are not managed properly. A full understanding of triggers will help you avoid that error.
A mutating table is a table that is currently being modified by an update, delete, or insert statement. You will encounter the ORA-04091 error if you have a row trigger that reads or modifies the mutating table. For example, if your trigger contains a select statement or an update statement referencing the table it is triggering off of you will receive the error.

Another way that this error can occur is if the trigger has statements to change the primary, foreign or unique key columns of the table the trigger is triggering from.

If you must have triggers on tables that have referential constraints, the workaround is to enforce the referential integrity through triggers as well.

It must be stressed that this solution should ONLY be used to overcome DML restrictions imposed on triggers in order to maintain referential integrity. Whenever possible it is recommended that normal declarative integrity should be used to maintain foreign key integrity. Enforcing this integrity through stored triggers and procedures will have an effect on performance compared with declarative integrity.

For this solution to work correctly there must be no declarative integrity constraints between objects to enforce the foreign key constraint. The basic principle behind this solution is to suppress the validation checks performed as a result of inserting or updating a foreign key in the CHILD table triggered by a cascade Update or Insert. These checks would normally verify the existence of the new foreign key value in the PARENT record (by SELECTING from the parent table). The suppression of this check is only carried out as a direct result of Cascade Update or Delete, as we can be confident that this new value for the foreign key in the CHILD record does exist (i.e. a result of it being inserted or updated in the PARENT table). In all other circumstances no suppression will take place (e.g. when changing the DEPTNO of an employee or when inserting a new employee).

The following code illustrates how this is achieved for the cascade Update scenario, the code can easily be modified to add the other functionality and encapsulate it all within the same package. The EMP and DEPT table have been used, with the column EMP.DEPTNO in the EMP table referencing DEPT.DEPTNO in the DEPT table.


... the following code ... verweist auf


/*****************************************************************************/

create table DEPT (
DEPTNO number not null,
DNAME varchar2(30),
LOC varchar2(30)
)
/
create table EMP (
EMPNO number primary key,
ENAME varchar2(30) not null,
SAL number,
JOB varchar2(30),
DEPTNO number
)
/

/*****************************************************************************/
/*
The mutation_prevention package is the KEY to the whole solution, it contains
only one variable which will indicate whether integrity checks should be
carried out as a result of an Insert or Update on the foreign key in the EMP
table.
*/

create or replace package MUTATION_PREVENTION as
FIRE_TRIGGER varchar2(5) := 'YES';
end MUTATION_PREVENTION;
/

/*
The package manual_cascade is a general purpose package which can also handle
Insert and Delete functionality
*/

create or replace package MANUAL_CASCADE as
procedure CASCADE_UPDATE(OLD_KEY in number,NEW_KEY in number,RESULT out boolean);
end MANUAL_CASCADE;
/

/*
The package body contains the procedures which will handle all the cascade
functionality, each will accept 3 parameters :
1. old_key -- The Old value which is used to reference CHILD records.
2. new_key -- The New value which is being inserted or substituted.
3. result -- Boolean returning the result of the operation.
Procedures contained in this package will be called by the trigger on the
parent table [call_manual_cascade].
*/

create or replace package body MANUAL_CASCADE as

procedure CASCADE_UPDATE(OLD_KEY in number,NEW_KEY in number,RESULT out boolean) is

LOOP_COUNT number;
DUMMY_2 number;
DUMMY_3 number;
L_RESULT boolean := FALSE;
L_OLD_KEY number;
L_NEW_KEY number;
--
-- Declare cursor c1 to lock all child records which may be related to the
-- PARENT record. The NOWAIT is specified such that the procedure does NOT
-- continue to wait if these records are already locked by another user.
--
cursor C1 (L_DEPT number) is
select EMPNO, DEPTNO from EMP
where DEPTNO = L_DEPT
for update of DEPTNO nowait;
--
-- Declare exceptions to gracefully handle the CURSOR irregularities.
--
RESOURCE_BUSY exception;
INVALID_CURSOR exception;

pragma exception_init(RESOURCE_BUSY, -54);
pragma exception_init(INVALID_CURSOR, -1001);

begin
--
-- Check to see if there are any related child records to be updated. If NOT
-- then exit the IF TEST, otherwise proceed in locking the rows.
--
select count(*) into LOOP_COUNT
from EMP
where DEPTNO = OLD_KEY;

if LOOP_COUNT >= 1 then
--
-- At this stage you could define an REPEAT LOOP which will retry this
-- transaction in case of failure to lock all CHILD records successfully
-- [n] time.
--
begin
--
-- Open the cursor and handle any Error Conditions.
--
open C1 (OLD_KEY);

if not (C1%isopen) then
raise INVALID_CURSOR;
end if;
--
-- If successful then loop and update each row, one at a time until no more
-- rows. Handle Exceptions and Close the cursor.
--
for I in 1..LOOP_COUNT loop
fetch C1 into DUMMY_2, DUMMY_3;

update EMP set DEPTNO = L_NEW_KEY where current of C1;
end loop;

exception
when INVALID_CURSOR then
RESULT := true;
when RESOURCE_BUSY then
RESULT := true;
when others then
raise_application_error(-20006,'General Package Error');
close C1;

end;
end if;
end CASCADE_UPDATE;
end MANUAL_CASCADE;
/

/*
This trigger is on the PARENT table DEPT and controls the value of the global
variable [mutation_prevention.fire_trigger] which will be persistent for the
duration of the user session.
*/

create or replace trigger CALL_MANUAL_CASCADE
after update of DEPTNO on DEPT
for each row

declare
l_result BOOLEAN;
transaction_failed EXCEPTION;
debug_var varchar2(5);
L_RESULT boolean;
TRANSACTION_FAILED exception;
begin
--
-- Set the global control variable indicating to the [TRIGGER EMP_DEPT_CHECK]
-- trigger which performs the foreign key integrity check, that NO check
-- should be performed at this stage.
--
MUTATION_PREVENTION.FIRE_TRIGGER := 'NO';
--
-- Check to see which function is being performed, execute the appropriate
-- procedure in the package and the RESET the value of the global variable
-- to 'YES' to allow subsequent integrity checks to be performed.
--
if updating then
MANUAL_CASCADE.CASCADE_UPDATE(:OLD.DEPTNO, :NEW.DEPTNO, L_RESULT);
MUTATION_PREVENTION.FIRE_TRIGGER := 'YES';
--
-- Check the results from the executed procedure .. act accordingly, If this
-- was a BEFORE update Trigger the OLD value for DEPTNO could be rest if
-- required.
--
if L_RESULT = true then
raise TRANSACTION_FAILED;
end if;
end if;

exception
when TRANSACTION_FAILED then
raise_application_error(-20001,
'Update Of Department '||to_char(:OLD.DEPTNO)||' FAILED');
when others then
raise_application_error(-20004,'GENERAL ERROR');
end;
/

/*
This trigger performs the integrity checking to validate that the foreign key
constraint is NOT violated when inserting and new employee or updating an
employees department.
*/

create or replace trigger EMP_DEPT_CHECK
before update or insert on EMP
for each row
declare
DUMMY number(5);
--
-- Declare a mutating table EXCEPTION, however if this solution works this
-- should never be invoked, but has been included for completeness.
--
MUTATING_TABLE exception;
pragma exception_init (MUTATING_TABLE, -4091);

begin
--
-- Check the Global variable, and either continue processing or EXIT.
--

if MUTATION_PREVENTION.FIRE_TRIGGER = 'YES' then
begin
select 1
into DUMMY
from DEPT
where DEPTNO = :NEW.DEPTNO;
if DUMMY != 1 then
raise NO_DATA_FOUND;
end if;
exception
when NO_DATA_FOUND then
raise_application_error(-20002,
'Department '||to_char(:NEW.DEPTNO)||' is NOT VALID');
when MUTATING_TABLE then
raise_application_error(-20003,
'Table is MUTATING !!!!!!');
end;
end if;

end;
/



Ich hoffe, das hilft Dir weiter.

Gruß!

nefu

nefu
2008-02-24, 21:46:53
Ein weiterer möglicher workaround:


Note:37861.1
Subject: SQL: Example Workaround for ORA-4091 Error
Applies to:
PL/SQL - Version:
Information in this document applies to any platform.
Goal

Oracle does not allow you the reading of a mutating table in a row trigger as it may lead to consistent data.

Attempt to do so, results in the following error:

ORA-04091 Table %s.%s is mutating, trigger/function may not see it

However, this operation can be performed within a statement trigger.

One way to work-around the mutating error within a row level trigger is to use a package PL/SQL table to store ROWIDs of updated records in a row trigger, and reprocess the updated records in a statement trigger.

Solution

Note that there are concurrency issues with this if more than one session tries to perform operations simultaneously. This is not intended as a total solution, but as the framework to help show one option.


The following is the sample work-around that used the EMP table.

Create the package to define the objects that will house the data.



create or replace package emp_pkg as
type emp_tab_type is table of rowid index by binary_integer;
emp_tab emp_tab_type;
emp_index binary_integer;
end emp_pkg;
/



Create the table level trigger to reset the index BEFORE any changes take place.



create or replace trigger emp_bef_stm_all before insert or update or delete on emp
begin
--Remember to reset the pl/sql table before each statement
emp_pkg.emp_index := 0;
end;
/



The following Row level trigger will fire for each row being modified and copy the RowID for each row to the EMP_TAB object defined within the package.



create or replace trigger emp_aft_row_all after insert or update or delete on emp
for each row
begin
--Store the rowid of updated record into global pl/sql table
emp_pkg.emp_index := emp_pkg.emp_index + 1;
emp_pkg.emp_tab(emp_pkg.emp_index) := :new.rowid;
end;
/



The following table level trigger will fire AFTER the others and will print out each RowID affected.



create or replace trigger emp_aft_stm_all after insert or update or delete on emp
begin
for i in 1 .. emp_pkg.emp_index loop
-- Re-process the updated records.
--There is no restriction here.
dbms_output.put_line(emp_pkg.emp_tab(i));
end loop;
emp_pkg.emp_index := 0;
end;
/



In order to see how this work from SQL*Plus, the output must be turned on.



SQL> SET SERVEROUTPUT ON



This first example, displays the RowIDs for Department 20 then issues a DELETE command. The trigger should fire and display the same RowIDs to confirm it is working. (The RowIDs will vary from machine to machine.)



SQL> SELECT ROWID, DEPTNO FROM EMP WHERE DEPTNO = 20;

ROWID DEPTNO
------------------ ----------
AAAM4iAAEAAAAG+AAA 20
AAAM4iAAEAAAAG+AAD 20
AAAM4iAAEAAAAG+AAH 20
AAAM4iAAEAAAAG+AAK 20
AAAM4iAAEAAAAG+AAM 20

SQL> DELETE EMP WHERE DEPTNO = 20;
AAAM4iAAEAAAAG+AAA
AAAM4iAAEAAAAG+AAD
AAAM4iAAEAAAAG+AAH
AAAM4iAAEAAAAG+AAK
AAAM4iAAEAAAAG+AAM



This second example, displays the RowIDs for Department 10 then issues an UPDATE command. The trigger should fire and display the same RowIDs to confirm it is working. (The RowIDs will vary from machine to machine.)



SQL> SELECT ROWID, DEPTNO FROM EMP WHERE DEPTNO=10;

ROWID DEPTNO
------------------ ----------
AAAM4iAAEAAAAG+AAG 10
AAAM4iAAEAAAAG+AAI 10
AAAM4iAAEAAAAG+AAN 10

SQL> UPDATE EMP SET JOB='Test' WHERE DEPTNO=10;
AAAM4iAAEAAAAG+AAG
AAAM4iAAEAAAAG+AAI
AAAM4iAAEAAAAG+AAN


3 rows updated.



To rollback the changes, issue the Rollback command.



SQL> ROLLBACK;

Rollback complete.



References
Note 19879.1 - OERR: ORA 4091 "table %s.%s is mutating, trigger/function may not see it"
Note 74859.1 - ORA-4091 Mutating Table Explanation and Workarounds
Note 37906.1 - Oracle Mutating Table Problem
Note 1019254.6 - ORA-4091 WHEN TRIGGER IS NOT MODIFYING/SELECTING THE TRIGGERING TABLE ABLE %S.%S IS MUTATING, TRIGGER/FUNCTION MAY NOT SEE IT"

AtTheDriveIn
2008-02-25, 11:34:13
Ein weiterer möglicher workaround:


Oha!

Danke erstmal für die Mühe,. Scheint ja ein größeres Problem zu sein. Ich werde mal schaun ob ich noch Zeit finde mich da durchzuarbeiten, ich habe jetzt die Fehlerverursachende Stelle erstmal rausgeschmissen...

nefu
2008-02-25, 14:41:48
Nur damit kein falscher Eindruck entsteht, das ist kein Bug, sondern ein gewolltes Verhalten.
Steht z.B. auch in der oben verlinkten Doku zu Triggern:


Trigger Restrictions on Mutating Tables

A mutating table is a table that is being modified by an UPDATE, DELETE, or INSERT statement, or a table that might be updated by the effects of a DELETE CASCADE constraint.

The session that issued the triggering statement cannot query or modify a mutating table. This restriction prevents a trigger from seeing an inconsistent set of data.

This restriction applies to all triggers that use the FOR EACH ROW clause. Views being modified in INSTEAD OF triggers are not considered mutating.

When a trigger encounters a mutating table, a runtime error occurs, the effects of the trigger body and triggering statement are rolled back, and control is returned to the user or application.



oder auch: http://download.oracle.com/docs/cd/B19306_01/server.102/b14220/triggers.htm#i13313


Die aufgezeigten Workarounds bestehen darin, entweder über eine globale Variable in einem package dem Trigger zu signalisieren, dass er nicht feuern soll (erster workaround) oder wie im zweiten Beispiel die betroffenen Sätze (bzw. deren RowIDs) zwischenzuspeichern und einem postprocessing zu unterziehen.

Gruß!

nefu