PLSQL Table keeps mutating - plsql

I'm trying to figure out why my table keeps mutating even though I have already created another package to maintain in memory a copy of the rows in the jobs table.
This is my emp_pkg that handles all the code for the employee, i will only be adding the procedure that i'm using in this package so it will be short:
CREATE OR REPLACE PACKAGE BODY emp_pkg
IS
PROCEDURE SET_SALARY(pjobid jobs.job_id%type,
psal jobs.min_salary%type)
IS
BEGIN
FOR i IN (SELECT * FROM jobs)
LOOP
UPDATE employees
SET salary = psal
WHERE job_id = pjobid;
END LOOP;
END SET_SALARY;
END emp_pkg;
Then I created a trigger on the jobs table that invokes the set_salary procedure on my emp_pkg package when the minimum salary in the jobs table is updated for a specified job id:
CREATE OR REPLACE TRIGGER upd_minsalary_trg
AFTER UPDATE OF min_salary ON jobs
FOR EACH ROW
WHEN (new.min_salary != old.min_salary)
BEGIN
emp_pkg.set_salary(:old.job_id, :new.min_salary);
END upd_minsalary_trg;
/
Now, I tested it by running the code below with the following errors as expected:
UPDATE jobs
SET min_salary = min_salary + 1000
WHERE job_id = 'IT_PROG';
--ORA-04091: table SQL_WLALHQOYXPNRGAIYBLVJOQTEB.JOBS is mutating, trigger/function may not see it
Now like I said before, to fix this i have created another package to maintain in memory a copy of the rows in the jobs table:
CREATE OR REPLACE PACKAGE BODY jobs_pkg
IS
TYPE jobs_tab_type IS TABLE OF jobs%rowtype INDEX BY jobs.job_id%type;
jobstab jobs_tab_type;
PROCEDURE initialize
IS
BEGIN
FOR rec IN (SELECT * FROM jobs)
LOOP
jobstab(rec.job_id) := rec;
END LOOP;
END;
FUNCTION get_minsalary(p_jobid VARCHAR2)
RETURN NUMBER
IS
vmin_salary jobs.min_salary%type;
BEGIN
SELECT min_salary
INTO vmin_salary
FROM jobs
WHERE job_id = jobstab(p_jobid).job_id;
RETURN vmin_salary;
END get_minsalary;
FUNCTION get_maxsalary(p_jobid VARCHAR2)
RETURN NUMBER
IS vmax_salary jobs.max_salary%type;
BEGIN
SELECT max_salary
INTO vmax_salary
FROM jobs
WHERE job_id = jobstab(p_jobid).job_id;
RETURN vmax_salary;
END get_maxsalary;
PROCEDURE set_minsalary(p_jobid VARCHAR2, pmin_salary NUMBER)
IS
BEGIN
UPDATE jobs
SET min_salary = pmin_salary
WHERE job_id = jobstab(p_jobid).job_id;
END set_minsalary;
PROCEDURE set_maxsalary(p_jobid VARCHAR2, pmax_salary NUMBER)
IS
BEGIN
UPDATE jobs
SET max_salary = pmax_salary
WHERE job_id = jobstab(p_jobid).job_id;
END set_maxsalary;
END jobs_pkg;
Then I implemented another trigger for before inserting or updating on the jobs table that invoke my jobs_pkg.initialize procedure on the jobs package to ensure that the package state is current before the DML operations are performed:
CREATE OR REPLACE TRIGGER init_jobpkg_trg
BEFORE UPDATE OR INSERT ON jobs
FOR EACH ROW
BEGIN
jobs_pkg.initialize;
END init_jobpkg_trg;
Now the problem is when I tried to run the code below it still mutates:
UPDATE jobs
SET min_salary = min_salary + 1000
WHERE job_id = 'IT_PROG';
I keep searching on why it still mutates but can't seem to find the problem.
Any help will be appreciated.

Triggers are a very powerful tool, but there are a couple of limitations, one of which is that you can't select from the table the trigger is on (the dreaded ORA-04091). This limitation makes total sense. There are plenty of articles on google explaining why this error occurs.
There are 2 ways to solve it.
Option 1: avoid the SELECT ... FROM jobs in the row level trigger
CREATE OR REPLACE TRIGGER JOBS_BIU
AFTER UPDATE OF min_salary ON jobs
FOR EACH ROW
WHEN (new.min_salary != old.min_salary)
BEGIN
UPDATE employees SET salary = :new.min_salary WHERE job_id = :new.job_id;
END;
/
Open 2: Do not use a row level trigger but a statement level trigger. This will fire after the statement has completed:
CREATE OR REPLACE TRIGGER JOBS_BIU
AFTER UPDATE OF min_salary ON jobs
BEGIN
FOR r IN (SELECT * FROM jobs) LOOP
UPDATE employees
SET salary = r.min_salary
WHERE job_id = r.job_id;
END LOOP;
END;
/

Thanks to Koen, I got an idea on how to fix my problem so here it is:
First I have updated my jobs package and eliminated all SELECT statement that was being used:
This is my code before:
CREATE OR REPLACE PACKAGE BODY jobs_pkg
IS
TYPE jobs_tab_type IS TABLE OF jobs%rowtype INDEX BY jobs.job_id%type;
jobstab jobs_tab_type;
PROCEDURE initialize
IS
BEGIN
FOR rec IN (SELECT * FROM jobs)
LOOP
jobstab(rec.job_id) := rec;
END LOOP;
END;
FUNCTION get_minsalary(p_jobid VARCHAR2)
RETURN NUMBER
IS
vmin_salary jobs.min_salary%type;
BEGIN
SELECT min_salary
INTO vmin_salary
FROM jobs
WHERE job_id = jobstab(p_jobid).job_id;
RETURN vmin_salary;
END get_minsalary;
FUNCTION get_maxsalary(p_jobid VARCHAR2)
RETURN NUMBER
IS vmax_salary jobs.max_salary%type;
BEGIN
SELECT max_salary
INTO vmax_salary
FROM jobs
WHERE job_id = jobstab(p_jobid).job_id;
RETURN vmax_salary;
END get_maxsalary;
PROCEDURE set_minsalary(p_jobid VARCHAR2, pmin_salary NUMBER)
IS
BEGIN
UPDATE jobs
SET min_salary = pmin_salary
WHERE job_id = jobstab(p_jobid).job_id;
END set_minsalary;
PROCEDURE set_maxsalary(p_jobid VARCHAR2, pmax_salary NUMBER)
IS
BEGIN
UPDATE jobs
SET max_salary = pmax_salary
WHERE job_id = jobstab(p_jobid).job_id;
END set_maxsalary;
END jobs_pkg;
c. Copy the CHECK_SALARY procedure from Practice 9, Exercise 1a, and modify the code by replacing the query on the JOBS table with statements to set the local minsal and maxsal variables with values from the JOBS_PKG data by calling the appropriate GET_*SALARY functions. This step should eliminate the mutating trigger exception.
CREATE OR REPLACE PROCEDURE check_salary (pjobid employees.job_id%type, psal employees.salary%type)
IS
vminsalary jobs.min_salary%type;
vmaxsalary jobs.max_salary%type;
BEGIN
vminsalary := jobs_pkg.get_minsalary(pjobid);
vmaxsalary := jobs_pkg.get_maxsalary(pjobid);
END check_salary;
and this is my code now:
CREATE OR REPLACE PACKAGE BODY jobs_pkg
IS
TYPE jobs_tab_type IS TABLE OF jobs%rowtype INDEX BY jobs.job_id%type;
jobstab jobs_tab_type;
PROCEDURE initialize
IS
BEGIN
FOR rec IN (SELECT * FROM jobs)
LOOP
jobstab(rec.job_id) := rec;
END LOOP;
END;
FUNCTION get_minsalary(p_jobid VARCHAR2)
RETURN NUMBER
IS
BEGIN
RETURN jobstab(p_jobid).min_salary;
END get_minsalary;
FUNCTION get_maxsalary(p_jobid VARCHAR2)
RETURN NUMBER
IS
BEGIN
RETURN jobstab(p_jobid).max_salary;
END get_maxsalary;
PROCEDURE set_minsalary(p_jobid VARCHAR2, pmin_salary NUMBER)
IS
BEGIN
jobstab(p_jobid).min_salary := pmin_salary;
END set_minsalary;
PROCEDURE set_maxsalary(p_jobid VARCHAR2, pmax_salary NUMBER)
IS
BEGIN
jobstab(p_jobid).max_salary := pmax_salary;
END set_maxsalary;
END jobs_pkg;
Then I removed the for each row from my initializer procedure that is found on my jobs package:
From this:
CREATE OR REPLACE TRIGGER init_jobpkg_trg
BEFORE UPDATE OR INSERT ON jobs
FOR EACH ROW
BEGIN
jobs_pkg.initialize;
END init_jobpkg_trg;
To this:
CREATE OR REPLACE TRIGGER init_jobpkg_trg
BEFORE INSERT OR UPDATE ON jobs
BEGIN
DBMS_OUTPUT.PUT_LINE('-----Initializing-----');
jobs_pkg.initialize;
END init_jobpkg_trg;
After testing of updaing the min_salary of the jobs table, it all now works :)

Related

How to fetch the records from cursor which contains columns from different table?

FIRST PROCEDURE
create or replace PROCEDURE TESTPROCEDURE
(
P_LAF_PK IN NUMBER,
P_RET_VAL OUT SYS_REFCURSOR
) AS
BEGIN
OPEN p_RET_VAL FOR
SELECT LAF.AF_FEE,LAF.AF_FEES_PAYABLE,
LAH.LH_DECISION_DT,LAH.LH_ISSUED
FROM LH_APP_HDR LAH JOIN LIQ_APP_FEE LAF
ON AF_PK = LH_PK
WHERE LAF_APPID = P_LAF_PK;
END TESTPROCEDURE;
Calling this procedure in another procedure
SECOND PROCEDURE
create or replace PROCEDURE TESTPROCEDURE1
AS
BEGIN
DECLARE
V_LIQ_CURSOR SYS_REFCURSOR;
V_LIQ_CURSOR_OUT1 LIQ_APP_FEE%ROWTYPE;
BEGIN
TESTPROCEDURE (2727,V_LIQ_CURSOR);
LOOP
FETCH V_LIQ_CURSOR INTO V_LIQ_CURSOR_OUT1; -- getting error in this line like "Return
types of Result Set variables or query do not match"
EXIT WHEN V_LIQ_CURSOR%NOTFOUND;
IF(V_LIQ_CURSOR_OUT1.AF_FEE != 0) THEN
SELECT (V_LIQ_CURSOR_OUT1.AF_FEES_PAYABLE + V_LIQ_CURSOR_OUT1.AF_FEE) INTO V_TOTALFEE FROM DUAL;
END IF;
END TESTPROCEDURE1;
Can anyone tell me how to get the result from cursor which contains multiple table columns.
NOTE:
I want only two columns from the first procedure

Call a stored procedure in a trigger PL/SQL

Well I'm trying to call a stored procedure in my trigger.
The error I'm getting
"table %s.%s is mutating, trigger/function may not see it"
*Cause: A trigger (or a user defined plsql function that is referenced in
this statement) attempted to look at (or modify) a table that was
in the middle of being modified by the statement which fired it.
*Action: Rewrite the trigger (or function) so it does not read that table.
This is my code of the trigger:
create or replace TRIGGER check_salary_trg
AFTER INSERT OR UPDATE ON employees
FOR EACH ROW
BEGIN
DBMS_OUTPUT.PUT_LINE(:new.job_id ||' '|| :new.salary);
check_salary(:new.job_id, :new.salary);
END;
This is my stored procedure:
PROCEDURE check_salary (job_id employees.job_id%TYPE, salary employees.salary%TYPE)
IS
maxSal NUMBER;
minSal NUMBER;
BEGIN
SELECT MAX(salary) INTO maxSal FROM employees WHERE job_id = job_id;
SELECT MIN(salary) INTO minSal FROM employees WHERE job_id = job_id;
IF maxSal >= salary OR minSal <= salary THEN
RAISE_APPLICATION_ERROR(-20001,'Invalid slary '||salary||'. Salaries for job '||job_id||' must be between '||minSal||' and '||maxSal);
ELSE
DBMS_OUTPUT.PUT_LINE('Test');
END IF;
END;
This is how I try to see that the trigger is working:
UPDATE employees SET salary = 100000 WHERE employee_id = 100;
Somehow the DBMS_OUTPUT.PUT_LINE in my Trigger code is working. But the stored procedure causes the error.
You can't do SELECT or any other DML(INSERT,UPDATE,DELETE) on the table on which trigger is being fired.You have to use compound trigger to get away with mutating table error.
The procedure invoked by trigger is doing a SELECT on the employee table on which trigger is firing and that is forbidden by oracle.
A working example for same issue can be found under, refer UPDATE section Trigger selecting child records, multiplying their values and updating parent record
first of all, why do you query the same table twice in your procedures? that's a huge waste of resources... rather run
SELECT min(salary), max(salary) INTO minSal, maxSal
FROM employees
WHERE job_id = job_id
Second of all, you cannot query the same table you are update.ing !! That's a huge data (in)consistency issue. Why don't you run this in a package/procedure instead? That will give you way better control over your flow
Something like:
CREATE OR REPLACE PROCEDURE prcd_update_salary(p_emp_id INT, p_salary INT)
IS
maxSal INT;
minSal INT;
job_id INT;
BEGIN
SELECT job_id, min(salary), max(salary) INTO job_id, minSal, maxSal
FROM employees
WHERE job_id = (SELECT job_id FROM employees WHERE employee_id = p_emp_id);
IF (p_salary >= minSal AND p_salary <= maxSal) THEN
UPDATE employees SET salary = p_salary WHERE employee_id = p_emp_id;
ELSE
dbms_output.put_line('Sorry, this is out of range!')
dmbs_output.put_line('You can only use from '||minSal||' up to '||maxSal||' for a job id: '|| job_id);
END IF;
END;
This is, of course, only a sample code to give you hints on how to do that.. you have all the logic in one place and not in two diff objects (very hard to debug !!) ... in your production code you have to sanitize the input a maybe do a bit more checks and of course proper indexing is a must - but this pretty much summarizes what I would do :)

Can not perform DML Operation inside a query? While trying to fetch data from collection

here is my PLSQL code:
declare
headerStr varchar2(1000):='C1~C2~C3~C5~C6~C7~C8~C9~C10~C11~C12~C16~C17~C18~C19~RN';
mainValStr varchar2(32000):='1327~C010802~9958756666~05:06AM~NO~DISPOSAL~NDL~4~P32~HELLO~U~28-OCT-2017~28-OCT-2017~Reject~C010741~1;1328~C010802~9958756666~06:07AM~MH~DROP~NDL~1~P32~~U~28-OCT-2017~28-OCT-2017~Reject~C010741~2;1329~C010802~9999600785~01:08AM~BV~DROP~NDL~2~P32~MDFG~U~28-OCT-2017~28-OCT-2017~Reject~C010741~3';
valStr varchar2(4000);
headerCur sys_refcursor;
mainValCur sys_refcursor;
valCur sys_refcursor;
header varchar2(1000);
val varchar2(1000);
iterator number:=1000;
strIdx number;
strLen number;
idx number;
TYPE T_APPROVAL_RECORD IS TABLE OF VARCHAR2(4000) INDEX BY VARCHAR2(1000);
headerTable T_APPROVAL_RECORD;
cnt number;
begin
open headerCur for select * from table(split_str(headerStr,'~'));
open mainValCur for select * from table(split_str(mainValStr,';'));
loop
fetch mainValCur into valStr;
exit when mainValCur%notfound;
insert into header_test values(cnt, valStr); -- for testing purpose
open valCur for select * from table(split_str(valStr,'~'));
loop
fetch valCur into val;
fetch headerCur into header;
exit when valCur%notfound;
exit when headerCur%notfound;
insert into header_test values(header, val);
headerTable(header):= val;
end loop;
idx := headerTable.FIRST; -- Get first element of array
WHILE idx IS NOT NULL LOOP
insert into header_test values (idx, headerTable(idx));
idx := headerTable.NEXT(idx); -- Get next element of array
END LOOP;
headerTable.delete;
end loop;
commit;
end;
c1 c2 ..... c19 are column name and RN is rownumber,
data for the columns of each will be in mainValString seperated by ;
Why i am getting ORA-14551 when i am trying to access collection "headerTable"?
Please help.
Problem is with this line.
idx := headerTable.FIRST;
The index of headertable is of TYPE VARCHAR2 whereas idx is defined as NUMBER.
declare idx as VARCHAR2(1000), it should work.
Having said that, ORA-14551 - Cannot perform DML ... is not related to this error. It is unclear to me why should you encounter this error.
Oh but it does:
EXCEPTION WHEN OTHERS THEN
v_msg:=sqlcode||sqlerrm;
insert into err_data_transfer values('SPLIT_STR',v_msg,sysdate,null);
It may only be during an exception, but it's still DML during a select statement. You may be able to create another procedure as an AUTONOMOUS_TRANSACTION to create the error log. Also, you should either re-raise or raise_application_error afterward. If not your procedure will continue as though the error did not occur; which leads to more problems as to why your main process does not work (including running to completion but doing the wrong thing).

00900. 00000 - "invalid SQL statement" for EXECUTE IMMEDIATE

I am trying to use Dynamic query in my code below but getting error (00900. 00000 - "invalid SQL statement"). Kindly suggest where i am mistaking in the code.
create or replace PROCEDURE CreateInsertTmpTable
AS
crttmp VARCHAR2(200);
intrtmp VARCHAR2(200);
printTableValues VARCHAR2(1000);
BEGIN
crttmp := 'CREATE GLOBAL TEMPORARY TABLE my_temp_table ON COMMIT PRESERVE ROWS AS SELECT * FROM VWBLKDATA WHERE 1=0';
EXECUTE IMMEDIATE crttmp;
intrtmp := 'INSERT INTO my_temp_table SELECT * FROM VWBLKDATA';
EXECUTE IMMEDIATE intrtmp;
printTableValues := ' for data in(SELECT * from my_temp_table)
loop
dbms_output.put_line(data.ID);
end loop';
EXECUTE IMMEDIATE printTableValues;
COMMIT;
END CreateInsertTmpTable;
I think you're overdoing the EXECUTE IMMEDIATE; you can run INSERT statements and PL/SQL without them like:
begin
for i in 1..10 loop
insert into test (some_column) values (to_char(i));
end loop;
end;
But anyways, it looks like you're last EXECUTE IMMEDIATE is trying to execute a partial PL/SQL anonymous block; it's missing a "begin" and "end;"
I would suggest just executing the for loop like so:
for data in (SELECT * from my_temp_table)
loop
dbms_output.put_line(data.ID);
end loop;
or else you'll need to add a begin/end around it in the text (and the "end loop" needs a trailing ";"):
printTableValues := 'begin
for data in (SELECT * from my_temp_table)
loop
dbms_output.put_line(data.ID);
end loop;
end;';

Can I pass an explicit cursor to a function/procedure for use in FOR loop?

I have a procedure that performs some calculations on all records returned by a cursor. It looks a bit like this:
PROCEDURE do_calc(id table.id_column%TYPE)
IS
CURSOR c IS
SELECT col1, col2, col3
FROM table
WHERE ...;
BEGIN
FOR r IN c LOOP
-- do some complicated calculations using r.col1, r.col2, r.col3 etc.
END LOOP;
END;
Now I have the case where I need to perform the exact same calculation on a different set of records that come from a different table. However, these have the same "shape" as in the above in example.
Is it possible to write a procedure that looks like this:
PROCEDURE do_calc2(c some_cursor_type)
IS
BEGIN
FOR r IN c LOOP
-- do the calc, knowing we have r.col1, r.col2, r.col3, etc.
END LOOP;
END;
I know about SYS_REFCURSOR, but I was wondering if it was possible to use the much more convenient FOR ... LOOP syntax and implicit record type.
Create a package.
Declare your cursor as package variable.
Use %rowtype to set function parameter type.
create or replace package test is
cursor c is select 1 as one, 2 as two from dual;
procedure test1;
function test2(test_record c%ROWTYPE) return number;
end test;
create or replace package body test is
procedure test1 is
begin
for r in c loop
dbms_output.put_line(test2(r));
end loop;
end;
function test2(test_record c%ROWTYPE) return number is
l_summ number;
begin
l_summ := test_record.one + test_record.two;
return l_summ;
end;
end test;
I had a similar problem, where I had two cursors that needed to be processed the same way, so this is how I figured it out.
DECLARE
--Define our own rowType
TYPE employeeRowType IS RECORD (
f_name VARCHAR2(30),
l_name VARCHAR2(30));
--Define our ref cursor type
--If we didn't need our own rowType, we could have this: RETURN employees%ROWTYPE
TYPE empcurtyp IS REF CURSOR RETURN employeeRowType;
--Processes the cursors
PROCEDURE process_emp_cv (emp_cv IN empcurtyp) IS
person employeeRowType;
BEGIN
LOOP
FETCH emp_cv INTO person;
EXIT WHEN emp_cv%NOTFOUND;
DBMS_OUTPUT.PUT_LINE('Name = ' || person.f_name ||
' ' || person.l_name);
END LOOP;
END;
--Defines the cursors
PROCEDURE mainProcedure IS
emp empcurtyp;
BEGIN
OPEN emp FOR SELECT first_name, last_name FROM employees WHERE salary > 50000;
process_emp_cv(emp);
CLOSE emp;
OPEN emp FOR SELECT first_name, last_name FROM kuren WHERE first_name LIKE 'J%';
process_emp_cv(emp);
CLOSE emp;
END;
BEGIN
mainProcedure;
END;
/
You can also use this if you want to bulk collect your cursors. You just need to change your helper procedure process_emp_cv; the rest can stay the same.
Using BULK COLLECT
--Processes the cursors
PROCEDURE process_emp_cv (emp_cv IN empcurtyp) IS
TYPE t_employeeRowTable IS TABLE OF employeeRowType;
employeeTable t_employeeRowTable;
BEGIN
LOOP
FETCH emp_cv BULK COLLECT INTO employeeTable LIMIT 50;
FOR indx IN 1 .. employeeTable.Count
LOOP
DBMS_OUTPUT.PUT_LINE('Name = ' || employeeTable(indx).f_name ||
' ' || employeeTable(indx).l_name);
END LOOP;
EXIT WHEN emp_cv%NOTFOUND;
END LOOP;
END;
Try this one, Usong ref cursor.
declare
type c is ref cursor;
c2 c;
type rec is record(
id number,
name varchar(20)
);
r rec;
procedure p1(c1 in out c,r1 in out rec)is begin
loop
fetch c1 into r1;
exit when c1%notfound;
dbms_output.put_line(r1.id || ' ' ||r1.name);
end loop;
end;
begin
open c2 for select id, name from student;
p1(c2,r);
end;
Yes you can use Cursor explicitly into procedure and function,for that cursor need to declare into package as variable

Resources