Atomic Transaction with nested BEFORE INSERT/UPDATE Triggers - database

Currently i am implementing a procedure, which creates a couple of rows in some related tables out of a template. So my Procedure consists of a SAVEPOINT followed by some INSERT statements on different tables, and a Cursor to insert some more rows to other tables while referencing on the newly created primary keys.
Each of those tables has an BEFORE INSERT/UPDATE trigger defined which has the purpose to:
Get a new primary key from a sequencer if it is not defined in the INSERT statement (there are cases where I need to set the Primary key explicitely to reference it later on in the same transaction)
Set some default values if they are NULL
Set auditing fields (last_change_date, last_change_user, etc..)
The transaction fails with ORA-04091: table is mutating, trigger/function may not see it
I am understanding, that I could Workaround this, by declaring PRAGMA AUTONOMOUS TRANSACTION in each Trigger, but my Transaction would not be atomic any more then, as it is the requirement that all those datasets should be created/inserted as a whole or None of them.
So what am I doing wrong in the design of my database?
UPDATE: This is the Code of the trigger
CREATE TRIGGER TRG_AUFTRAG_B_IU
BEFORE INSERT OR UPDATE
ON AUFTRAG
FOR EACH ROW
BEGIN
IF INSERTING THEN
IF :new.id is NULL or :new.id = 0 THEN
SELECT SEQ_AUFTRAG.nextval into :new.id from dual;
END IF;
IF :new.nummer is NULL or :new.nummer = 0 THEN
SELECT nvl(MAX(NUMMER),0)+1 INTO :new.nummer FROM AUFTRAG WHERE EXTRACT(YEAR from DATUM) = EXTRACT(YEAR from :new.DATUM);
END IF;
--DEFAULT Values
IF :new.BETR_GRENZWERTE_RELEVANT is NULL THEN
SELECT 0 INTO :new.BETR_GRENZWERTE_RELEVANT FROM dual;
END IF;
IF :new.DOKUMENTE_ABGELEGT is NULL THEN
SELECT 0 INTO :new.DOKUMENTE_ABGELEGT FROM dual;
END IF;
IF :new.EXT_ORG is NULL or :new.EXT_ORG < 1 THEN
SELECT 1 INTO :new.EXT_ORG FROM dual;
END IF;
:new.ERSTELLT_VON := nvl(:new.ERSTELLT_VON,user);
:new.ERSTELLT_DATUM := nvl(:new.ERSTELLT_DATUM,sysdate);
END IF;
:new.GEAENDERT_VON := user;
:new.GEAENDERT_DATUM := sysdate;
END;

You can write it more compact like this:
CREATE TRIGGER TRG_AUFTRAG_B_IU
BEFORE INSERT OR UPDATE
ON AUFTRAG
FOR EACH ROW
BEGIN
IF INSERTING THEN
:new.id = NVL(NULLIF(:new.id, 0), SEQ_AUFTRAG.nextval);
--DEFAULT Values
:new.BETR_GRENZWERTE_RELEVANT := NVL(:new.BETR_GRENZWERTE_RELEVANT, 0);
:new.DOKUMENTE_ABGELEGT := NVL(:new.DOKUMENTE_ABGELEGT, 0);
IF :new.EXT_ORG is NULL or :new.EXT_ORG < 1 THEN
:new.EXT_ORG := 1;
END IF;
:new.ERSTELLT_VON := nvl(:new.ERSTELLT_VON,user);
:new.ERSTELLT_DATUM := nvl(:new.ERSTELLT_DATUM,sysdate);
END IF;
:new.GEAENDERT_VON := user;
:new.GEAENDERT_DATUM := sysdate;
END;
Only "problem" is this part
IF :new.nummer is NULL or :new.nummer = 0 THEN
SELECT nvl(MAX(NUMMER),0)+1 INTO :new.nummer
FROM AUFTRAG
WHERE EXTRACT(YEAR from DATUM) = EXTRACT(YEAR from :new.DATUM);
END IF;
This one you should put into your procedure or in a statement trigger (i.e. without FOR EACH ROW clause) like this:
CREATE TRIGGER TRG_AUFTRAG_B_A
AFTER INSERT ON AUFTRAG
BEGIN
UPDATE
(SELECT ID, NUMMER,
ROW_NUMBER() OVER (PARTITION BY EXTRACT(YEAR from DATUM) ORDER BY ID) as N
FROM AUFTRAG)
SET NUMMER = N
WHERE NUMMER IS NULL;
END;

Related

SQL command not properly ended errror database oracle

I have a request to add data that triggers a trigger that checks one condition, and only after that adds it to the purchase table. He also swears at the lines of initializations total_rasr and id_buyer invalid number, although there are the same types in the table and in the trigger. And the biggest question is, this trigger worked, but at one point it stopped and gives these errors.
INSERT INTO PAYMENT (ID, ADDRESS, DATE_PAYMENT, PAYMENT_METHOD_ID, EMPLOYEE_ID, BUYER_ID)
VALUES (Key('PAYMENT'), 'Moscow', TO_DATE('2002-08-23', 'YYYY-MM-DD'),'005!00002','1','002!00005');
trigger
create or replace TRIGGER checking_the_availability_work
BEFORE INSERT
ON PAYMENT
FOR EACH ROW
DECLARE
rasr_is_possible boolean := true;
total_rasr VARCHAR(10);
id_buyer VARCHAR(10);
AVAILABILITY_OF_WORK VARCHAR(5);
xSQL VARCHAR(100);
BEGIN
total_rasr := :NEW.PAYMENT_METHOD_ID;
id_buyer := :NEW.BUYER_ID;
IF total_rasr = '005!00002' THEN
xSQL := 'select AVAILABILITY_OF_WORK FROM BUYER WHERE ID =' || id_buyer;
execute immediate xSQL into AVAILABILITY_OF_WORK;
IF AVAILABILITY_OF_WORK = 'Нет' THEN
rasr_is_possible := false;
END IF;
END IF;
if not rasr_is_possible THEN
Raise_application_error(-20201, 'У вас нет места работы!');
end if;
END;
Why use dynamic SQL here at all? It seems really convoluted and is resulting in an error, perhaps when :new.buyer_id is null or has some other unexpected value? Try something like this, and perhaps consider a check on :new.buyer_id to make sure it has a value as expected.
DECLARE
L_AVAILABILITY_OF_WORK VARCHAR(5);
BEGIN
IF :NEW.PAYMENT_METHOD_ID = '005!00002' THEN
select AVAILABILITY_OF_WORK into L_AVAILABILITY_OF_WORK
FROM BUYER WHERE ID = :NEW.BUYER_ID;
IF L_AVAILABILITY_OF_WORK = 'Нет' THEN
Raise_application_error(-20201, 'У вас нет места работы!');
END IF;
END IF;
END;

PL/SQL How to convert a record to an assoc array

I am trying to fetch data from a table(employee) with cursor and save to asso. array. However, fetching data with cursor to a record is more straight-forward and it is troublesome to convert a record to an assoc array(arr). Code below is what I am trying to fetch data to a assoc array and improvement is needed. Or any approaches other than cursor? Thank you.
DECLARE
TYPE AssoArray IS TABLE OF varchar2(30) INDEX BY varchar2(30);
arr AssoArray;
table_rec employee%rowtype;
CURSOR cur IS
SELECT * FROM employee;
BEGIN
OPEN cur;
LOOP
FETCH cur into table_rec;
EXIT WHEN cur%notfound;
-- how to improve the code in the section below,
arr('col1') := table_rec.col1;
arr('col2') := table_rec.col2;
arr('col3') := table_rec.col3;
...
arr('col50') := table_rec.col50;
-- end of section
-- do sth
END LOOP;
END;
Could you explain what do you want to get. But I see in your code arr AssoArray will contain only one last fetched row and will be rewrite each cycle. Are you realy need it? My suggestion is you want to get some rows of table as associated array. If its true, you may create the array as table of rowtype and (for example) indexed it by id(if you ID-s is integer).
TYPE AssoArray IS TABLE OF employee%rowtype INDEX BY PLS_INTEGER;
For example
create table tmp_table_01 as select * from all_objects where rownum < 11;
DECLARE
TYPE AssoArray IS TABLE OF tmp_table_01%rowtype INDEX BY varchar2(30);
arr AssoArray;
table_rec tmp_table_01%rowtype;
CURSOR cur IS
SELECT * FROM all_objects where rownum < 20;
BEGIN
OPEN cur;
LOOP
FETCH cur into table_rec;
EXIT WHEN cur%notfound;
-- how to improve the code in the section below,
arr(table_rec.object_name) := table_rec;
-- end of section
dbms_output.put_line(table_rec.object_name ||' '||arr(table_rec.object_name).object_id );
-- do sth
END LOOP;
END;
EDIT:
If you want to make comraisons by table structure you may use Dynamic SQL. NExt code get data from table TMP_TABLE_01 sort it by object_id, compare neighbour rows, and return count of difference row.
DECLARE
l_sql varchar2(32767);
TYPE t_cols_type IS table of varchar2(30);
l_cols t_cols_type ;
l_result number;
BEGIN
SELECT c.COLUMN_NAME
BULK COLLECt INTO l_cols
FROM user_tab_cols c
WHERE c.TABLE_NAME = 'TMP_TABLE_01';
l_sql := 'WITH s AS (SELECT t.*, row_number() OVER (ORDER BY object_id) AS rn FROM TMP_TABLE_01 t)
SELECT count(*)
FROM s
JOIN s sub_s ON s.rn = sub_s.rn - 1
where 1=0 ';
FOR i IN 1 .. l_cols.last LOOP
l_sql := l_sql || ' OR decode(s.'||l_cols(i)||',sub_s.'||l_cols(i)||',1,0) = 0';
END LOOP;
dbms_output.put_line(l_sql);
EXECUTE IMMEDIATE l_sql
INTO l_result ;
dbms_output.put_line('count of conseuence different rows ' ||l_result);
END;

Oracle PL/SQL loop issue

Hey guys trying to use a loop to cycle though days so my script doesn't fail the loop fails to execute each time I am not sure what I am doing wrong however i know for a fact the select statement is fine and working its just the loop i have trouble with also is it possible to have the loop check the max date in the table delete only the most recent day in case of incomplete data then add only whats needed to date?
Error line 41, column 4:
PL/SQL: ORA-00933: SQL command not properly ended
ORA-06550: line 7m Column 1:
Pl/sql statement ignored
delete Target_table
commit;
DECLARE
i_date date;
BEGIN
i_date := '01-Jan-2014';
WHILE i_date < sysdate LOOP
insert into Target_table
Select field_1,
field_2
From Data_table_1
LEFT JOIN Data_table_2
ON Data_table_1.ACCOUNT_ID=Data_table_2.account_id
Where Data_table_1.Date >= i_date
and Data_table_1.Date < i_date+1
and Data_table_1.COST_CENTRE In ('1','2','3','4','5','6','7','8','9','10')
And Data_table_1.field_3 In ('C', 'D')
And Data_table_1.field_4 Is Null
And Data_table_2.field_5 in (1,2)
END;
commit;
i_date := i_date +1;
END LOOP;
END
Just want to mention that the most efficent way is write the query in in SQL whenever possible, when PL/SQL can be avoided. (There is an overhead involved in the context switch between SQL and PL/SQL blocks). This is how i would write the query. The new block, using CONNECT by would generate all dates between i_date and sysdate
DECLARE
i_date DATE;
BEGIN
DELETE Target_table;
SELECT field_1,
field_2
FROM Data_table_1
JOIN (SELECT i_date + level -1 as i_date_new
FROM DUAL
CONNECT BY LEVEL<=TRUNC(sysdate)-i_date
)dates_generated
ON Data_table_1.Date_field1 >= dates_generated.i_date_new
AND Data_table_1.Date_field1 < dates_generated.i_date_new+1
LEFT JOIN Data_table_2
ON Data_table_1.ACCOUNT_ID =Data_table_2.account_id
WHERE Data_table_1.COST_CENTRE IN ('1','2','3','4','5','6','7','8','9','10')
AND Data_table_1.field_3 IN ('C', 'D')
AND Data_table_1.field_4 IS NULL
AND Data_table_2.field_5 IN (1,2);
END;
I have made some modifications in your code. I think this should work. I have also mentioned comments for which i have made changes.
DECLARE
i_date DATE;
BEGIN
DELETE Target_table; -- Inserted inside the Anonymous block
i_date := to_date('10-Nov-2015','DD-MON-YYYY'); -- Specified Date format
WHILE i_date < sysdate
LOOP
INSERT INTO Target_table
SELECT field_1,
field_2
FROM Data_table_1
LEFT JOIN Data_table_2
ON Data_table_1.ACCOUNT_ID =Data_table_2.account_id
WHERE Data_table_1.Date_field1 >= i_date
AND Data_table_1.Date_field1 < i_date+1
AND Data_table_1.COST_CENTRE IN ('1','2','3','4','5','6','7','8','9','10')
AND Data_table_1.field_3 IN ('C', 'D')
AND Data_table_1.field_4 IS NULL
AND Data_table_2.field_5 IN (1,2);
dbms_output.put_line(i_date);
i_date := i_date +1;
END LOOP;
COMMIT; -- Added commit at the end
END;

PLSQL loop for checking data availability in table

There should be script that checks count() in one table and if count() is NULL then Exit, else sleep for some time and then proceed again with checking table while count(*) is NULL. My efforts were in vain:
declare
v_cnt pls_integer;
begin
while v_cnt >0
loop
select count(*) into v_cnt from TestTable;
if v_cnt is Null
then
exit;
else
dbms_lock.sleep(6);
dbms_output.put_line('Count is greater then Null. Current values: ' || v_cnt);
end loop;
dbms_output.put_line('TestTable does not have any data');
end;
Count always returns a number, not null. Change the if to zero instead of null and it should work. And I changed your logic around so it does not kick off if there are zero records. Unless you are deleting records from the table I have assumed that not having any records will only happen once. If you are deleting records and table could have zero records many times then kick this procedure off with a job that runs as often as you like and remove the while loop.
declare
v_cnt pls_integer;
begin
select count(*) into v_cnt from TestTable;
if v_cnt > 0 then
while v_cnt > 0
loop
select count(*) into v_cnt from TestTable;
dbms_lock.sleep(6);
dbms_output.put_line('Count is greater than zero . Current values: ' || v_cnt);
end loop;
else
dbms_output.put_line('TestTable does not have any data');
end if;
end;

PL/SQL trigger giving error?

Got this question for an exam but getting error from trigger. Please help.
Question
customer(cust_id,cust_name,address)
rent_info(cust_id,date_out,date_due_in,date_returned,fine)
rented_video(cust_id,no_of_videos)
Issuedate should be current date and due date should be 7 days after issue date. When video is returned by the customer the rent_info table should contain cust_id, date_out, date_due_in. A trigger is used to insert data into rent_info table. The following validation must be done before inserting data.
A customer is not allowed to take more than 3 videos in the same date. When a video is returned the returm date is updated. And fine is calculated if any.
Fine is calculated by
For the first three days of delay Rs
10 per day
For the follwoing three days of
delay Rs 20 per day
After the six dates charge fine of
Rs 30 per day
Write a procedure/trigger for doing the updation operation.
I solved it like this.
Made three table:
create table customer(
cust_id number(4),
cust_name varchar2(8),
address varchar2(8)
);
create table rent_info(
cust_id number(4),
date_out date,
date_due_in date,
date_returned date,
fine number(10)
);
create table rented_video(
cust_id number(4),
no_vid number(4)
);
Procedure for taking book
create or replace procedure take_proc(c_id in int,d_out in date) is
val number(3) :=0;
begin
insert into rent_info values(c_id,d_out,d_out+7,NULL,0);
update rented_video set no_vid=no_vid+1 where cust_id=c_id;
--val := select count(date_out) from rent_info where (date_out='12-jan-2010');
--dbms_output.put_line('Values is '||val);
end;
/
Procedure for returning book
create or replace procedure return_proc(c_id in int,d_ret in date) is
val number(3) :=0;
begin
update rented_video set no_vid=no_vid-1 where cust_id=c_id;
update rent_info set date_returned=d_ret where cust_id=c_id;
--insert into rent_info values(c_id,d_out,d_out+7,NULL,0);
update rented_video set no_vid=no_vid-1 where cust_id=c_id;
--val := select count(date_out) from rent_info where (date_out='12-jan-2010');
--dbms_output.put_line('Values is '||val);
end;
/
Trigger for updating Fine when book is returned
create or replace trigger ret_trig
before update on rent_info
for each row
declare
tfine number(7) := 0;
temp number(7) := 0;
rdate date;
dudate date;
cid number(4);
begin
--select date_returned into rdate from rent_info;
--select date_due_in into dudate from rent_info;
--select cust_id into cid from rent_info;
--if (rdate- dudate) <=3 then
--temp := rdate- dudate;
--tfine := tfine+ temp * 10;
--end if;
if (:new.date_returned-:old.date_due_in ) <=3 then
temp := :new.date_returned-:old.date_due_in;
tfine := tfine+ temp * 10;
dbms_output.put_line('Fine Values is '|| tfine);
elsif (:new.date_returned-:old.date_due_in ) <=6 then
temp := :new.date_returned-:old.date_due_in;
tfine := tfine+ 3 * 10;
tfine := tfine+ 20*(temp-3);
dbms_output.put_line('Fine Values is '|| tfine);
else
temp := :new.date_returned-:old.date_due_in;
tfine := tfine+ 3 * 10;
tfine := tfine+ 3 * 20;
tfine := tfine+ 30*(temp-6);
dbms_output.put_line('Fine Values is '|| tfine);
end if;
--update rent_info set fine=fine+tfine where cust_id=:old.cust_id;
end;
/
I could calculate the fine correctly but couldnt update it to the rent_info table ( Last line of the trigger which does the updation is commented).
I Think I have made some logical mistake in trigger creation. Please tell my how to solve the problem correctly.
Sample values to insert
insert into customer values(1,'john','abc h');
insert into customer values(2,'joseph','cde h');
insert into rented_video values(1,0);
insert into rented_video values(2,0);
exec take_proc(1,'12-jan-2010');
exec take_proc(2,'13-jan-2010');
exec return_proc(1,'16-jan-2010');
exec return_proc(2,'29-jan-2010');
Inside a row-level trigger, you change the value of a column by simply doing an assignment to :NEW - you don't issue an UPDATE statement. e.g.:
:NEW.fine := :OLD.fine + tfine;
Why are you using a trigger? You are building a very clear API by developing these procedures, but then you shove code into a trigger. Why not incorporate this logic into return_proc to make things more obvious?

Resources