我相信它们很简单
其实这些任务对于触发器来说并不简单。业务逻辑很简单,执行业务逻辑的SQL很简单,但是在触发器中实现却很难。要了解为什么您需要了解触发器的工作原理。
触发器作为事务的一部分触发,这意味着它们被应用于 SQL 语句的结果,例如插入或更新。有两种类型的触发器,行级触发器和语句级触发器。
行级触发器为结果集中的每一行触发一次,我们可以引用当前行中的值,这对于评估行级规则很有用。但是我们不能对拥有的表执行 DML:Oracle hurls ORA- 04088 mutating table 异常,因为此类操作违反了事务完整性。
语句级触发器每个语句只触发一次。因此,它们对于执行表级规则很有用,但关键是它们无法访问结果集,这意味着它们不知道哪些记录受到了 DML 的影响。
您的两个业务规则都是表级规则,因为它们需要评估多个 EMP 记录。那么,我们可以通过触发器来强制执行它们吗?让我们从第二条规则开始:
每个部门不得少于两名员工
我们可以像这样使用触发器 AFTER 语句触发器来实现这一点:
CREATE or replace TRIGGER MIN_LIMIT
AFTER DELETE OR UPDATE on EMP
declare
EMPLOYEES pls_integer;
BEGIN
for i in ( select * from dept) loop
SELECT COUNT(EMPNO) INTO EMPLOYEES
FROM EMP
where i.DEPTNO = EMP.DEPTNO;
IF EMPLOYEES < 2 THEN
raise_application_error(-20042, 'problem with dept #' || i.DEPTNO || '. There cannot be less than two employees per department');
END IF;
end loop;
END;
/
请注意,此触发器使用 RAISE_APPLICATION_ERROR() 而不是 DBMS_OUTPUT.PUT_LINE()。引发实际异常始终是最好的方法:可以忽略消息,但必须处理异常。
这种方法的问题在于,它会使任何员工的任何更新或删除都失败,因为经典的 SCOTT.DEPT 表有一条记录 DEPTNO=40,它在 EMP 中没有子记录。那么,也许我们可以对员工为零的部门保持冷静,但对只有一名员工的部门就不行了?
CREATE or replace TRIGGER MIN_LIMIT
AFTER DELETE OR UPDATE on EMP
declare
EMPLOYEES pls_integer;
BEGIN
for i in ( select deptno, count(*) as emp_cnt
from emp
group by deptno having count(*) < 2
) loop
raise_application_error(-20042, 'problem with dept #' || i.DEPTNO || '. There cannot be less than two employees per department');
end loop;
END;
/
这将强制执行规则。当然,除非有人试图将一名员工插入部门 40:
insert into emp
values( 2323, 'APC', ‘DEVELOPER', 7839, sysdate, 4200, null, 40 )
/
我们可以提交这个。它会成功,因为我们的触发器不会在插入时触发。但是其他一些用户的更新随后会失败。这显然是线轴。所以我们需要在触发动作中包含 INSERT。
CREATE or replace TRIGGER MIN_LIMIT
AFTER INSERT or DELETE OR UPDATE on EMP
declare
EMPLOYEES pls_integer;
BEGIN
for i in ( select deptno, count(*) as emp_cnt
from emp
group by deptno having count(*) < 2
) loop
raise_application_error(-20042, 'problem with dept #' || i.DEPTNO || '. There cannot be less than two employees per department');
end loop;
END;
/
很遗憾,现在我们无法在部门 40 中插入一名员工:
ORA-20042:部门 #40 出现问题。每个部门不得少于两名员工
ORA-06512:在“APC.MIN_LIMIT”,第 10 行
ORA-06512:在“SYS.DBMS_SQL”,第 1721 行
我们需要在一个语句中插入两个员工:
insert into emp
select 2323, 'APC', 'DEVELOPER', 7839, sysdate, 4200, null, 40 from dual union all
select 2324, 'ANGEL', 'DEVELOPER', 7839, sysdate, 4200, null, 40 from dual
/
请注意,将现有员工切换到新部门也有同样的限制:我们必须在同一个语句中更新至少两名员工。
另一个问题是触发器可能性能不佳,因为我们必须在每条语句之后查询整个表。也许我们可以做得更好?是的。复合触发器(Oracle 11g 及更高版本)允许我们跟踪受影响的记录,以便在语句级别的 AFTER 触发器中使用。让我们看看如何使用一个来实现第一条规则
任何员工的薪水都不能高于老板的 80%
复合触发器非常简洁。它们允许我们跨触发器的所有事件共享程序构造。这意味着我们可以将来自行级事件的值存储在一个集合中,我们可以使用它在代码之后的语句级驱动一些 SQL..
所以这个触发器会触发三个事件。在处理 SQL 语句之前,我们初始化一个使用 EMP 表投影的集合。如果员工有经理,则行前的代码会存储当前行中的相关值。 (显然这条规则不适用于没有老板的金总统)。 after 代码遍历隐藏的值,查找相关经理的薪水,并根据老板的薪水评估员工的新薪水。
CREATE OR REPLACE TRIGGER MAX_SALARY
FOR INSERT OR UPDATE ON EMP
COMPOUND TRIGGER
type emp_array is table of emp%rowtype index by simple_integer;
emps_nt emp_array ;
v_idx simple_integer := 0;
BEFORE STATEMENT IS
BEGIN
emps_nt := new emp_array();
END BEFORE STATEMENT;
BEFORE EACH ROW IS
BEGIN
v_idx := v_idx + 1;
if :new.mgr is not null then
emps_nt(v_idx).empno := :new.empno;
emps_nt(v_idx).mgr := :new.mgr;
emps_nt(v_idx).sal := :new.sal;
end if;
END BEFORE EACH ROW;
AFTER EACH ROW IS
BEGIN
null;
END AFTER EACH ROW;
AFTER STATEMENT IS
mgr_sal emp.sal%type;
BEGIN
for i in emps_nt.first() .. emps_nt.last() loop
select sal into mgr_sal
from emp
where emp.empno = emps_nt(i).mgr;
if emps_nt(i).sal > (mgr_sal * 0.8) then
raise_application_error(-20024, 'salary of empno ' || emps_nt(i).empno || ' is too high!');
end if;
end loop;
END AFTER STATEMENT;
END;
/
此代码将检查每个员工的更新是否具有普遍性,例如当每个人都获得 20% 的加薪时...
update emp
set sal = sal * 1.2
/
但如果我们只更新 EMP 表的一个子集,它只会检查它需要的老板记录:
update emp set sal = sal * 1.2
where deptno = 20
/
这使它比之前的触发器更有效率。我们可以将触发器 MIN_LIMIT 重写为复合触发器;留给读者作为练习:)
同样,一旦发现单个违规行,每个触发器都会失败:
ORA-20024:empno 7902 的工资太高!
ORA-06512:在“APC.MAX_SALARY”,第 36 行
可以评估所有受影响的行,将违规行存储在另一个集合中,然后显示集合中的所有行。给读者的另一个练习。
最后,请注意,让两个触发器在同一个表上的同一个事件上触发并不是一个好习惯。拥有一个能完成所有工作的触发器通常会更好(更高效、更容易调试)。
事后考虑。如果一个会话增加了员工的薪水,而另一会话同时降低了老板的薪水,规则 #1 会发生什么?触发器将通过两个更新,但我们最终可能会违反规则。这是触发器与 Oracle 的读提交事务一致性一起工作的必然结果。没有办法避免它,除非采用悲观锁定策略并抢先锁定所有可能受更改影响的行。这可能无法扩展,并且使用纯 SQL 肯定很难实现:它需要存储过程。这是触发器不利于执行业务规则的另一个原因。
我用的是Oracle10g
这很不幸。 Oracle 10g 已经过时了将近十年。甚至不推荐使用 11g。但是,如果您真的别无选择,只能坚持使用 10g,那么您有两个选择。
首先是遍历整个表,为每个员工查找每个老板。这对于 EMP 等玩具桌来说几乎是可以忍受的,但在现实生活中可能会导致性能灾难。
更好的选择是使用我们曾经应用的相同解决方法来伪造复合触发器:编写一个包。我们依靠全局变量 - 集合 - 来维护对打包过程的调用之间的状态,并使用不同的触发器来进行这些调用。基本上,每个触发器都需要一个过程调用,复合触发器中的每个步骤都需要一个触发器。 @JustinCave 发布 an example of how to do this on another question;将我上面的代码翻译成他的模板应该很简单。