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 INSERT
、BEFORE/AFTER UPDATE
和BEFORE/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 触发器得到一个变异表错误的主要内容,如果未能解决你的问题,请参考以下文章