INSERT trigger for inserting record in same table - oracle

I have a trigger that is fire on inserting a new record in table in that i want to insert new record in the same table.
My trigger is :
create or replace trigger inst_table
after insert on test_table referencing new as new old as old
for each row
declare
df_name varchar2(500);
df_desc varchar2(2000);
begin
df_name := :new.name;
df_desc := :new.description;
if inserting then
FOR item IN (SELECT pid FROM tbl2 where pid not in(1))
LOOP
insert into test_table (name,description,pid) values(df_name,df_desc,item.pid);
END LOOP;
end if;
end;
its give a error like
ORA-04091: table TEST_TABLE is mutating, trigger/function may not see it
i think it is preventing me to insert into same table.
so how can i insert this new record in to same table.
Note :- I am using Oracle as database

Mutation happens any time you have a row-level trigger that modifies the table that you're triggering on. The problem, is that Oracle can't know how to behave. You insert a row, the trigger itself inserts a row into the same table, and Oracle gets confused, cause, those inserts into the table due to the trigger, are they subject to the trigger action too?
The solution is a three-step process.
1.) Statement level before trigger that instantiates a package that will keep track of the rows being inserted.
2.) Row-level before or after trigger that saves that row info into the package variables that were instantiated in the previous step.
3.) Statement level after trigger that inserts into the table, all the rows that are saved in the package variable.
An example of this can be found here:
http://asktom.oracle.com/pls/asktom/ASKTOM.download_file?p_file=6551198119097816936
Hope that helps.

I'd say that you should look at any way OTHER than triggers to achieve this. As mentioned in the answer from Mark Bobak, the trigger is inserting a row and then for each row inserted by the trigger, that then needs to call the trigger to insert more rows.
I'd look at either writing a stored procedure to create the insert or just insert via a sub-query rather than by values.
Triggers can be used to solve simple problems but when solving more complicated problems they will just cause headaches.
It would be worth reading through the answers to this duplicate question posted by APC and also these this article from Tom Kyte. BTW, the article is also referenced in the duplicate question but the link is now out of date.

Although after complaining about how bad triggers are, here is another solution.
Maybe you need to look at having two tables. Insert the data into the test_table table as you currently do. But instead of having the trigger insert additional rows into the test_table table, have a detail table with the data. The trigger can then insert all the required rows into the detail table.
You may again encounter the mutating trigger error if you have a delete cascade foreign key relationship between the two tables so it might be best to avoid that.

Related

Copy row to history table before update without enumerating column names in PL/SQL trigger?

I am trying to create a PL/SQL trigger that copies the current version of a row into a history table when the row is updated. This can be easily done like this:
CREATE OR REPLACE TRIGGER foo BEFORE UPDATE ON bar FOR EACH ROW
BEGIN
INSERT INTO bar_history VALUES bar.id = :old.id, bar.col1 = :old.col1 /* ...and so on */;
END;
However, I would like to avoid enumerating all the column names since the tables bar and bar_history are identical and I don't want to update the trigger every time I change the table. I have tried two approaches, none of which are working. Is there any other way to solve this?
Approach 1:
CREATE OR REPLACE TRIGGER foo BEFORE UPDATE ON bar FOR EACH ROW
BEGIN
INSERT INTO bar_history VALUES :old;
END;
Since you apparently can not use :old as a rowtype (see this question) I get the following error message:
PLS-00049: bad bind variable 'OLD'
Approach 2:
CREATE TRIGGER foo BEFORE UPDATE ON bar FOR EACH ROW
BEGIN
INSERT INTO bar_history
SELECT * FROM bar WHERE id = :old.id;
END;
This also gives me an error:
ORA-04091: BAR is mutating, trigger/function may not see it
You need to reference the individual columns. There is no way to reference the entire row in the trigger. If you need this for many tables or your table definition changes frequently, you could write pl/sql based on the data dictionary views to generate the trigger for you.
Update: Similar question/answer here: In an Oracle trigger, can I assign new and old to a rowtype variable?
Some people have run into this situation before.
Their approach - use package global variables to store rowid and dynamically select column names in after statement trigger.
https://community.oracle.com/message/370167
Tom Kyte also has some advice on this
https://asktom.oracle.com/pls/apex/f?p=100:11:0::::P11_QUESTION_ID:734825535375
https://asktom.oracle.com/pls/asktom/f?p=100:11:::::P11_QUESTION_ID:59412348055
In this case you can create partition table using list partitioning strategy (one for current C and history H). You can create instead of trigger, in case of update of the table, record will be inserted and existing record status will be updated to H - which implicitly move the record to historical partition.

Dynamically read the columns of the :NEW object in an oracle trigger

I have an oracle trigger that needs to copy values from the updated table to another table.
The problem is that the columns aren't known when the trigger is created. Part of this system allows the table schema to be updated by the application. (don't ask).
Essentially what I want to do is pivot the table to another table.
I have a stored procedure that will do the pivot, but I can't call it as part of the trigger because it does a select on the table being updated. Causing a "mutating" error.
What would be ideal would be to create a dynamic scripts that reads all the column names from user_tab_cols for the updated table, and reads the value from the :new object.
But of course...I can't :)
:NEW doesn't exist at the point the dynamic script is executed. So something like the following would fail:
EXECUTE IMMEDIATE `insert into pivotTable values(:NEW.' || variableWithColumnName ||')';
So, I'm stuck.
I can't read from the table that was updated, and I can't read the value that was updated from the :NEW object.
Is there anyway to accomplish this other than rebuilding the trigger each time the schema is changed?
No. You'll need to rebuild the trigger whenever the table changes.
If you want to get really involved, you could write a procedure that dynamically generated the DDL to CREATE OR REPLACE the trigger by reading user_tab_columns. You could then create a DDL trigger that fired when the table was altered, submitted a job via dbms_job that called the procedure to recreate the trigger. That works but it's a rather large number of moving parts which means that it can fail in all sorts of subtle and spectacular ways particularly if the application that is making schema changes on the fly decides to add columns in the middle of the day.

Fire one trigger when modifying two different tables

We have a problem with our DBMS (Oracle) which prevents us from using materialized views, so my boss came with this idea of implementing it using a real table and triggers; inserting, updating or deleting from this table when an insert, update, or delete is done in one of the tables upon which the materialized view would had been based.
I know I'm going to hell for agreeing to this, but the time for lamentations is well overdue.
My problem is, I don't know how to make this trigger to fire when a change is done in any of these tables, and not in only one of them. Something like this doesn't seem to work:
create trigger my_trigger
after insert or update or delete on table1, table2
Also, would there be a way to create just one trigger instead of one for insert, one for update, and one for delete?
Try this,
CREATE or REPLACE TRIGGER test
AFTER INSERT OR UPDATE OR DELETE ON tabletest
REFERENCING OLD AS OLD NEW AS NEW
FOR EACH ROW
DECLARE
<< Your declarations>>
BEGIN
IF INSERTING THEN
<<Your insertions>>
END IF;
IF UPDATING THEN
<<Your updations>>
END IF;
IF DELETING THEN
<<Your deletions>>
END IF;
EXCEPTION
WHEN OTHERS THEN
<<exception handling>>;
END;
Also you cannot have multiple tables in the same trigger, you need to write the same code and change the table name in each trigger if the functionality is the same.

Insert data into a different column when copying from one table to another in Oracle

I am using an AFTER INSERT row trigger in Oracle 11g to copy specific columns from one table to another on insert. I have the trigger and insert working ok. The problem I have is that I need to insert the new data from one column to a different column when copying it.
The trigger info reads:
BEGIN
insert into BALES_STORAGE
(CROP,
CUTTING,
DESTINATION,
BALES_MOVED,
DATE_MOVED,
PASTURE,
TARGET_LB_PER_DAY)
values
(:new.CROP,
:new.CUTTING,
:new.MOVING_LOCATION,
:new.BALES_MOVED,
:new.DATE_MOVED,
:new.PASTURE,
:new.TARGET_LB_PER_DAY);
END;
The first table is called "BALES_HARVESTED" and the 2nd table the trigger inserts the selected columns into is called "BALES_STORAGE". I need to insert the :new.MOVING_LOCATION data into the column called DESTINATION on the second table.
So my question is: when using an after insert row trigger, how to I change the column that the data is inserted into?
Thanks for any help.
Matthew
Your trigger code worked just fine for me. Not sure what the problem is. The 3rd column in your INSERT statement does the column mapping correctly.
http://sqlfiddle.com/#!4/2d2fd5/1/1
Maybe you have different structures or foreign key constraints. Could you elaborate on what error you get? Does it produce an ORA- error? or does it simply not produce the desired result, but no error?

Is it possible to compare other tables within a trigger?

I have a database with tables that are chained together with foreign keys, and the last one in the chain also has a foreign key to itself. I want to delete them with cascade on, exapt for the last one in the chain. That one should be set null, unless it's parent record has a certain value. I figured i would do that with a trigger: whenever the last table updated, if the foreign key to itself had been set to null, check the field in the parent record, and if it is the value "default", delete the record in the last table.
However, I haven't found any help online indicting that comparing a parent record in another table.
Is this possible?
In general, a row-level trigger on table A cannot query table A. Doing so would generally raise a mutating table exception (ORA-04091). So a trigger is generally not the right solution.
Presumably, you have some sort of API (i.e. a stored procedure) to delete records from the parent table. That API should query this last table before issuing the DELETE against the parent table. It should take care of updating the last table in the chain as well as deleting the data from the parent table.
If you really wanted a trigger-based solution, life would get substantially more complicated. You could work around the mutating table exception by
Creating a package with a collection of primary keys from the parent table
Creating a before statement trigger that initializes this collection
Creating a row-level trigger that populates the collection with the primary keys that were modified by the SQL statement
Creating an after statement trigger that iterates over the collection and issues whatever DML is necessary (unlike row-level triggers, statement-level triggers on table A can query or modify table A).
If you're using 11g, you can simplify this a bit with a compound trigger with before statement, after row, and after statement sections. But you've still got a number of moving pieces to try to coordinate.
AFAIK you won't be able to really delete the record in the last table (mutating table problem), but you could update a status field indicating the record has been logically deleted (untested):
create or replace trigger last_table_trig
before update on last_table
for each row
declare
l_parentField varchar2(100);
begin
if :new.self_ref_fk is null then
select p.parent_field into l_parentField from parent_table p
where p.pk = :new.parent_fk;
if l_parentField = 'default' then
:new.status := 'DELETED';
end if;
end if;
end;

Resources