In Oracle (11.2.0.1) I have 2 tables, let's say A and B with a referencial constraint between them ("on delete cascade"). A is a "parent" for B.
create table A (id number(9) not null primary key);
create table B (id number(9) not null primary key, parent_id not null references A(id) on delete cascade);
The child table also has a related compound trigger on deleting from it (The main purpose is to avoid mutation errors in a series of complex integrity checks).
create or replace trigger trg_b
for delete on b
compound trigger
type b_table is table of b%rowtype;
b_collection b_table := b_table();
before each row is
begin
b_collection.extend;
b_collection(b_collection.last).id := :old.id;
b_collection(b_collection.last).parent_id := :old.parent_id;
end before each row;
after statement is
begin
for i in b_collection.first .. b_collection.last loop
--logging into another table in an autonomous transaction
end loop;
b_collection.delete;
end after statement;
end trg_b;
One of the algorithms to work with these tables is following:
All rows with the specific parent_id will be deleted from B.
Additional check if no child rows are left.
Delete this specific id from the parent A.
In our production enviroment Step 3 generates an exception ORA-06502: PL/SQL: numeric or value error at string b_collection.first .. b_collection.last. It means a deletion from the parent table leads to firing the trigger on the child table even though there are no possible child rows on affect. It could make some sense since the collection doesn't exist when trigger is fired and the first and last collection indexes are NULL, but I just can not reproduce such behaviour.
In our dev enviroment I only get an ORA-06502 when I try to delete some non-existent id/parent_id from B. In its turns when I try to delete some existent (with no child rows) or non-existent id from A I get no errors - 0 rows deleted, 0 child rows affected. From logging I could tell the child related trigger isn't even fired in such cases. Why not?
Any ideas why the described behaviour may be so different? Did I miss something?
I figured it out. The Bug 8830338 - BEFORE and AFTER STATEMENT not executed in compound trigger for DELETE CASCADE (Doc ID 8830338.8) for 11.2.0.1 was involved into my fun with triggers. It's claimed to be fixed since the patchset 11.2.0.2, ever since such a compound trigger with an after statement should've been fired and ended up in a bad way.
You will get the ORA-06502: PL/SQL: numeric or value error exception whenever your collection is empty:
SQL> declare
2 type t is table of number;
3 numtab t := t();
4 begin
5 for i in numtab.first..numtab.last loop
6 null;
7 end loop;
8 end;
9 /
declare
*
ERROR at line 1:
ORA-06502: PL/SQL: numeric or value error
ORA-06512: at line 5
Unless there is any way the collection could end up being sparse (i.e. have some members deleted from the middle) I would use this loop instead:
1 declare
2 type t is table of number;
3 numtab t := t();
4 begin
5 for i in 1..numtab.count loop
6 null;
7 end loop;
8* end;
SQL> /
PL/SQL procedure successfully completed.
Otherwise you need to test whether the collection is empty or not before processing it (which always seems excessively cumbersome to me when collections are known to be dense):
1 declare
2 type t is table of number;
3 numtab t := t();
4 begin
5 if numtab.count > 0 then
6 for i in numtab.first..numtab.last loop
7 null;
8 end loop;
9 end if;
10* end;
SQL> /
PL/SQL procedure successfully completed.
instead of delete cascade , write your own trigger before delete on master table to delete the Childs records . Then the delete compound trigger will be fired .
Related
As discussed here, I'm unable to use :OLD and :NEW on columns with collation other than USING_NLS_COMP. I'm trying to find a way around this but haven't been successful so far.
This is the original trigger:
CREATE OR REPLACE TRIGGER SYS$PERSONSSALUTATIONAU
AFTER UPDATE ON PERSONS
FOR EACH ROW
begin
State_00.Salutations_ToDelete(State_00.Salutations_ToDelete.Count + 1) := :old.SalutationTitle;
State_00.Salutations_ToInsert(State_00.Salutations_ToInsert.Count + 1) := :new.SalutationTitle;
end;
This is what I've tried:
CREATE OR REPLACE TRIGGER SYS$PERSONSSALUTATIONAU
FOR UPDATE ON Persons
COMPOUND TRIGGER
TYPE Persons_Record IS RECORD (
SalutationTitle NVARCHAR2(30)
);
TYPE Persons_Table IS TABLE OF Persons_Record INDEX BY PLS_INTEGER;
gOLD Persons_Table;
gNEW Persons_Table;
BEFORE EACH ROW IS BEGIN
SELECT SalutationTitle
BULK COLLECT INTO gOLD
FROM Persons
WHERE ID = :OLD.ID;
END BEFORE EACH ROW;
AFTER EACH ROW IS BEGIN
SELECT SalutationTitle
BULK COLLECT INTO gNEW
FROM Persons
WHERE ID = :NEW.ID;
END AFTER EACH ROW;
AFTER STATEMENT IS BEGIN
FOR i IN 1 .. gNEW.COUNT LOOP
State_00.Salutations_ToDelete(State_00.Salutations_ToDelete.Count + 1) := gOLD(i).SalutationTitle;
State_00.Salutations_ToInsert(State_00.Salutations_ToInsert.Count + 1) := gNEW(i).SalutationTitle;
END LOOP;
END AFTER STATEMENT;
END;
This results in error ORA-04091. I've also tried moving the select into the AFTER STATEMENT section which works, but there is no way to access the old values. If somebody has a solution for this it would be most appreciated.
EDIT:
I created a minimal reproducible example:
CREATE TABLE example_table (
id VARCHAR2(10),
name NVARCHAR2(100)
);
CREATE TABLE log_table (
id VARCHAR2(10),
new_name NVARCHAR2(100),
old_name NVARCHAR2(100)
);
CREATE OR REPLACE TRIGGER example_trigger
AFTER UPDATE ON example_table
FOR EACH ROW BEGIN
INSERT INTO log_table VALUES(:old.id, :new.name, :old.name);
END;
INSERT INTO example_table VALUES('01', 'Daniel');
-- this works as expected
UPDATE example_table SET name = ' John' WHERE id = '01';
SELECT * FROM log_table;
DROP TABLE example_table;
CREATE TABLE example_table (
id VARCHAR2(10),
-- this is the problematic part
name NVARCHAR2(100) COLLATE XCZECH_PUNCTUATION_CI
);
INSERT INTO example_table VALUES('01', 'Daniel');
-- here nothing is inserted into log_example, if you try to
-- recompile the trigger you'll get error PLS-00049
UPDATE example_table SET name = ' John' WHERE id = '01';
SELECT * FROM log_table;
DROP TABLE example_table;
DROP TABLE log_table;
DROP TRIGGER example_trigger;
In the discussion you reference a document concerning USING_NLS_COMP. That has nothing to do with the error you are getting. The error ORA-04091 is a reference to the table that fired the trigger (mutating). More to come on this. I am not saying you do not have USING_NLS_COMP issues, just that they are NOT causing the current error.
There are misconceptions shown in your trigger. Beginning with the name itself; you should avoid the prefix SYS. This prefix is used by Oracle for internal objects. While SYS prefix is not specifically prohibited at best it causes confusion. If this is actually created in the SYS schema then that in itself is a problem. Never use SYS schema for anything.
There is no reason to create a record type containing a single variable, then create a collection of that type, and finally define variables of the collection. Just create a collection to the variable directly, and define variables of the collection.
The bulk collect in the select statements is apparently misunderstood as used. I assume you want to collect all the new and old values in the collections. Bulk collect however will not do this. Each time bulk collect runs the collection used is cleared and repopulated. Result being the collection contains only the only the LAST population. Assuming id is unique the each collection would contain only 1 record. And now that brings us to the heart of the problem.
The error ORA-04091: <table name> is mutating, trigger/function may not see it results from attempting to SELECT from the table that fired the trigger; this is invalid. In this case the trigger fired due to a DML action on the persons table as a result you cannot select from persons in a row level trigger (stand alone or row level part of a compound trigger. But it is not needed. The pseudo rows :old and :new contain the complete image of the row. To get a value just reference the appropriate row and column name. Assign that to your collection.
Taking all into account we arrive at:
create or replace trigger personssalutation
for update
on persons
compound trigger
type persons_table is table of
persons.salutationtitle%type;
gold persons_table := persons_table();
gnew persons_table := persons_table();
before each row is
begin
gold.extend;
gold(gold.count) := :old.salutationtitle;
end before each row;
after each row is
begin
gnew.extend;
gold(gold.count) := :new.salutationtitle;
end after each row;
after statement is
begin
for i in 1 .. gnew.count loop
state_00.salutations_todelete(state_00.salutations_todelete.count + 1) := gold(i);
state_00.salutations_toinsert(state_00.salutations_toinsert.count + 1) := gnew(i);
end loop;
end after statement;
end personssalutation;
NOTE: Unfortunately you did not provide sample data, nor description of the functions in the AFTER STATEMENT section. Therefore the above is not tested.
I am currently trying to delete all rows of a table that is linked to another one via foreign key.
My Code looks kinda like this:
CREATE OR REPLACE Procedure test
BEGIN
DELETE FROM person;
End;
/
The Errors says: ora-02292 integrity constraint violated - child record found
When I try to just diable the constraints then it says i cant 'ALTER' the table.
What do i have to do/change?
You shouldn't just disable constraint, because you'll leave child records orphans (there will be NO parent record for them). What will you do, then?
Correct way to handle it is to
delete children first
delete parents last
If foreign key constraint was created with the on delete cascade option, database would handle it for you.
P.S. As of "you can't ALTER the table" - that's not an Oracle error message. They have their codes, such as ORA-06550. It is difficult to guess what you actually did, and - if I had to guess - I'd say that you tried to do that within the procedure:
SQL> create table temp (id number constraint pkt primary key);
Table created.
SQL> begin
2 alter table temp disable constraint pkt;
3 end;
4 /
alter table temp disable constraint pkt;
*
ERROR at line 2:
ORA-06550: line 2, column 3:
PLS-00103: Encountered the symbol "ALTER" when expecting one of the following:
( begin case declare exit for goto if loop mod null pragma
raise return select update while with <an identifier>
<a double-quoted delimited-identifier> <a bind variable> <<
continue close current delete fetch lock insert open rollback
savepoint set sql execute commit forall merge pipe purge
You'll need dynamic SQL to do that:
SQL> begin
2 execute immediate 'alter table temp disable constraint pkt';
3 end;
4 /
PL/SQL procedure successfully completed.
SQL>
But, once again, that's not the way you should handle this situation.
I want to solve this:
Create a stored procedure named insert_num_proc that will insert numbers into the FLOAT_SAMPLE table.
This procedure should have two parameters, one for each of the two columns in this table.
The procedure should check the float_id column to ensure there is no duplicate (as unique values are not enforced in this column). If
there is a duplicate float_id, then the row should not be inserted,
instead printing a message saying “Duplicate value in float_id”.
Below is my query. For the second query, I execute my stored procedure to see if it shows my application error message, and it is not working.
CREATE OR REPLACE PROCEDURE insert_num_proc
(
float_id_param float_sample.float_id%TYPE,
float_value_param float_sample.float_value%TYPE)
AS
float_checker float_sample.float_id%TYPE;
BEGIN
SELECT float_id INTO float_checker FROM float_sample;
INSERT INTO float_sample (float_id, float_value)
VALUES
(float_id_param, float_value_param);
IF float_checker = float_id_param THEN
RAISE_APPLICATION_ERROR(-20001, 'Duplicate value is inserted.');
END IF;
END;
/
EXECUTE insert_num_proc(3,2);
Below is error message I get
Error report - ORA-01422: exact fetch returns more than requested
number of rows ORA-06512: at "DL29232.INSERT_NUM_PROC", line 9
ORA-06512: at line 1
01422. 00000 - "exact fetch returns more than requested number of rows"
*Cause: The number specified in exact fetch is less than the rows returned.
*Action: Rewrite the query or change number of rows requested
You are missing a WHERE clause to check if that id exists, so it returns multiple rows which the single static variable float_checker cannot hold.
Another problem is that if you don't have an entry already in the table, your select statement will fail with no_data_found exception.
So, define your float_checker differently so that it will save the count.
Further, your INSERT should come after the IF condition
CREATE OR REPLACE PROCEDURE insert_num_proc (
float_id_param float_sample.float_id%TYPE,
float_value_param float_sample.float_value%TYPE
) AS
float_checker INTEGER := 0;
BEGIN
SELECT COUNT(*)
INTO float_checker
FROM float_sample WHERE
float_id = float_id_param; --where clause for the passed id.
IF float_checker > 0
THEN
raise_application_error(-20001,'Duplicate value is inserted.');
END IF;
INSERT INTO float_sample (
float_id,
float_value
) VALUES (
float_id_param,
float_value_param
);
END;
/
Since this is an exercise, it's ok to use this code. But, in a real time scenario, the 3rd requirement should be handled by a UNIQUE CONSTRAINT on the table.
I am facing an issue wherein thru JMeter if I try to insert same record from two different transactions and at the same time (even the same second) then duplicate records appear in a table temp_tab . Even though we have trigger deployed to to avoid duplicate records getting inserted into temp_tab table. Due to design limitation we cannot use constraints on this table.
Need your valuable suggestion on this issue.
Below is the trigger code
SELECT COUNT(1) INTO row_c
FROM temp_tab
WHERE offer_id = oiv_pkg.trig_tab(idx).offer_id
AND view_id != oiv_pkg.trig_tab(idx).view_id
AND offer_inst_id != oiv_pkg.trig_tab(idx).offer_inst_id
AND subscr_no = oiv_pkg.trig_tab(idx).subscr_no
AND subscr_no_resets = oiv_pkg.trig_tab(idx).subscr_no_resets
AND view_status IN (view_types.cPENDING, view_types.cCURRENT)
AND disconnect_reason IS NULL
AND ((oiv_pkg.trig_tab(idx).active_dt >= active_dt AND
(oiv_pkg.trig_tab(idx).active_dt < inactive_dt OR inactive_dt IS NULL)) OR
(oiv_pkg.trig_tab(idx).active_dt < active_dt AND
(oiv_pkg.trig_tab(idx).inactive_dt IS NULL OR
oiv_pkg.trig_tab(idx).inactive_dt > active_dt)));
IF row_c > 0 THEN
oiv_pkg.trig_tab.DELETE;
raise_application_error (-20001, '269901, TRIG: INSERT Failed: OID: ' || oiv_pkg.trig_tab(idx).offer_inst_id ');
END IF;
If you really want to prevent duplicates without using the proper solution, a constraint, you'd need to implement some sort of locking mechanism. In this example, I'll create a table foo with a single column col1 and create a couple of triggers that ensure that the data in col1 is unique. In order to do this, I'm introducing a new table that exists just to have its single row locked to provide a serialization mechanism. Note that I'm only handling insert operations, I'm ignoring updates that create duplicates. I'm also simplifying the problem by not bothering to track which rows are inserted in row-level triggers in order to make the final check more efficient. Of course, serializing insert operations on your table will absolutely crush you application's scalability.
SQL> create table foo( col1 number );
Table created.
SQL> create table make_application_slow(
2 dummy varchar2(1)
3 );
Table created.
SQL> insert into make_application_slow values( 'A' );
1 row created.
SQL> ed
Wrote file afiedt.buf
1 create or replace trigger trg_foo_before_stmt
2 before insert on foo
3 declare
4 l_dummy varchar2(1);
5 begin
6 -- Ensure that only one session can ever be inserting data
7 -- at any time. This is a great way to turn a beefy multi-core
8 -- server into a highly overloaded server with one effective
9 -- core.
10 select dummy
11 into l_dummy
12 from make_application_slow
13 for update;
14* end;
SQL> /
Trigger created.
SQL> create or replace trigger trg_foo_after_stmt
2 after insert on foo
3 declare
4 l_cnt pls_integer;
5 begin
6 select count(*)
7 into l_cnt
8 from( select col1, count(*)
9 from foo
10 group by col1
11 having count(*) > 1 );
12
13 if( l_cnt > 0 )
14 then
15 raise_application_error( -20001, 'Duplicate data in foo is not allowed.' );
16 end if;
17 end;
18 /
Now, if you try to insert data with the same col1 value in two different sessions, the second session will block indefinitely waiting for the first session to commit (or rollback). That prevents duplicates but it is generally hideously inefficient. And if there is any possibility that a user would be able to walk away from an active transaction, your DBA will curse you for building an application that forces them to constantly kill sessions when someone locks up the entire application because they went to lunch without committing their work.
I want to calculate the average of values in a column of my table (PRA_COEFF) (RAPPORT_VISITE) for a field (PRA_NUM) given, when I add or change a row of it. Then I want to save this value in another table (PRACTITIONER) to the row where PRA_NUM worth PRA_NUM given above.
CREATE TABLE "RAPPORT_VISITE"
(
"RAP_NUM" NUMBER (10,0),
"PRA_NUM" NUMBER (10,0),
"PRA_COEFF" NUMBER (10,0),
)
CREATE TABLE "PRATICIEN"
(
"PRA_NUM" NUMBER (10,0),
"PRA_COEFCONF" NUMBER
)
The trigger is called when adding or Modification RAPPORT_VISITE the table. I tried like this, but I can not retrieve the row affected by the trigger, and thus PRA_NUM, that I need to read.
create or replace TRIGGER UDPATE_PRAT_COEFCONF
AFTER INSERT or UPDATE ON RAPPORT_VISITE
DECLARE
somme NUMBER;
nb NUMBER;
moyenne NUMBER;
rapport NUMBER;
pra_id NUMBER;
BEGIN
/*SELECT MAX(RAP_NUM) INTO rapport FROM RAPPORT_VISITE; // Not want I need in case where I modify a row... */
SELECT PRA_NUM INTO pra_id FROM RAPPORT_VISITE WHERE RAP_NUM=rapport;
SELECT SUM(PRA_COEFF) INTO somme FROM RAPPORT_VISITE WHERE PRA_NUM=pra_id;
SELECT COUNT(*) INTO nb FROM RAPPORT_VISITE WHERE PRA_NUM=pra_id;
IF (nb != 0) THEN
moyenne := somme/nb;
moyenne := TRUNC (moyenne,1);
UPDATE PRATICIEN SET PRA_COEFCONF=moyenne WHERE PRA_NUM=pra_id;
END IF;
END;
Here are 2 limits that triggers are usually have:
when invoked for the all affected records, you don't know what exactly have changed
when invoked for individual records (FOR EACH ROW), you are limited with access to the modified table
To address that limitation, starting from Oracle 11g, we can use compound trigger:
CREATE OR REPLACE TRIGGER <trigger-name>
FOR <trigger-action> ON <table-name>
COMPOUND TRIGGER
-- Global declaration.
g_global_variable VARCHAR2(10);
-- block 1
BEFORE STATEMENT IS
BEGIN
NULL; -- Do something here.
END BEFORE STATEMENT;
-- block 2
BEFORE EACH ROW IS
BEGIN
NULL; -- Do something here.
END BEFORE EACH ROW;
-- block 3
AFTER EACH ROW IS
BEGIN
NULL; -- Do something here.
END AFTER EACH ROW;
-- block 4
AFTER STATEMENT IS
BEGIN
NULL; -- Do something here.
END AFTER STATEMENT;
END <trigger-name>;
And looks like this is what you need. In block 1, initialize your variables, in block 2 or 3 collect changes from individual rows, then in block 4 use that information to create the rest of the business logic.
If we are limited by 10g, then we can emulate compound trigger using package variables.
This solution is limited, because package variables are global for the session. If withing a session you have 2 similar operations, their results would be merged.
Here is the solution
You will have 3 separate triggers, that would represent block 1, (2 or 3), and 4 from the trigger above.
You will have a package with variable g_global_variable (from above)
3 actions:
1. in trigger for block 1 initiate g_global_variable
2. in trigger for block 2 or 3, populate it with actual values
3. in trigger for block 4, create your logic
Ofcourse, g_global_variable could be not alone, it could be record or collection.