Search code examples
plsqltriggersoracle10gmutating-table

PL/SQL Trigger gets a mutating table error


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;

Solution

  • 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.