Trying to create a trigger that prevents updates based on value from another table - oracle

I am trying to prevent users from updating one table based on a date value from another table.
Table A contains rows that I would like to make un editable if a date value in the Table B is older than sysdate.
I need to somehow tell the trigger to check the row and use a foreign key in Table A's row to query its corresponding rows in Table B and then do this:
raise_application_error(-20000, 'It is too late to change this record');
Thank You

Assuming this is a homework assignment, you'd want something like this (I'm guessing at table structures and cardinalities since you don't specify).
CREATE OR REPLACE TRIGGER trigger_name
AFTER UPDATE ON a
FOR EACH ROW
DECLARE
l_dt_b b.dt_col%type;
BEGIN
SELECT dt_col
INTO l_dt_b
FROM b
WHERE b.b_key = :new.b_key;
IF( l_dt_b < sysdate )
THEN
RAISE_APPLICATION_ERROR( -20001, 'Too late' );
END IF;
END;
If this is for a real system, however, trigger-based validation is problematic. It is not safe in a multi-user system, for example. In session 1, I may have modified the row in B but not yet committed the change. You can then query the row in session 2, see the old value, and allow the UPDATE. We can both commit our changes and nothing will detect that we have data in an invalid state.

Related

Oracle_Trigger: Auto Update Of a Column Based Upon INSERT/UPDATE

This might appear a simple query for most of you, but I am a beginner in Oracle DB.
A table has been created with below script-
CREATE TABLE PLAN_TABLE
(
PL_ID DECIMAL(10,0) PRIMARY KEY NOT NULL
,PL_NAME VARCHAR2(300) DEFAULT NULL
,UPDATED_TS TIMESTAMP DEFAULT SYSDATE NOT NULL
,DELETE_FLAG DECIMAL(10,0) DEFAULT 0 NOT NULL
);
The requirement is to have SYSDATE for UPDATED_TS for any new record inserted into the table and also in case when the DELETE_FLAG is updated to 1. Can it be done by trigger?
The below trigger was created-
CREATE OR REPLACE TRIGGER PT_BEFORE_INSERT_TR
BEFORE INSERT ON PLAN_TABLE
FOR EACH ROW
BEGIN
SELECT SYSDATE INTO :new.UPDATED_TS FROM DUAL;
dbms_output.put_line('Inserted');
END;
/
Below error was encountered while inserting record into the table-
error: ORA-04091: table I60_SCH04.PLAN_TABLE is mutating, trigger/function may not see it
Can you please help in letting me know that where am I committing the mistake? Is there any better way to achieve the requirement based upon INSERT/UPDATE?
The actual error you get is due to the fact that you try to select from a table that you actually are changing. To prevent the issue there are a couple of methods, but in you case things are really simple.
SYSDATE is a function, that you could call directly inside PL/SQL block (which a trigger actually is) and use the value returned to update the set the column value
CREATE OR REPLACE TRIGGER PT_BEFORE_INSERT_TR
BEFORE INSERT ON PLAN_TABLE
FOR EACH ROW
BEGIN
:new.UPDATED_TS := sysdate;
dbms_output.put_line('Inserted');
END;
/
OK, this covers the insert part.
For updating - once again, many options. One could be - change your trigger to BEFORE INSERT OR UPDATE ON PLAN_TABLE.
In this case whenever you issue update or insert - this trigger is fired for each row and updates the date column accordingly.
And of course you could use particular checks available in triggers, something like
IF INSERTING OR UPDATING('DELETE_FLAG') THEN
...
END IF;
and code in the logic you need.

Update table column on update related table column in pl sql

I have a database that among the others contains these two tables:
NARUDZBENICA(**SIFANAR**,DATUM,NAZIV,*SIFRADOB,SIFRAKATALOGA,SIFRAZAP,SIFRANACISP*)
DOBAVLJAC(**SIFRADOB**,NAZIV,MAIL,TELEFON,FAKS)
I need a statement trigger to update column 'naziv' in all rows in table NARUDZBENICA where SIFRADOB starts with '0' when I change column 'naziv' on DOBAVLJAC where SIFRADOB is a primary key.
This is what I came up with:
CREATE OR REPLACE TRIGGER "STATEMENT_DOB"
AFTER UPDATE OF NAZIV ON DOBAVLJAC
BEGIN
EXECUTE IMMEDIATE 'ALTER TRIGGER UPDATE_NAR_FRB DISABLE';
UPDATE NARUDZBENICA
SET NAZIV = (SELECT :OLD.NAZIV FROM DOBAVLJAC)
WHERE ROWNUM > 1 AND SIFRADOB = '%0';
EXECUTE IMMEDIATE 'ALTER TRIGGER UPDATE_NAR_FRB ENABLE';
END;
I don't know what you are trying to do disabling and enabling one trigger in another. Otherwise I can see pretty much what you're trying to do, I just don't understand why.
In plain language: when the field NAZIV in table DOBAVLJAC is updated, the old value of the field is saved to the same field in table NARUDZBENICA where the field SIFRADOB begins with the character '0'.
create or replace trigger STATEMENT_DOB
after update of NAZIV on DOBAVLJAC
begin
update NARUDZBENICA
set NAZIV = :old.NAZIV
where SIFRADOB like '0%';
end;
It just occurred to me why you may be disabling the other trigger. It is an update trigger on the other table that watches for that same field to, in turn, propagate the change to DOBAVLJAC. This would create an endless loop of updates. (It would also means you probably should be using the :new.NAZIV value rather than :old.NAZIV.)
There are several tricks to solve that problem. The more involved one is to rename both tables and create views with the original table names. The Instead Of trigger on each view updates the NAZIV changes to both tables. No looping.
That is a rather involved solution. A simpler one (involving fewer object changes) is to create a flag column in both tables. The value of this column is always NULL. When the trigger executes (and it will have to be the before trigger), it checks the NEW value of the flag column. If it is still null, that means this is the first Update so sends an update to the other table. That update sets the NAZIV value to the new value and the flag field to any non-null value. The non-null value tells the other trigger that this is a propagation update so ends the propagation. It changes the :new.flag value to null (you never actually change the contents of the flag field in the table) and just allows the update. This logic would be the same in the trigger on both tables.
This is made a little easier with Oracle 12c and invisible columns. It just allows you to hide the flag field from normal view so people aren't always coming around and asking what it is for.
In reading over my description, I don't think I made my point clearly, especially for non-native English speakers. So here is the trigger code:
create or replace trigger STATEMENT_DOB -- UPDATE_NAR_FRB
before update of NAZIV, Flag on DOBAVLJAC -- NARUDZBENICA
begin
if updating( Flag ) and not updating( NAZIV ) then
-- Someone playing around updating Flag only. Don't allow.
:new.FLAG := null;
elsif :new.FLAG is null then
-- Original Update. Propagate to other table
update NARUDZBENICA -- DOBAVLJAC
set NAZIV = :new.NAZIV,
Flag = 1
where SIFRADOB like '0%';
else
-- This was propagation from other table. Just allow the update of NAZIV
-- but first reset the flag...
:new.FLAG := null;
end if;
end;
That is not quite production-level coding, but I hope it illustrates the idea.
:new or :old identifiers can only be used in row-level triggers. For statement level trigger use of :new and :old identifiers is forbidden.
Also you cannot commit in a trigger. So incase you want to do any DML operation you need to use an autonomous transaction. See below how you can do it.
This trigger will be fired for any change in column value of ID in table A_TABLE.
The new value will be captured and passed on to the anonymous transaction.
CREATE OR REPLACE TRIGGER STATEMENT_DOB
AFTER UPDATE OF ID ON A_TABLE
for each row
BEGIN
proc_upd_tb(:new.id);
END;
Autonomous Transaction:
Data can be updated here and committed as shown.
create or replace procedure proc_upd_tb(id number)
as
PRAGMA AUTONOMOUS_TRANSACTION;
begin
UPDATE AA
SET A = id;
where <condition> ;
commit;
end;

create complex trigger on table

I have a table participants having structure as shown below:
Pid number
name varchar2(20)
version number
Whenever i inserted any record in participants table ,version =1 get populated.
For Example ,if i inserted pid=1 ,name='Gaurav' then record with version =1 get populated in participants table .
Now my issue is with update on participants table,
Suppose i am updating name ='Niharika' for pid=1 in participants table then a new record with pid=1 ,name='Niharika' and version =2 need to be created on the same table .
Again i update name='Rohan' for pid='1' in participants table a new record with pid=1 ,name='Rohan' and version=3 needs to be created .
How can i achieve this , clearly speaking i need to get max(version)+1 for that pid that is going to update .
I can achieve this using view and insert into view using instead of trigger ,but i am not satisfied with my solution .
I have also created compound trigger ,even that is not working for me because inside trigger i need to use insert statement for that table and this will give me recursive error
You should really have two tables. Make one with the structure you described as a "logging" table. It will keep the history of all the records. Have another table which is considered "current" which is the same but without the version column. Then, when inserts/update occur on the "current" tables' records, have a mechanism (trigger, for example) SELECT FOR UPDATE the max(version) in the logging table, add one, and insert into the logging table. This way, you're not going to run into mutating table errors or anything weird like that. There is a bit of serialization this way, but it's the closest to what you're trying to do.
Not usually recommended, but here's how you can do it anyways with no other extra logging table(s)-
CREATE or REPLACE
TRIGGER part_upd
AFTER UPDATE of name
ON participants
FOR EACH ROW
DECLARE
retval BOOLEAN;
BEGIN
retval := insert_row(:old.pid,:new.name);
END part_upd;
The function-
CREATE or REPLACE
FUNCTION insert_row (pid1 number, name1 varchar2)
RETURN boolean
IS
PRAGMA autonomous_transaction;
BEGIN
INSERT INTO participants
SELECT pid1, name1, max(vers)+1
FROM participants
WHERE pid = pid1;
COMMIT;
RETURN true;
END;
You'll have to fine tune the Trigger and Function properly by adding logging and exception handling. Read more about autonomous_transaction.

while creating a audit trigger throwing warning as Compilation error

I try to create a audit trigger it throwing compilation error.
could you please help me for creating trigger..
DROP TRIGGER DB.DAT_CAMPLE_REQ_Test;
CREATE OR REPLACE TRIGGER DB."DAT_CAMPLE_REQ_Test"
AFTER insert or update or delete on DAT_CAMPLE_REQ
FOR EACH ROW
declare
dmltype varchar2(6);
BEGIN
if deleting then
INSERT INTO h_dat_cample_req VALUES (
:Old.REQUEST_ID,
:Old.SAMPLE_ID,
:Old.CASSAY_ID,
:Old.CASCADE_ID,
:Old.STATUS_ID,
:Old.AUTHOR,
:Old.CRT_SAE,
:Old.SCREEN_SAE
);
else
if inserting then
dmltype := 'insert';
elsif updating then
dmltype := 'update';
end if;
INSERT INTO h_dat_cample_req VALUES
(
:New.REQUEST_ID,
:New.SAMPLE_ID,
:New.CASSAY_ID,
:New.CASCADE_ID,
:New.STATUS_ID,
:New.AUTHOR,
:New.CRT_SAE,
:New.SCREEN_SAE
);
end if;
END;
You haven't provided the exact error message nor the structure of the table h_dat_cample_req, so I'm afraid I'm going to have to guess.
I suspect the column names in your h_dat_cample_req are not in the order you expect, or there are other columns in the table that you haven't specified a value for in your INSERT statements.
You are using INSERT statements without listing the columns that each value should go in to. The problem with using this form of INSERT statement is that if the columns in the table aren't in exactly the order you think they are, or there are columns that have been added or removed, you'll get an error and it'll be difficult to track it down. Furthermore, if you don't get a compilation error there's still the chance that data will be inserted into the wrong columns. Naming the columns makes it clear which value goes in which column, makes it easier to identify columns that have been removed, and also means that you don't have to specify values for all of the columns in the table - any column not listed gets a NULL value.
I would strongly recommend always naming columns in INSERT statements. In other words, instead of writing
INSERT INTO some_table VALUES (value_1, value_2, ...);
write
INSERT INTO some_table (column_1, column_2, ...) VALUES (value_1, value_2, ...);
Incidentally, you're assigning a value to your variable dmltype but you're not using its value anywhere. This won't cause a compilation error, but it is a sign that your trigger might not be doing quite what you would expect it to. Perhaps your h_dat_cample_req table is a history table and has a column for the type of operation performed?

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