PL/SQL 触发器得到一个变异表错误

Posted

技术标签:

【中文标题】PL/SQL 触发器得到一个变异表错误【英文标题】:PL/SQL Trigger gets a mutating table error 【发布时间】:2014-09-18 15:55:46 【问题描述】:

我的触发器想要检查“新”经理是否监督不超过 5 名员工。 BLOCKED_MANAGER 表(ssn,numberofemployees)中仅监督 5 人的经理。 最后,每个更新都记录在 SUPERLOG 表中(日期、用户、旧管理器、新管理器)。 我没有收到有关触发器的编译错误,但是当我更新 superssn 时出现此错误:

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'

我该如何解决这个触发器?谢谢

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;

【问题讨论】:

【参考方案1】:

可能最快的解决方法是使用精心构造的语句触发器而不是行触发器。行触发器在其中包含短语FOR EACH ROW,对修改的每一行调用(基于触发器上的BEFORE/AFTER INSERTBEFORE/AFTER UPDATEBEFORE/AFTER DELETE 约束),可以看到相应的:NEW 和:OLD值,并且受制于“不能查看定义触发器的表”规则。语句触发器在每个执行的 语句 的适当时间调用,无法查看行值,但不受查看定义它们的特定表的限制。因此,对于不需要使用 :NEW 或 :OLD 值的逻辑部分,这样的触发器可能会很有用:

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;

请注意,如果您删除 BLOCKED_MANAGER 表(此触发器仍然维护该表,尽管我不知道它是否真的有必要),逻辑将大大减少。

您仍然需要一个行触发器来处理日志记录,但这只是减少现有触发器的问题,我将把它留给您。 :-)

分享和享受。

【讨论】:

此解决方案不处理并发控制,因此对于多个会话,可以让主管管理超过 5 名员工。如果添加了并发控制,那么使用此方法您将需要序列化对整个员工表的访问,这可能会导致可伸缩性问题。【参考方案2】:

正如您所发现的,您不能从定义行级触发器的同一个表中进行选择;它会导致表变异异常。

为了使用触发器正确创建此验证,应创建一个过程以获取用户指定的锁,以便验证可以在多用户环境中正确序列化。

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;

然后可以在复合触发器中使用此过程(假设至少是 Oracle 11,这将需要在早期版本中拆分为单独的触发器)

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;

您不需要blocked_manager 表来管理此约束。此信息可以从employee 表中获得。

或者在 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;

【讨论】:

以上是关于PL/SQL 触发器得到一个变异表错误的主要内容,如果未能解决你的问题,请参考以下文章

Oracle触发器-变异表触发器不能访问本表

Oracle 触发器 - 变异表的问题

更新前Oracle SQL变异表触发器

仅在 old = new 上进行选择时获取变异表 (ORA-04091)

PL/SQL 触发游标错误

触发器中的 Oracle PL/SQL 游标