Prevent update on multiple rows in oracle db - oracle

i had a major screw up in my latest patch.
An update condition was incomplete and i updated multiple rows
by accident.
What i wanna do now is to prevent this by setting a constraint for
a table wich cause an exception as soon as i try to update multiple
rows. Optionally with specific parameters.
Is there a way to do this in oracle 11.2 ?

You can accomplish this by using a compound trigger:
CREATE OR REPLACE TRIGGER TABLE1_FAIL_MULT_UPDATES_TRG
FOR UPDATE ON TABLE1
COMPOUND TRIGGER
nUpdate_count NUMBER;
BEFORE STATEMENT IS
BEGIN
nUpdate_count := 0;
END BEFORE STATEMENT;
BEFORE EACH ROW IS
BEGIN
IF UPDATING THEN
nUpdate_count := nUpdate_count + 1;
IF nUpdate_count > 1 THEN
RAISE_APPLICATION_ERROR(-20100, 'Attempted to update more than 1 row');
END IF;
END IF;
END BEFORE EACH ROW;
END TABLE1_FAIL_MULT_UPDATES_TRG;
You can read further on compound triggers here.
Best of luck.

You can use the Answer on this question which offer a solution with three triggers and package variable to count the number of rows affected. In the third trigger, if the number of rows is greater than one then raise an exception. The entire statement will be rolled back.
This is also safe for concurrency because package variables are "stored" session level.

Related

Trigger on a table that uses Merge Insert Update as Incremental strategy

Trigger under question is for Table which has ETL in ODI but user also has option to edit certain **columns **
if they want to adjust them. This is done using APEX
Trigger is used to change two columns : Changed_by and Change_on.
Both indicating Changes done on APEX PAGE only.
The issue comes when ODI load is run and is MERGE INSERT UPDATE , Trigger thinks its updating and changes the above two columns to "NULL" as its a manual update done by ODI and not on APEX.
Solution
For each Editable Column, there should be a logic which checks NEW: <> :OLD, but as i have 15 columns need to write a lot of code.
Are there others way to achieve this ?
create or replace TRIGGER DW.TRG BEFORE
UPDATE ON DW.TABLE
REFERENCING
NEW AS new
OLD AS old
FOR EACH ROW
BEGIN
IF updating THEN
SELECT
SYSDATE,
v('APP_USER')
INTO
:new.changed_on_dt,
:new.changed_by
FROM
dual;
END IF;
END;
Check if an apex session exists for the current database session and only execute when it is the case.
create or replace TRIGGER DW.TRG BEFORE
UPDATE ON DW.TABLE
REFERENCING
NEW AS new
OLD AS old
FOR EACH ROW
BEGIN
IF SYS_CONTEXT('APEX$SESSION','APP_SESSION') IS NOT NULL AND updating THEN
:new.changed_on_dt := SYSDATE;
:new.changed_by := SYS_CONTEXT('APEX$SESSION','APP_USER');
END IF;
END;
Notes
avoid the SELECT FROM DUAL, you can just assign the values in the trigger.
The "V" functions are pretty slow. For a while there have been sys_context settings that store the session and user data. Those are a lot faster than a function call to the "V" function.
You could make it so that it never overwrites a non-null value with a null one:
IF v('APP_USER') IS NOT NULL
THEN
:new.changed_by := v('APP_USER');
:new.changed_on_dt := SYSDATE;
END IF;

Mutating Trigger Error with Trigger in Oracle PL/SQL [duplicate]

I get an error (ORA-04091: table DBPROJEKT_AKTIENDEPOT.AKTIE is mutating, trigger/function may not see it) when executing my trigger:
CREATE OR REPLACE TRIGGER Aktien_Bilanz_Berechnung
AFTER
INSERT OR UPDATE OF TAGESKURS
OR INSERT OR UPDATE OF WERT_BEIM_EINKAUF
ON AKTIE
FOR EACH ROW
DECLARE
bfr number;
Begin
bfr := :new.TAGESKURS - :new.WERT_BEIM_EINKAUF;
UPDATE AKTIE
SET BILANZ = TAGESKURS - WERT_BEIM_EINKAUF;
IF bfr < -50
THEN
DBMS_OUTPUT.PUT_LINE('ACHTUNG: The value (Nr: '||:new.AKTIEN_NR||') is very low!');
END IF;
END;
I want to check the value "BILANZ" after calculating it, wether it is under -50.
Do you have any idea why this error is thrown?
Thanks for any help!
There are several issues here:
Oracle does not allow you to perform a SELECT/INSERT/UPDATE/DELETE against a table within a row trigger defined on that table or any code called from such a trigger, which is why an error occurred at run time. There are ways to work around this - for example, you can read my answers to this question and this question - but in general you will have to avoid accessing the table on which a row trigger is defined from within the trigger.
The calculation which is being performed in this trigger is what is referred to as business logic and should not be performed in a trigger. Putting logic such as this in a trigger, no matter how convenient it may seem to be, will end up being very confusing to anyone who has to maintain this code because the value of BILANZ is changed where someone who is reading the application code's INSERT or UPDATE statement can't see it. This calculation should be performed in the INSERT or UPDATE statement, not in a trigger. It considered good practice to define a procedure to perform INSERT/UPDATE/DELETE operations on a table so that all such calculations can be captured in one place, instead of being spread out throughout your code base.
Within a BEFORE ROW trigger you can modify the values of the fields in the :NEW row variable to change values before they're written to the database. There are times that this is acceptable, such as when setting columns which track when and by whom a row was last changed, but in general it's considered a bad idea.
Best of luck.
You are modifying the table with the trigger. Use a before update trigger:
CREATE OR REPLACE TRIGGER Aktien_Bilanz_Berechnung
BEFORE INSERT OR UPDATE OF TAGESKURS OR INSERT OR UPDATE OF WERT_BEIM_EINKAUF
ON AKTIE
FOR EACH ROW
DECLARE
v_bfr number;
BEGIN
v_bfr := :new.TAGESKURS - :new.WERT_BEIM_EINKAUF;
:new.BILANZ := v_bfr;
IF v_bfr < -50 THEN
Raise_Application_Error(-20456,'ACHTUNG: The value (Nr: '|| :new.AKTIEN_NR || ') is very low!');
END IF;
END;

trigger auto_increment oracle without sequence

I created a trigger to manage the auto_increment but for the reason that I do not know it always shows me this error: maximum number of recursive SQL levels (50) exceeded.
CREATE OR REPLACE TRIGGER auto_increment BEFORE INSERT ON people FOR EACH ROW
DECLARE
mat number;
namm varchar(40);
coun number;
BEGIN
namm:=:new.name;
IF inserting() THEN T
SELECT COUNT(*) INTO coun FROM people;
IF coun = 0 THEN
INSERT INTO people VALUES(100,namm);
ELSE
SELECT MAX(:old.matricule+1) INTO mat FROM people;
INSERT INTO people (matricule,name) VALUES(mat,namm);
END IF;
END IF;
END;
It is recursive because every time a record is inserted into PEOPLE your trigger inserts another record into PEOPLE, which causes the trigger to fire and insert yet another record into PEOPLE, which... well you get the idea.
In an Oracle trigger you just set the :NEW values to change what is being inserted e.g.:
:NEW.matricule := 100;
However, your trigger still would not work because it selects from the same table, which will cause the "table is mutating" exception.
Really the best answer here is to use a sequence - they are designed to avoid these issues as well as giving the best performance. But if you really need this increment functionality, do it before inserting not in a trigger.

Oracle Forms - Commit Single SQL Statement Instead of Entire Form

I'm working on an Oracle Form (10g) that has two blocks on a single canvas. The top block is called QUERY_BLOCK which the user fills out to fill PRICING_BLOCK with rows of data.
However, in QUERY_BLOCK I also have a checkbox which needs to perform an INSERT and DELETE on the database, respectively. My WHEN-CHECKBOX-CHANGED trigger looks like this:
begin
if :query_block.profile_code is not null then
if :query_block.CHECKBOX_FLAG = 'Y' then
begin
INSERT INTO profile_table VALUES ('Y', :query_block.profile_code);
end;
else
begin
DELETE FROM profile_table WHERE profile_code = :query_block.profile_code and profile_type_code = 'FR';
end;
end if;
end if;
end;
I know that I need to add some sort of commit statement in here, otherwise the record locks and nothing actually happens. However, if I do a COMMIT; then the entire form goes through validation and updates any changed rows.
How do I execute these one-line queries I have without the rest of my form updating as well?
Without commenting on the actual wisdom of this, you could create a procedure in the database that performed an autonomous transaction:
CREATE OR REPLACE FUNCTION my_fnc(p_flag IN VARCHAR2)
RETURN VARCHAR2 IS
PRAGMA AUTONOMOUS_TRANSACTION;
BEGIN
IF p_flag = 'Y' THEN
INSERT...
ELSE
DELETE...
END IF;
COMMIT;
RETURN 'SUCCESS';
EXCPTION
WHEN OTHERS THEN
RETURN 'FAIL';
END;
Your Forms code could then look like:
begin
if :query_block.profile_code is not null then
stat := my_fnc(:query_block.CHECKBOX_FLAG);
end if;
end;
This allows your function to commit independent of the calling transaction. Beware of this, however - if your outer transaction must roll back, the autonomous transaction will still be committed. I would think there should be a transactional way to do what you need done to solve your locking problem, which would likely be the superior approach. Without knowing the specifics of your process, I can't tell. Generally speaking, autonomous transactions are used when an update must occur regardless of whether the transaction commits or rolls back, e.g., logging.

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