如何编写触发器以执行业务规则?

时间:2019-05-26 10:43:53

标签: oracle plsql oracle10g database-trigger

我想创建用于练习PL / SQL的触发器,我被这两个触发器卡住了,我确定它们很简单,但是我无法掌握这段代码。

第一个触发器禁止员工的薪水高于其老板的80%(代码不完整,因为我不知道如何继续):

CREATE OR REPLACE TRIGGER MAX_SALARY
BEFORE INSERT ON EMP
FOR EACH ROW
P.BOSS EMP.JOB%TYPE := 'BOSS'
P.SALARY EMP.SAL%TYPE
BEGIN
SELECT SAL FROM EMP
WHERE  
 JOB != P.BOSS
...

第二个,每个部门不得少于两名员工

CREATE TRIGGER MIN_LIMIT
AFTER DELETE OR UPDATE EMPNO
EMPLOYEES NUMBER(2,0);
BEGIN
SELECT COUNT(EMPNO)INTO EMPLOYEES FROM EMP
WHERE DEPTNO = DEPT.DEPTNO;
IF EMPLOYEES < 2 THEN
DBMS_OUTPUT.PUT_LINE('There cannot be less than two employees per department');
END IF;
END;

我真的不知道我是否真的要离它越来越近...

2 个答案:

答案 0 :(得分:0)

  

我确定它们很简单

实际上,这些任务对于触发器而言并不简单。业务逻辑很简单,执行业务逻辑的SQL很简单,但是很难在触发器中实现它。要了解为什么需要了解触发器如何工作。

触发器作为事务的一部分触发,这意味着它们将应用于SQL语句的结果,例如插入或更新。触发器有两种类型,行级触发器和语句级触发器。

行级触发器为结果集中的每一行触发一次,我们可以引用当前行中的值,这对于评估行级规则很有用。但是我们无法对拥有的表执行DML:Oracle催促ORA- 04088变异表例外,因为此类动作违反了交易的完整性。

语句级别每个语句仅触发一次触发。因此,它们对于执行表级规则很有用,但至关重要的是,它们无法访问结果集,这意味着它们不知道哪些记录已受到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%

复合触发器非常整洁。它们使我们能够在触发器的所有事件之间共享程序构造。这意味着我们可以将行级事件的值存储在一个集合中,该值可以用于在语句级AFTER代码中驱动某些SQL。

因此,此触发器在三个事件上触发。在处理SQL语句之前,我们初始化一个使用EMP表的投影的集合。如果员工有经理,则行前的代码会隐藏当前行中的相关值。 (显然,该规则不适用于没有老板的金总统)。之后的代码循环遍历隐藏的值,查找相关经理的薪水,并根据其老板的薪水评估员工的新薪水。

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行

可以评估所有受影响的行,将违反的行存储在另一个集合中,然后显示该集合中的所有行。读者的另一项练习。

最后,请注意,在同一张桌子上的同一事件上触发两个触发器不是一个好习惯。通常,拥有一个可以完成所有任务的触发器会更好(更高效,更容易调试)。


  

我正在使用Oracle10g

那是不幸的。 Oracle 10g已经过时了将近十年。不建议使用11g。但是,如果您真的别无选择,只能坚持使用10克,那么您有两种选择。

第一个方法是遍历整个表,为每个员工进行每个老板的查找。这对于EMP之类的玩具桌来说是可以忍受的,但在现实生活中可能会造成性能损失。

更好的选择是使用我们都曾经使用过的相同解决方法来伪造复合触发器:编写一个程序包。我们依靠全局变量(集合)来维护对打包过程的调用之间的状态,并具有进行这些调用的不同触发器。基本上,每个触发器需要一个过程调用,复合触发器中的每个步骤都需要一个触发器。 @JustinCave发布了an example of how to do this on another question;将上面的代码翻译成他的模板应该很简单。

答案 1 :(得分:0)

请在应用程序或数据库级别使用过程/函数而不是使用触发器来处理这类验证/业务逻辑,而在大多数情况下,触发器会减慢触发器所基于的DML操作/语句的速度。

如果您在应用程序或过程级别上处理业务逻辑,则DB服务器将仅需执行DML语句;它不必执行TRIGGER,执行触发器涉及处理异常;在执行DRIGGER之前,该DML语句之前的语句将在执行DML的表上放置一个锁(INSERT语句-Exclusive共享锁除外)。