I have a table that includes customer ID and order ID and some other data.
I want to create a procedure that takes customer ID as input and look inside the table.
if that customer exists then print the order details for that customer and
if customer does not exist then raise an exception "Customer not found."
I have this code, but it's not working properly, or maybe I have the wrong approach to this question.
CREATE OR REPLACE PROCEDURE order_details(customer NUMBER)
IS
CURSOR order_cursor IS
SELECT ORDER_ID, ORDER_DATE, TOTAL, CUSTOMER_ID
FROM PRODUCT_ORDER
WHERE CUSTOMER_ID = customer ;
order_row order_cursor%ROWTYPE ;
customer_error EXCEPTION ;
BEGIN
FOR order_row IN order_cursor
LOOP
IF order_cursor%FOUND THEN
dbms_output.put_line ('order id = ' || order_row.ORDER_ID) ;
ELSE
RAISE customer_error ;
END IF;
END LOOP;
EXCEPTION
WHEN customer_error THEN
dbms_output.put_line ('no customer' ) ;
END;
So if I run the procedure with this line
BEGIN
order_details(103);
END;
I get two results because order exists for this customer.
and if I run the procedure with this line
BEGIN
order_details(101);
END;
I don't get anything (not even the error ) because there is no order for that customer.
Table Data
You must use an "Explicit Cursor" instead of "Cursor FOR LOOP". Because the latter just enter the code between LOOP and END LOOP when the query returns more than one record.
CREATE OR REPLACE PROCEDURE order_details(customer NUMBER)
IS
CURSOR order_cursor IS
SELECT ORDER_ID, ORDER_DATE, TOTAL, CUSTOMER_ID
FROM PRODUCT_ORDER
WHERE CUSTOMER_ID = customer ;
order_row order_cursor%ROWTYPE ;
customer_error EXCEPTION ;
BEGIN
OPEN order_cursor;
LOOP
FETCH order_cursor INTO order_row;
EXIT WHEN order_cursor%NOTFOUND;
dbms_output.put_line ('order id = ' || order_row.ORDER_ID);
END LOOP;
IF order_cursor%rowcount = 0 THEN
RAISE customer_error;
END IF;
CLOSE order_cursor;
EXCEPTION
WHEN customer_error THEN
dbms_output.put_line ('no customer' ) ;
END;
Regards
Related
create table Employees (emp_id number, emp_name varchar2(50), salary number, department_id number, DESIGNATION varchar2(50), DEVELOPED_TESTED varchar2(50)) ;
insert into Employees values(1,'ALex',10000,10,'Developer','VLC');
insert into Employees values(2,'Duplex',20000,20,'Developer','VLC');
insert into Employees values(3,'Charles',30000,30,'Tester','Tested_VLC');
insert into Employees values(4,'Demon',40000,40,'Tester ','Tested_VLC');
insert into employees values(5,'Chaem',5000,50,'Developer','');
Requirement :
I want to return 2 if the designation is 'Developer' and developed_tested column is null. But in the output I am getting null and NOT 2. Can you please check on this.
Code :
create or replace FUNCTION calculate_royalty (
i_empno IN NUMBER
) RETURN VARCHAR2 IS
l_employee employees%ROWTYPE;
BEGIN
SELECT *
INTO l_employee
FROM
employees
WHERE emp_id = i_empno;
IF l_employee.designation = 'Developer' THEN
RETURN l_employee.developed_tested;
ELSIF l_employee.designation = 'Developer' and l_employee.developed_tested is null THEN
RETURN 2;
ELSE
RETURN 1;
END IF;
EXCEPTION WHEN NO_DATA_FOUND THEN
RETURN 0;
END ;
select CALCULATE_ROYALTY(5) from dual; -- Output coming as NULL but it should be 2 ideally.
Just select the entire row into a variable, no need to use a count. Use the NO_DATA_FOUND exception to handle the case of user does not exist.
Your question mentions some functionality related to the "royalty" column. That is missing from your test case but it should be a piece of cake to modify code below so it caters to all your needs.
create or replace FUNCTION calculate_royalty (
empno_i IN NUMBER
-- don't use column names as names of input parameters, that is a recipe for disaster. Use prefix/suffix.
) RETURN VARCHAR2 IS
lv_count NUMBER;
l_employee employees%ROWTYPE;
BEGIN
SELECT *
INTO l_employee
FROM
employees
WHERE emp_id = empno_i;
IF l_employee.designation = 'Developer' THEN
RETURN l_employee.salary;
ELSE
RETURN 1;
END IF;
-- dbms_output just sends data to a buffer that you then print to the console. it cannot be used as a return value.
-- RETURN dbms_output.put_line('Emp Name :'...
-- elsif needs to be followed by an expression. It cannot be followed by a RETURN keyword. Use "ELSE" instead if there is no condition.
-- elsif return 1;
EXCEPTION WHEN NO_DATA_FOUND THEN
RETURN 0;
END ;
/
set serveroutput on size 999999
clear screen
BEGIN
dbms_output.put_line('output for Developer: '|| calculate_royalty(EMPNO_I => 2));
dbms_output.put_line('output for no Developer: '|| calculate_royalty(EMPNO_I => 3));
dbms_output.put_line('output for non-existing user: '|| calculate_royalty(EMPNO_I => 10));
END;
output for Developer: 20000
output for no Developer: 1
output for non-existing user: 0
PL/SQL procedure successfully completed.
I have a PL/SQL procedure for deleting records corresponding to a field having NULL value. Am able to achieve this by the below query.
set serveroutput on;
begin
dbms_output.put_line('Execution started');
for rec in (
select name from employee where emp_id is null
and location = 'SITE_A'
)
loop
delete from employeedetails#sitea where name = rec.name;
dbms_output.put_line('name '|| rec.name ||' deleted');
end loop;
dbms_output.put_line('Execution completed');
end;
/
set serveroutput off;
I am running the for loop query from one database and deleting the records in another database ( sitea ) using a database link.
I need to add a condition like, if the name=rec.name is not returning any records to be deleted,then dbms_output.put_line('No records to be deleted');
Is there a comfortable way to achieve it ?
You can use the sql%rowcount implicit cursor attribute to see how many rows were affected by a DML statement; that works across a database link as well as locally:
if sql%rowcount = 0 then
dbms_output.put_line('No records to be deleted');
else
dbms_output.put_line('name '|| rec.name ||' deleted');
end if;
You can include it in the messages too:
if sql%rowcount = 0 then
dbms_output.put_line('name '|| rec.name ||': no records to be deleted');
else
dbms_output.put_line('name '|| rec.name ||': '|| sql%rowcount ||' record(s) deleted');
end if;
Then you'll see something like:
Execution started
name B: 1 record(s) deleted
name C: no records to be deleted
Execution completed
PL/SQL procedure successfully completed.
As name probably isn't unique you could encounter the same value twice as you go through your loop; in which case the first delete will find multiple rows and the second delete will find none. You could avoid the second one by adding distinct to your cursor query.
And if you didn't want to see which names did and did not have remote data to delete then you could use a much simpler single delete, with no loop or PL/SQL, but it seems like this is an exercise anyway...
My whole intention to catch exception,WRONG parameter is NOT CATCHING exception.
Here is the code:
CREATE OR REPLACE PROCEDURE list_emp (p_emp_id IN employees.employee_id%TYPE,
p_dept_id IN employees.department_id%TYPE)
IS
CURSOR c1 IS
SELECT *
FROM EMPLOYEES
WHERE EMPLOYEE_ID=p_emp_id
AND DEPARTMENT_ID=p_dept_id;
emp_rec c1%ROWTYPE;
BEGIN
OPEN c1;
LOOP
FETCH c1 INTO emp_rec;
EXIT WHEN c1%NOTFOUND;
DBMS_OUTPUT.PUT_LINE(emp_rec.employee_id||' '||emp_rec.first_name||' '||emp_rec.last_name);
END LOOP;
CLOSE c1;
EXCEPTION
WHEN NO_DATA_FOUND THEN
DBMS_OUTPUT.PUT_LINE('No Record Found ');
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('No Record Found ');
END;
When the cursor is opened and fetched with the wrong parameter that does not match any row from the corresponding table, the following line
EXIT WHEN c1%NOTFOUND;
cause the plsql procedure to terminate (because there were no rows found). Hence no exception is raised.
If you do want to display some sort of output you can do the following instead
IF c1%FOUND THEN
dbms_output.put_line('Record Found');
ELSE
dbms_output.put_line('Finished/Done');
EXIT;
END IF;
If you want to raise an error after looping through a cursor that returns no rows, then you're going to have to use a counter to work out how many rows have been processed, and then you can do something if no rows have been processed.
Something like:
create or replace procedure list_emp (p_emp_id in employees.employee_id%type,
p_dept_id in employees.department_id%type)
is
cursor c1 is
select employee_id,
first_name,
last_name
from employees
where employee_id = p_emp_id
and department_id = p_dept_id;
v_count number := 0;
begin
for emp_rec in c1
loop
v_count := v_count + 1;
dbms_output.put_line(emp_rec.employee_id||' '||emp_rec.first_name||' '||emp_rec.last_name);
end loop;
if v_count = 0 then
raise no_data_found;
end if;
exception
when no_data_found then
dbms_output.put_line('No Record Found.');
raise;
when others then
dbms_output.put_line('An error occurred: '||sqlerrm);
raise;
end;
/
A few notes:
I converted your cursor loop into a cursor-for-loop; you don't need to worry about declaring the record type and also Oracle handles the opening and closing of the cursor for you.
I added raise; to each of your exception handlers - in general, having when others then null (which is effectively what your original code was doing - no errors are raised to the calling code) is a bad idea. I added the raise to the no_data_found condition as that wasn't doing anything either; typically, if you have an exception condition, you want it to do something to let the calling code know there was a problem (not always, of course; sometimes you don't want the processing to stop if a particular error condition is met).
Your cursor was selecting all columns, but in your procedure, you were only using three of them. I've therefore amended the cursor so that it only pulls back those three columns.
Don't rely on dbms_output in your production code. Code that calls this procedure won't see anything populated in dbms_output, unless it explicitly looks for it - and that's not something I've ever seen in any production code, outside of Database tools (eg. SQL*Plus, Toad, etc). I've left this in your procedure as I've a feeling this is a learning exercise for you, but please don't think that this is in any way acceptable in production code.
You're passing p_emp_id in as a parameter - typically, that's the primary key of the employees table. If that's the case, then there's no need for the cursor for loop at all - you could do it by using select ... into ... instead, like so:
.
create or replace procedure list_emp (p_emp_id in employees.employee_id%type,
p_dept_id in employees.department_id%type)
is
v_emp_id employees.employee_id%type;
v_first_name employees.first_name%type;
v_last_name employees.last_name%type;
begin
select employee_id,
first_name,
last_name
into v_emp_id,
v_first_name,
v_last_name
from employees
where employee_id = p_emp_id
and department_id = p_dept_id;
dbms_output.put_line(emp_rec.employee_id||' '||emp_rec.first_name||' '||emp_rec.last_name);
exception
when no_data_found then
dbms_output.put_line('No Record Found.');
raise;
when others then
dbms_output.put_line('An error occurred: '||sqlerrm);
raise;
end;
/
Alternatively, just pass back a ref cursor:
create or replace procedure list_emp (p_emp_id in employees.employee_id%type,
p_dept_id in employees.department_id%type,
p_ref_cur out sys_refcursor)
is
begin
open p_ref_cur for select employee_id,
first_name,
last_name
from employees
where employee_id = p_emp_id
and department_id = p_dept_id;
-- No need for an exception handler here since you're not storing the error details anyway.
-- By not having an error handler, any error will automatically be raised up to the calling code
-- and it will have the correct error stack trace info (e.g. the line number the error occurred,
-- rather than the line the error was reraised from
end;
/
And to run the ref cursor in SQL*Plus (or as a script in Toad/SQL Developer/etc), you do the following:
-- create a variable outside of PL/SQL to hold the ref cursor pointer (this is a SQL*Plus command):
variable rc refcursor;
-- populate our ref cursor variable with the pointer. Note how we pass it in as a bind variable
begin
list_emp(p_emp_id => 1234,
p_dept_id => 10,
p_ref_cur => :rc);
end;
/
-- finally, print the contents of the ref cursor.
print rc;
I have table named "players" like this
Name Country
---------- ------------
Sachin India
Ponting Australia
I have written a PL/SQL Procedure to execute it by giving "name" as parameter.
Here is the code-
CREATE OR REPLACE PROCEDURE NEW_TEST ( player IN players.name%type, place IN players.country%type ) IS
countri players.country%type;
BEGIN
SELECT country into countri from players where name = player;
END;
DECLARE
player players.name%type;
place players.country%type;
CURSOR cu_new0 is
SELECT name, country from players where name=player;
BEGIN
player:='Sachin' ;
FOR pl_all in cu_new0
LOOP
NEW_TEST (player, place);
dbms_output.put_line ('The player ' || player || ' play for ' || pl_all.country);
END LOOP;
EXCEPTION
WHEN NO_DATA_FOUND THEN
dbms_output.put_line('No such player!');
WHEN OTHERS THEN
dbms_output.put_line('Error!');
END;
Now when I am putting player:='Sachin' it is giving output but when I am giving player:= 'Sachin1' is not showing any output and more importantly it is not even going to exception of 'NO_DATA_FoUND'. Can you please help me in this regard. Thanx
If you code a loop, Oracle does not throw an exception if no data is returned (just as it doesn't throw a TOO_MANY_ROWS exception if more than one row is returned).
Your one of the select clause is not handling exception; you are consuming exception in that code.
Your Original Code:
BEGIN
SELECT country into countri from players where name = player;
END;
Modify with below code--
BEGIN
SELECT country into countri from players where name = player;
exception when no_data_found then
raise_application_error(......);
END;
DECLARE
player players.name%type;
place players.country%type;
CURSOR cu_new0 is
SELECT name, country from players where name=player;
type l_cu_new0 is table of cu_new0%rowtype;
v_cu_new0 l_cu_new0;
BEGIN
player:='Sachin' ;
open cu_new0;
fetch cu_new0 bulk collect into v_cu_new0;
close cu_new0;
if v_cu_new0.count = 0 then
raise NO_DATA_FOUND;
end;
FOR i in v_cu_new0.first .. v_cu_new0.last
LOOP
...
END LOOP;
EXCEPTION
WHEN NO_DATA_FOUND THEN
dbms_output.put_line('No such player!');
WHEN OTHERS THEN
dbms_output.put_line('Error!');
END;
There are your cursor is NULL. Because;
Temp code block :
select name
,country
from players
where name = :player;
Run code block :
select name
,country
from players
where name = 'Schin1';
Run code block return NULL and your cursor is have NULL value.
Then your for loop code block is not work.
You can solve this problem such as ;
CURSOR cu_new0 is
SELECT name, country from players;
Hi #Warrior92 for edit,
There are maybe you can try it ;
for pl_all in cu_new0 loop
new_test(player
,place);
if pl_all.name = player then
dbms_output.put_line('The player ' || player || ' play for ' || pl_all.country);
end if;
end loop;
If you really want to use a cursor, the for loop and an exception, you can try the following code:
DECLARE
-- create table type of players
TYPE t_players_tab IS TABLE OF players%ROWTYPE;
-- declare variable / array which will contain the result of cursor's select
l_players_arr t_players_tab := NEW t_players_tab();
CURSOR c_fetch_player IS
SELECT
*
FROM
players
WHERE
name = player;
-- declare exception which is to be caught within the EXCEPTION block
EXCEPTION e_player_not_found;
-- init the exception giving it the sqlcode -20001 (valid numbers for custom exceptions are in range from -20000 to -20999)
PRAGMA EXCEPTION_INIT(e_player_not_found, -20001);
BEGIN
-- fetch the cursor result into the array
OPEN c_fetch_player;
FETCH c_fetch_player BULK COLLECT INTO l_players_arr;
CLOSE c_fetch_player;
-- check if the array contains any results
IF l_players_arr.COUNT > 0 THEN
-- iterate through the rows in the array
FOR idx l_players_arr.FIRST .. l_players_arr.LAST
LOOP
dbms_output.put_line ('The player ' || player || ' play for ' || l_players_arr(idx).country);
END LOOP;
ELSE -- if the array has no rows, raise application arror with the same sqlcode as defined in EXCEPTION_INIT
raise_application_error(-20001,'Player ' || player || 'not found');
END IF;
EXCEPTION
-- catch the exception
WHEN e_player_not_found THEN
dbms_output.put_line(sqlcode || ': ' || sqlerrm);
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE(DBMS_UTILITY.FORMAT_CALL_STACK);
DBMS_OUTPUT.PUT_LINE(DBMS_UTILITY.FORMAT_ERROR_STACK);
DBMS_OUTPUT.PUT_LINE(DBMS_UTILITY.FORMAT_ERROR_BACKTRACE);
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;