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 :)
Related
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 :)
i have this table
TABLE "KEYWORD_RSLT"
( "ID" NUMBER PRIMARY KEY,
"SESSION_MONTH" VARCHAR2(40) NOT NULL ENABLE,
"PATIENT_NAME" VARCHAR2(50)
and i have this procedure that imports data to my table KEYWORD_RSLT.
create or replace PROCEDURE "PR_KEYWORD_SEARCH" (v_patient_id NUMBER, v_keyWord varchar2)
IS
BEGIN
delete from KEYWORD_RSLT;
insert into KEYWORD_RSLT (SESSION_MONTH, PATIENT_NAME)
select distinct
to_char(s.SESSION_DATE, 'MM-YYYY') as SESSION_MONTH,
p.FIRST_NAME ||' '||p.LAST_NAME as PATIENT_NAME
from SESSIONS s,
CLIENTS p
where s.CLIENTS_ID = p.ID
and (s.CRITICAL_POINT like LOWER ('%'||v_keyWord||'%') and s.CLIENTS_ID = v_patient_id
or s.ACTIONS like LOWER ('%'||v_keyWord||'%') and s.CLIENTS_ID = v_patient_id);
END PR_KEYWORD_SEARCH;
I want my primary key "ID" to take automatically the next available number starting from 1, but when my procedure deletes all data from this table, i want to start again from 1.
I tried with sequence ("SQ_KEYWORD_RSLT_INCREAMENT") and trigger but i can not reset this sequence from a new procedure using this code:
alter sequence SQ_KEYWORD_RSLT_INCREAMENT restart start with 1;
How can i fill my ID automatically from the beginning every time i delete all the data?
You say i can not reset this sequence but you don't say why so I'm assuming that you got an error. It is not possible to execute ddl statements in pl/sql directly, but it can be done using EXECUTE IMMEDIATE.
CREATE SEQUENCE koen_s START WITH 1;
Sequence KOEN_S created.
SELECT koen_s.NEXTVAL FROM DUAL;
NEXTVAL
----------
1
BEGIN
EXECUTE IMMEDIATE 'ALTER SEQUENCE koen_s RESTART START WITH 1';
END;
/
PL/SQL procedure successfully completed.
SELECT koen_s.NEXTVAL FROM DUAL;
NEXTVAL
----------
1
I need to create a trigger that when the last employee of the dept is deleted from emp table, the dept is deleted from the dept table.
emp(empno, ename, deptno)
dept(deptno, dname)
First, I created a procedure that deletes the dept from dept table given a deptno.
CREATE OR REPLACE PROCEDURE del_dept
(v_dno in number)
is
begin
delete from DEPT where deptno = v_dno;
end;
Then I created a trigger that deletes the dept when the last emp in that dept is deleted. I tried to test the trigger by deleting one of the three emps in deptno10, but I got error mesg from command that trigger is invalid and failed re-validation.
create or replace trigger del_dept
after delete on EMP
for each row
DECLARE
emp_count Number;
g_dno Number;
begin
SELECT COUNT(:old.ename) INTO emp_count FROM emp group by deptno;
FOR i IN 1.. emp_count LOOP
IF i = emp_count THEN
del_dept(g_dno);
end if;
END LOOP;
End;
Error trigger is invalid and failed re-validation specifies that yor trigger was created with compilation error hence you were not able to test it. This is not a good idea to write a trigger in your case. I can see few issues in you code. So lets first resolve these issues.
When you write the code below, your trigger would be compiled and would be a valid one. So you will not get the error trigger is invalid and failed re-validation
CREATE OR REPLACE TRIGGER del_dept
AFTER DELETE
ON EMP
FOR EACH ROW
DECLARE
EMP_CNT NUMBER;
BEGIN
SELECT COUNT (1)
INTO emp_cnt
FROM emp
WHERE deptno = :old.deptno;
IF emp_cnt = 0
THEN
---del_dept is your procedure to delete records from dept table
del_dept (:old.deptno);
END IF;
END;
But when you would try to execute it you will further get the error:
ORA-04091: table EMP is mutating, trigger/function may not see it
ORA-06512: at "DEL_DEPT", line 4 ORA-04088: error during execution of
trigger 'DEL_DEPT'
The reason for getting this error is you are trying to select from the table from which you are deleting records and thats not allowed in oracle.
The best way for your case is to write a procedure which would delete the records once the last employee data is deleted from your dept table . See below:
CREATE OR REPLACE PROCEDURE del_dept (dept_no NUMBER)
IS
EMP_CNT NUMBER;
BEGIN
SELECT COUNT (1)
INTO emp_cnt
FROM emp
WHERE deptno = dept_no;
IF emp_cnt = 0
THEN
del_dept (dept_no);
END IF;
END;
Lets say I have a table as follows--
create table employees
(
eno number(4) not null primary key,
ename varchar2(30),
zip number(5) references zipcodes,
hdate date
);
And I'm trying to create a trigger with--
CREATE OR REPLACE TRIGGER TWELVE_ONE
BEFORE INSERT OR UPDATE
ON EMPLOYEES
FOR EACH ROW
DECLARE
V_DATE VARCHAR2 (10);
BEGIN
SELECT TO_CHAR (SYSDATE, 'hh24:mi:ss') INTO V_DATE FROM DUAL;
IF (V_DATE >= '12:00:01' AND V_DATE < '13:00:00')
THEN
INSERT INTO TABLE ?????
ELSE
ROLLBACK? TERMINATE TRANSACTION?
END IF;
END;
Purpose of the trigger is to allow an insertion/update during 12:00-13:00 and prevent the insertion at any other time. The trigger construction (thanks to #Melkikun) is seems ok. However now I'm facing the following issues--
How is it possible to pass the values here? I mean lets say my create statement is:
Insert into employees Values (1, 'someone', 11111, '17-12-2015')
And lets say the time is 12:30:01 now. How would the trigger perform the insertion without knowing the values?
And lets say the time is now 13:00:1 now. How would the trigger stop/prevent the insertion?
I'm using Oracle SQL Developer 4.02.15
Many Thanks
You just have to do it the other way.
If the time is not correct,then you raise an exception, so the insert won't be done.
CREATE OR REPLACE TRIGGER TWELVE_ONE
BEFORE INSERT OR UPDATE
ON EMPLOYEES
FOR EACH ROW
DECLARE
V_DATE VARCHAR2 (10);
MyException exception;
BEGIN
SELECT TO_CHAR (SYSDATE, 'hh24:mi:ss') INTO V_DATE FROM DUAL;
IF (V_DATE < '12:00:01' OR V_DATE > '13:00:00')
THEN
raise MyException;
END IF;
EXCEPTION
When MyException then
ROLLBACK;
//output message ...
END;
How would the trigger perform the insertion without knowing the values?
The trigger knows the value thanks to :NEW and :OLD.
You normally use the terms in a trigger using :old to reference the old value and :new to reference the new value.So you will have :NEW.eno ,:NEW.ename ...
Here is an example from the Oracle documentation :
CREATE OR REPLACE TRIGGER Print_salary_changes
BEFORE DELETE OR INSERT OR UPDATE ON Emp_tab
FOR EACH ROW
WHEN (new.Empno > 0)
DECLARE
sal_diff number;
BEGIN
sal_diff := :new.sal - :old.sal;
dbms_output.put('Old salary: ' || :old.sal);
dbms_output.put(' New salary: ' || :new.sal);
dbms_output.put_line(' Difference ' || sal_diff);
END;
My trigger wants to check if a 'new' manager supervises no more than 5 employees.
Manager supervising only 5 people are in BLOCKED_MANAGER table(ssn,numberofemployees).
Finally, every update is recorded in SUPERLOG table(date,user,old_manager,new_manager).
I get no compiling error about the trigger, but when I update a superssn I get this error:
SQL> update employee set superssn='666666607' where ssn='111111100';
update employee set superssn='666666607' where ssn='111111100'
*
ERROR at line 1:
ORA-04091: Table FRANK.EMPLOYEE is mutating, the trigger/function
can't read it
ORA-06512: a "FRANK.TLOG", line 20
ORA-04088: error during execution of trigger 'FRANK.TLOG'
How can I solve this trigger? Thank you
create or replace trigger tlog
before update of superssn on employee
for each row
declare
t1 exception;
n number:=0;
cont number:=0;
empl varchar2(16);
cursor cur is (select ssn from blocked_manager where ssn is not null);
begin
open cur;
loop
fetch cur into empl;
exit when cur%notfound;
if(:new.superssn = empl) then
n:=1;
end if;
end loop;
close cur;
if n=1 then
raise t1;
end if;
select count(*) into cont from employee group by superssn having superssn=:new.superssn;
if(cont=4) then
insert into blocked_manager values(:new.superssn,5);
end if;
insert into superlog values(sysdate,user,:old.superssn, :new.superssn );
exception
when t1 then
raise_application_error(-20003,'Manager '||:new.superssn||' has already 5 employees');
end;
Probably the quickest way around this is to use a carefully constructed statement trigger instead of a row trigger. Row triggers have the phrase FOR EACH ROW in them, are invoked for each row which is modified (based on the BEFORE/AFTER INSERT, BEFORE/AFTER UPDATE, and BEFORE/AFTER DELETE constraints on the trigger), can see the appropriate :NEW and :OLD values, and are subject to the "can't look at the table on which the trigger is defined" rule. Statement triggers are invoked at the appropriate time for each statement which is executed, can't see row values, but aren't subject to the limits on looking at the particular table on which they're defined. So for the portions of your logic which don't need to work with :NEW or :OLD values a trigger such as this might prove useful:
CREATE OR REPLACE TRIGGER EMPLOYEE_S_BU
BEFORE UPDATE ON EMPLOYEE
-- Note: no BEFORE EACH ROW phrase, so this is a statement trigger
BEGIN
-- The following FOR loop should insert rows into BLOCKED_MANAGER for all
-- supervisors which have four or more employees under them and who are not
-- already in BLOCKED_MANAGER.
FOR aRow IN (SELECT e.SUPERSSN, COUNT(e.SUPERSSN) AS EMP_COUNT
FROM EMPLOYEE e
LEFT OUTER JOIN BLOCKED_MANAGER b
ON b.SSN = e.SUPERSSN
WHERE b.SSN IS NULL
GROUP BY e.SUPERSSN
HAVING COUNT(e.SUPERSSN) >= 4)
LOOP
INSERT INTO BLOCKED_MANAGER
(SSN, EMPLOYEE_COUNT)
VALUES
(aRow.SUPERSSN, aRow.EMP_COUNT);
END LOOP;
-- Remove rows from BLOCKED_MANAGER for managers who supervise fewer
-- than four employees.
FOR aRow IN (SELECT e.SUPERSSN, COUNT(e.SUPERSSN) AS EMP_COUNT
FROM EMPLOYEE e
INNER JOIN BLOCKED_MANAGER b
ON b.SSN = e.SUPERSSN
GROUP BY e.SUPERSSN
HAVING COUNT(e.SUPERSSN) <= 3)
LOOP
DELETE FROM BLOCKED_MANAGER
WHERE SSN = aRow.SUPERSSN;
END LOOP;
-- Finally, if any supervisor has five or more employees under them,
-- raise an exception. Note that we go directly to EMPLOYEE to determine
-- the number of employees supervised.
FOR aRow IN (SELECT SUPERSSN, COUNT(*) AS EMP_COUNT
FROM EMPLOYEE
GROUP BY SUPERSSN
HAVING COUNT(*) >= 5)
LOOP
-- If we get here we've found a supervisor with 5 (or more) employees.
-- Raise an exception
RAISE_APPLICATION_ERROR(-20000, 'Found supervisor ' || aRow.SUPERSSN ||
' supervising ' || aRow.EMP_COUNT ||
' employees');
END LOOP;
END EMPLOYEE_S_BU;
Note that if you get rid of the BLOCKED_MANAGER table (which this trigger still maintains, although I don't know if it's truly necessary) the logic gets cut down considerably.
You'll still need a row trigger to handle the logging, but as that's just a matter of cutting down your existing trigger I'll leave that to you. :-)
Share and enjoy.
As you have discovered, you cannot select from the same table that a row-level trigger is defined against; it causes a table mutating exception.
In order to properly create this validation using a trigger a procedure should be created to obtain user-specified locks so the validation can be correctly serialized in a multi-user environment.
PROCEDURE request_lock
(p_lockname IN VARCHAR2
,p_lockmode IN INTEGER DEFAULT dbms_lock.x_mode
,p_timeout IN INTEGER DEFAULT 60
,p_release_on_commit IN BOOLEAN DEFAULT TRUE
,p_expiration_secs IN INTEGER DEFAULT 600)
IS
-- dbms_lock.allocate_unique issues implicit commit, so place in its own
-- transaction so it does not affect the caller
PRAGMA AUTONOMOUS_TRANSACTION;
l_lockhandle VARCHAR2(128);
l_return NUMBER;
BEGIN
dbms_lock.allocate_unique
(lockname => p_lockname
,lockhandle => p_lockhandle
,expiration_secs => p_expiration_secs);
l_return := dbms_lock.request
(lockhandle => l_lockhandle
,lockmode => p_lockmode
,timeout => p_timeout
,release_on_commit => p_release_on_commit);
IF (l_return not in (0,4)) THEN
raise_application_error(-20001, 'dbms_lock.request Return Value ' || l_return);
END IF;
-- Must COMMIT an autonomous transaction
COMMIT;
END request_lock;
This procedure can then be used in a compound trigger (assuming at least Oracle 11, this will need to be split into individual triggers in earlier versions)
CREATE OR REPLACE TRIGGER too_many_employees
FOR INSERT OR UPDATE ON employee
COMPOUND TRIGGER
-- Table to hold identifiers of inserted/updated employee supervisors
g_superssns sys.odcivarchar2list;
BEFORE STATEMENT
IS
BEGIN
-- Reset the internal employee supervisor table
g_superssns := sys.odcivarchar2list();
END BEFORE STATEMENT;
AFTER EACH ROW
IS
BEGIN
-- Store the inserted/updated supervisors of employees
IF ( ( INSERTING
AND :new.superssn IS NOT NULL)
OR ( UPDATING
AND ( :new.superssn <> :old.superssn
OR :new.superssn IS NOT NULL AND :old.superssn IS NULL) ) )
THEN
g_superssns.EXTEND;
g_superssns(g_superssns.LAST) := :new.superssn;
END IF;
END AFTER EACH ROW;
AFTER STATEMENT
IS
CURSOR csr_supervisors
IS
SELECT DISTINCT
sup.column_value superssn
FROM TABLE(g_superssns) sup
ORDER BY sup.column_value;
CURSOR csr_constraint_violations
(p_superssn employee.superssn%TYPE)
IS
SELECT count(*) employees
FROM employees
WHERE pch.superssn = p_superssn
HAVING count(*) > 5;
r_constraint_violation csr_constraint_violations%ROWTYPE;
BEGIN
-- Check if for any inserted/updated employee there exists more than
-- 5 employees for the same supervisor. Serialise the constraint for each
-- superssn so concurrent transactions do not affect each other
FOR r_supervisor IN csr_supervisors LOOP
request_lock('TOO_MANY_EMPLOYEES_' || r_supervisor.superssn);
OPEN csr_constraint_violations(r_supervisor.superssn);
FETCH csr_constraint_violations INTO r_constraint_violation;
IF csr_constraint_violations%FOUND THEN
CLOSE csr_constraint_violations;
raise_application_error(-20001, 'Supervisor ' || r_supervisor.superssn || ' now has ' || r_constraint_violation.employees || ' employees');
ELSE
CLOSE csr_constraint_violations;
END IF;
END LOOP;
END AFTER STATEMENT;
END;
You do not need the blocked_manager table to manage this constraint. This information can be derived from the employee table.
Or in versions earlier than Oracle 11i:
CREATE OR REPLACE PACKAGE employees_trg
AS
-- Table to hold identifiers of inserted/updated employee supervisors
g_superssns sys.odcivarchar2list;
END employees_trg;
CREATE OR REPLACE TRIGGER employee_biu
BEFORE INSERT OR UPDATE ON employee
IS
BEGIN
-- Reset the internal employee supervisor table
employees_trg.g_superssns := sys.odcivarchar2list();
END;
CREATE OR REPLACE TRIGGER employee_aiur
AFTER INSERT OR UPDATE ON employee
FOR EACH ROW
IS
BEGIN
-- Store the inserted/updated supervisors of employees
IF ( ( INSERTING
AND :new.superssn IS NOT NULL)
OR ( UPDATING
AND ( :new.superssn <> :old.superssn
OR :new.superssn IS NOT NULL AND :old.superssn IS NULL) ) )
THEN
employees_trg.g_superssns.EXTEND;
employees_trg.g_superssns(employees_trg.g_superssns.LAST) := :new.superssn;
END IF;
END;
CREATE OR REPLACE TRIGGER employee_aiu
AFTER INSERT OR UPDATE ON employee
IS
DECLARE
CURSOR csr_supervisors
IS
SELECT DISTINCT
sup.column_value superssn
FROM TABLE(employees_trg.g_superssns) sup
ORDER BY sup.column_value;
CURSOR csr_constraint_violations
(p_superssn employee.superssn%TYPE)
IS
SELECT count(*) employees
FROM employees
WHERE pch.superssn = p_superssn
HAVING count(*) > 5;
r_constraint_violation csr_constraint_violations%ROWTYPE;
BEGIN
-- Check if for any inserted/updated employee there exists more than
-- 5 employees for the same supervisor. Serialise the constraint for each
-- superssn so concurrent transactions do not affect each other
FOR r_supervisor IN csr_supervisors LOOP
request_lock('TOO_MANY_EMPLOYEES_' || r_supervisor.superssn);
OPEN csr_constraint_violations(r_supervisor.superssn);
FETCH csr_constraint_violations INTO r_constraint_violation;
IF csr_constraint_violations%FOUND THEN
CLOSE csr_constraint_violations;
raise_application_error(-20001, 'Supervisor ' || r_supervisor.superssn || ' now has ' || r_constraint_violation.employees || ' employees');
ELSE
CLOSE csr_constraint_violations;
END IF;
END LOOP;
END;