0

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;
Franktrt
  • 373
  • 1
  • 8
  • 18

2 Answers2

0

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;
DrabJay
  • 2,989
  • 2
  • 13
  • 12
0

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.

  • This solution does not deal with concurrency control, so with multiple sessions it is possible to have a supervisor managing more than 5 employees. If concurrency control is added then using this method you would need to serialize access to the entire employees table which may cause scalability issues. – DrabJay Sep 19 '14 at 11:26