"ORA-14450: attempt to access a transactional temp table already in use" in a compound trigger - oracle

I have a table which can hold many records for one account: different amounts.
ACCOUNTID | AMOUNT
id1 | 1
id1 | 2
id2 | 3
id2 | 4
Every time a record in this table is inserted/updated/deleted we need to evaluate an overall amount in order to know if we should trigger or not an event (by inserting data into another table). The amount is computed based on the sum of records (per account) present in this table.
The computation of the amount should use new values of the records, but we need also old values in order to check some conditions (e.g. old value was X - new value is Y: if [X<=threshold and Y>threshold] then trigger event by inserting a record into another table).
So in order to compute and trigger the event, we created a trigger on this table. Something like this:
CREATE OR REPLACE TRIGGER <trigger_name>
AFTER INSERT OR UPDATE OR DELETE OF MOUNT ON <table_name>
FOR EACH ROW
DECLARE
BEGIN
1. SELECT SUM(AMOUNT) INTO varSumAmounts FROM <table_name> WHERE accountid = :NEW.accountid;
2. varAmount := stored_procedure(varSumAmounts);
END <trigger_name>;
The issue is that statement 1. throws the following error: 'ORA-04091: table is mutating, trigger/function may not see it'.
We tried the following but without success (same exception/error) to select all records which have rowId different than current rowId:
(SELECT SUM(AMOUNT)
INTO varSumAmounts
FROM <table_name>
WHERE accountId = :NEW.accountid
AND rowid <> :NEW.rowid;)
in order to compute the amount as the sum of amounts of all rows beside current row + the amount of current row (which we have in the context of the trigger).
We searched for other solutions and we found some but I don’t know which of them is better and what is the downside for each of them (although they are somehow similar)
Use compound trigger
http://www.oracle-base.com/articles/9i/mutating-table-exceptions.php
http://asktom.oracle.com/pls/asktom/ASKTOM.download_file?p_file=6551198119097816936
To avoid 'table is mutating' error based on solutions 1&2, I used a combination of compound triggers with global temporary tables.
Now we have a compound trigger which uses some global temporary tables to store relevant data from :OLD and :NEW pseudo records. Basically we do the next things:
CREATE OR REPLACE TRIGGER trigger-name
FOR trigger-action ON table-name
COMPOUND TRIGGER
-------------------
BEFORE STATEMENT IS
BEGIN
-- Delete data from global temporary table (GTT) for which source is this trigger
-- (we use same global temporary tables for multiple triggers).
END BEFORE STATEMENT;
-------------------
AFTER EACH ROW IS
BEGIN
-- Here we have access to :OLD and :NEW objects.
-- :NEW and :OLD objects are defined only inside ROW STATEMENTS.
-- Save relevant data regarding :NEW and :OLD into GTT table to use it later.
END AFTER EACH ROW;
--------------------
AFTER STATEMENT IS
BEGIN
-- In this block DML operations can be made on table-name(the same table on which
--the trigger is created) safely.
-- Table is mutating error will no longer appear because this block is not for EACH ROW specific.
-- But we can't access :OLD and :NEW objects. This is the reason why in 'AFTER EACH ROW' we saved them in GTT.
-- Because previously we saved :OLD and :NEW data, now we can continue with our business logic.
-- if (oldAmount<=threshold && newAmount>threshold) then
-- trigger event by inserting record into another table
END AFTER STATEMENT;
END trigger-name;
/
The global temporary tables used are created with option 'ON COMMIT DELETE ROWS', this way I make sure that data from this table will be cleaned at the end of the transaction.
Yet, this error occurred: 'ORA-14450: attempt to access a transactional temp table already in use'.
The problem is that the application uses distributed transactions and in oracle documentation is mentioned that:
"A variety of internal errors can be reported when using Global Temporary Tables (GTTs) in conjunction with Distributed or XA transactions.
...
Temporary tables are not supported in any distributed, and therefore XA, coordinated transaction.
The safest option is to not use temporary tables within distributed or XA transactions as their use in this context is not officially supported.
...
A global temporary table can be safely used if there is only single branch transaction at the database using it, but if there are loopback database links or XA transactions involving multiple branches, then problems can occur including block corruption as per Bug 5344322.
"
It's worth mentioning that I can't avoid XA transactions or making DML on same table which is the subject of the trigger (fixing the data model is not a feasible solution). I've tried using instead of the global temporary table a trigger variable - a collection (table of objects) but I am not sure regarding this approach. Is it safe regarding distributed transactions?
Which other solutions will be suitable in this case to fix either initial issue: 'ORA-04091: table name is mutating, trigger/function may not see it', or the second one: 'ORA-14450: attempt to access a transactional temp table already in use'?

You should carefuly check that you code doesn't use autonomous transactions to access temporary table data:
SQL> create global temporary table t (x int) on commit delete rows
2 /
SQL> insert into t values(1)
2 /
SQL> declare
2 pragma autonomous_transaction;
3 begin
4 insert into t values(1);
5 commit;
6 end;
7 /
declare
*
error in line 1:
ORA-14450: attempt to access a transactional temp table already in use
ORA-06512: error in line 4

In case you do a DELETE FROM <temp-table-name> in BEFORE STATEMENT and AFTER STATEMENT is should not matter if you GTT is defined with ON COMMIT PRESERVE ROWS or ON COMMIT DELETE ROWS.
In your trigger you can define a RECORD/TABLE variable. This variable you can initialize in BEFORE STATEMENT block and loop over it in BEFORE STATEMENT block.
Would be something like this:
CREATE OR REPLACE TRIGGER TRIGGER-NAME
FOR TRIGGER-action ON TABLE-NAME
COMPOUND TRIGGER
TYPE GTT_RECORD_TYPE IS RECORD (ID NUMBER, price NUMBER, affected_row ROWID);
TYPE GTT_TABLE_TYPE IS TABLE OF GTT_RECORD_TYPE;
GTT_TABLE GTT_TABLE_TYPE;
-------------------
BEFORE STATEMENT IS
BEGIN
GTT_TABLE := GTT_TABLE_TYPE(); -- init the table variable
END BEFORE STATEMENT;
-------------------
AFTER EACH ROW IS
BEGIN
GTT_TABLE.EXTEND;
GTT_TABLE(GTT_TABLE.LAST) := GTT_RECORD_TYPE(:OLD.ID, :OLD.PRICE, :OLD.ROWID);
END AFTER EACH ROW;
--------------------
AFTER STATEMENT IS
BEGIN
FOR i IN GTT_TABLE.FIRST..GTT_TABLE.LAST LOOP
-- do something with values
END LOOP;
END AFTER STATEMENT;
END TRIGGER-NAME;
/

Related

Insert Data into multiple tables in oracle stored procedure

I have five tables and 5 tables are linked.
1) Rules_Group (GroupId-Primary Key)
2) Rules_Sub_Group (GroupId - Foreign Key from Rules_Group)
3) Rules (SubGroupId - Foreign Key from Rules_Sub_Group)
4) Rules_Expression (RulesId - Foreign Key from Rules)
5) Rules_Expression_Eval (RulesId - Foreign Key from Rules)
How can I write a stored procedure to insert/update records all at a time and if anything fails, I want to roll back the transaction.
What is the best way to do this?
How can I write a stored procedure to insert/update records all at a time and if anything fails, I want to roll back the transaction.
A common way is to establish a SAVEPOINT and, if errors occur, ROLLBACK to the savepoint. That gives you the option of continuing the transaction with the next record, if you like. It also, preserves the transaction to allow you to process a single COMMIT only at the very end (which is good for several reasons which may be controversial and weren't the point of your question).
Here is the general idea:
FOR r IN ( ... select your source data, or maybe load it from a file ... ) LOOP
BEGIN
SAVEPOINT myproc_pre_insert;
INSERT INTO rules_group ...;
INSERT INTO rules_sub_group ...;
INSERT INTO rules ...;
INSERT INTO rules_expression ...;
INSERT INTO rules_expression_eval ...;
EXCEPTION
WHEN others THEN
ROLLBACK TO myproc_pre_insert;
... log error, possibly ...
... either (A) abort the process ...
RAISE;
... or (B) continue with the next record
CONTINUE;
END;
END LOOP;
COMMIT;
Depending on how your tables are and how your source data is, you might also combine some of the five (5) INSERT statements, using INSERT ALL.

Precise difference between statement on Row and on Table

What's the difference between these two blocks and when to use the first or the second?
Create OR Replace trigger trig_before_insert before insert on Employee For each Row
Begin
DBMS_OUTPUT.PUT_LINE('Inserting');
END;
And
Create OR Replace trigger trig_before_insert before insert on Employee
Begin
DBMS_OUTPUT.PUT_LINE('Inserting');
END;
If you perform an
INSERT INTO EMPLOYEE
SELECT ...
and that SELECT returns 100 rows so that the INSERT inserts 100 rows, your first trigger will execute 100 times, once for each row. In the same situation, your second trigger will execute only once.
You can use a BEFORE INSERT...FOR EACH ROW trigger to change the values that are being inserted by accessing them via the :NEW variable. E.g.,
:new.column_1 := 'a different value';
You cannot do that in a statement level trigger (which is what your 2nd trigger is).
There are also limitations in row level triggers (which is what your 1st trigger is). In particular, you may not SELECT from the trigger's base table (EMPLOYEES in this case), because that table is said to be "mutating". The exact reasons, as I understand them, go back to the core principles of relational databases -- specifically that the results of a statement (like INSERT INTO...SELECT) should not depend on the order in which the rows are processed. There are workarounds to this limitation, however, which are beyond the scope of your original question, I think.

Mutating error on after insert trigger

The below code is giving a mutating error.
Can any1 pls help in solving this.
CREATE OR REPLACE TRIGGER aso_quote_cuhk_trigger
BEFORE INSERT
ON aso.aso_quote_headers_all
FOR EACH ROW
BEGIN
UPDATE aso.aso_quote_headers_all
SET quote_expiration_date=sysdate+90
where quote_header_id=:new.quote_header_id;
END;
/
In oracle there are two levels of triggers: row level and table level.
Row level triggers are executed for each row. Table level triggers executed per statement, even if a statement changed more then one row.
In a row level trigger, you cannot select/update the table itself that has the trigger: you will get a mutating error.
In this case, there is no need for an UPDATE statement. Just try this:
CREATE OR REPLACE TRIGGER aso_quote_cuhk_trigger
BEFORE INSERT
ON aso.aso_quote_headers_all
FOR EACH ROW
BEGIN
:new.quote_expiration_date=sysdate+90;
END;
/
EDIT Rajesh mentioned it is possible, that before inserting a new row, OP wants to update all other records in the aso_quote_headers_all table.
Well, this is feasible, but it's a little tricky. To do this properly, you will need
A pl/sql package and a variable in the package header that is modified by the triggers. This variable could be a list holding the IDs of newly inserted records. Row level after insert trigger would add a new ID to the list. The content of this package variable will be different for each different session, so let's call this variable session_variable.
Row level after insert trigger, that would add new ID to the session_variable.
Table level after insert trigger that would get IDs from the session_variable, process the ID and then remove it from the session_variable. This trigger could execute necessary selects/updates on the aso_quote_headers_all. After a newly inserted ID is processed, this trigger should make sure it gets removed from the session_variable.
I realise you must have resolved your issue by now. However I am adding this answer below to help anyone else facing similar problem as you and I faced.
I recently encountered mutating table (ORA-04091: table XXXX is mutating, trigger/function may not see it) issue and after searching around realised the Compound Triggers feature available in 11g. If you're on 11g following compound trigger would have solved your issue.
CREATE OR REPLACE TRIGGER aso_quote_cuhk_trigger
FOR INSERT ON aso.aso_quote_headers_all
COMPOUND TRIGGER
row_id rowid;
AFTER EACH ROW IS
BEGIN
row_id := :new.rowid;
END AFTER EACH ROW;
AFTER STATEMENT IS
BEGIN
UPDATE aso.aso_quote_headers_all
SET quote_expiration_date = sysdate+90
WHERE rowid = row_id;
END AFTER STATEMENT;
END aso_quote_cuhk_trigger;
/
A word about how it works. This compound trigger fires 2 events :
First is AFTER EACH ROW where we capture the rowid of newly inserted row
Next is AFTER STATEMENT where we update the table using rowid (captured during first event) in the WHERE clause.
A useful link if you want to read more about Compound Triggers.

Can I copy :OLD and :NEW pseudo-records in/to an Oracle stored procedure?

I have an AFTER INSERT OR UPDATE OR DELETE trigger that I'm writing to store every record revision that occurs in a certain table, by copying the INSERT and UPDATE :NEW values into a mirror table, and for DELETE the :OLD values.
I could un-clutter my code considerably by conditionally passing either the :NEW or :OLD record into a procedure which would then do the insert into my history table. Unfortunately I cannot seem to find a way to pass the entire :OLD or :NEW record.
Am I missing something or is there no way to avoid enumerating every :NEW and :OLD column as I invoke my insert procedure?
I want to do the following:
DECLARE
PROCEDURE LOCAL_INSERT(historyRecord in ACCT.ACCOUNTS%ROWTYPE) IS
BEGIN
INSERT INTO ACCT.ACCOUNTS_HISTORY (ID, NAME, DESCRIPTION, DATE) VALUES (historyRecord.ID, historyRecord.NAME, historyRecord.DESCRIPTION, SYSDATE);
END;
BEGIN
IF INSERTING OR UPDATING THEN
LOCAL_INSERT(:NEW);
ELSE --DELETING
LOCAL_INSERT(:OLD);
END IF;
END;
But I'm stuck doing this:
DECLARE
PROCEDURE LOCAL_INSERT(id in ACCT.ACCOUNTS.ID%TYPE,
name in ACCT.ACCOUNTS.NAME%TYPE,
description in ACCT.ACCOUNTS.DESCRIPTION%TYPE) IS
BEGIN
INSERT INTO ACCT.ACCOUNTS_HISTORY (ID, NAME, DESCRIPTION, DATE) VALUES (id, name, description, SYSDATE);
END;
BEGIN
IF INSERTING OR UPDATING THEN
LOCAL_INSERT(:NEW.ID, :NEW.NAME, :NEW.DESCRIPTION);
ELSE --DELETING
LOCAL_INSERT(:OLD.ID, :OLD.NAME, :OLD.DESCRIPTION);
END IF;
END;
Okay, so it doesn't look like a big difference, but this is just an example with 3 columns rather than dozens.
It isn't. You have to do it yourself through enumeration.
The reasons it can't/doesn't work automatically include:
the :old and :new are default conventions; you can name the :old and :new references to be whatever you want through the REFERENCING clause of the CREATE TRIGGER statement.
you'd have to have a public declaration of a type (through CREATE TYPE or through a package declaration) to be able to use it as an argument to another piece of code.
trigger code is interpreted code, not compiled code.
I don't think it's possible like that. Documentation doesn't mention anything like that.
This would certainly cost performance, but you could try to define your trigger AFTER INSERT and another one BEFORE UPDATE OR DELETE, and in the trigger do something like:
SELECT *
INTO rowtype_variable
FROM accounts
WHERE accounts.id = :NEW.id; -- :OLD.id for UPDATE and DELETE
and then call your procedure with that rowtype_variable.
Use SQL to generate the SQL;
select ' row_field.'||COLUMN_NAME||' := :new.'||COLUMN_NAME||';' from
ALL_TAB_COLUMNS cols
where
cols.TABLE_NAME = 'yourTableName'
order by cols.column_name.
Then copy and paste output.
If you use AFTER trigger you can use rowid as parameter to call procedure
insert into t_hist
select * from t where rowid = r;
If you use BEFORE trigger you will get ORA-04091 mutating table, BUT you solution can be (http://www.dba-oracle.com/t_avoiding_mutating_table_error.htm):
Don't use triggers - The best way to avoid the mutating table error is not to use triggers. While the object-oriented Oracle provides "methods" that are associated with tables, most savvy PL/SQL developers avoid triggers unless absolutely necessary.
Use an "after" or "instead of" trigger - If you must use a trigger, it's best to avoid the mutating table error by using an "after" trigger, to avoid the currency issues associated with a mutating table. For example, using a trigger ":after update on xxx", the original update has completed and the table will not be mutating.
Re-work the trigger syntax - Dr. Hall has some great notes on mutating table errors, and offers other ways to avoid mutating tables with a combination of row-level and statement-level triggers.
Use autonomous transactions - You can avoid the mutating table error by marking your trigger as an autonomous transaction, making it independent from the table that calls the procedure.

ORA-04091: table [blah] is mutating, trigger/function may not see it

I recently started working on a large complex application, and I've just been assigned a bug due to this error:
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'
The trigger in question looks like
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;
The table has a primary key "t1_pk", an "appointment event id"
t1_appnt_evnt_id and another column "t1_prnt_t1_pk" which may or may
not contain another row's t1_pk.
It appears the trigger is trying to make sure that nobody else with the
same t1_appnt_evnt_id has referred to the same one this row is referring to a referral to another row, if this one is referring to another row.
The comment on the bug report from the DBA says "remove the trigger, and perform the check in the code", but unfortunately they have a proprietary code generation framework layered on top of Hibernate, so I can't even figure out where it actually gets written out, so I'm hoping that there is a way to make this trigger work. Is there?
I think I disagree with your description of what the trigger is trying to
do. It looks to me like it is meant to enforce this business rule: For a
given value of t1_appnt_event, only one row can have a non-NULL value of
t1_prnt_t1_pk at a time. (It doesn't matter if they have the same value in the second column or not.)
Interestingly, it is defined for UPDATE OF t1_appnt_event but not for the other column, so I think someone could break the rule by updating the second column, unless there is a separate trigger for that column.
There might be a way you could create a function-based index that enforces this rule so you can get rid of the trigger entirely. I came up with one way but it requires some assumptions:
The table has a numeric primary key
The primary key and the t1_prnt_t1_pk are both always positive numbers
If these assumptions are true, you could create a function like this:
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;
and an index like this:
CREATE UNIQUE INDEX my_index ON my_table
( t1_appnt_event, f( t1_prnt_t1_pk, primary_key_column) );
So rows where the PMNT column is NULL would appear in the index with the inverse of the primary key as the second value, so they would never conflict with each other. Rows where it is not NULL would use the actual (positive) value of the column. The only way you could get a constraint violation would be if two rows had the same non-NULL values in both columns.
This is perhaps overly "clever", but it might help you get around your problem.
Update from Paul Tomblin: I went with the update to the original idea that igor put in the comments:
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);
I agree with Dave that the desired result probalby can and should be achieved using built-in constraints such as unique indexes (or unique constraints).
If you really need to get around the mutating table error, the usual way to do it is to create a package which contains a package-scoped variable that is a table of something that can be used to identify the changed rows (I think ROWID is possible, otherwise you have to use the PK, I don't use Oracle currently so I can't test it). The FOR EACH ROW trigger then fills in this variable with all rows that are modified by the statement, and then there is an AFTER each statement trigger that reads the rows and validate them.
Something like (syntax is probably wrong, I haven't worked with Oracle for a few years)
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;
With any trigger-based (or application code-based) solution you need to
put in locking to prevent data corruption in a multi-user environment.
Even if your trigger worked, or was re-written to avoid the mutating table
issue, it would not prevent 2 users from simultaneously updating
t1_appnt_evnt_id to the same value on rows where t1_appnt_evnt_id is not
null: assume there are currenly no rows where t1_appnt_evnt_id=123 and
t1_prnt_t1_pk is not null:
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;
You now have a corrupted database!
The way to avoid this (in trigger or application code) would be to lock
the parent row in the table referenced by t1_appnt_evnt_id=123 before performing the check:
select appe_id
into v_app_id
from parent_table
where appe_id = :new.t1_appnt_evnt_id
for update;
Now session 2's trigger must wait for session 1 to commit or rollback before it performs the check.
It would be much simpler and safer to implement Dave Costa's index!
Finally, I'm glad no one has suggested adding PRAGMA AUTONOMOUS_TRANSACTION to your trigger: this is often suggested on forums and works in as much as the mutating table issue goes away - but it makes the data integrity problem even worse! So just don't...
I had similar error with Hibernate. And flushing session by using
getHibernateTemplate().saveOrUpdate(o);
getHibernateTemplate().flush();
solved this problem for me. (I'm not posting my code block as I was sure that everything was written properly and should work - but it did not until I added the previous flush() statement). Maybe this can help someone.

Resources