【问题标题】:How to write triggers to enforce business rules?如何编写触发器来执行业务规则?
【发布时间】:2019-10-12 05:54:07
【问题描述】:

我想创建用于练习 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;

我真的不知道我是否真的离它越来越近了……

【问题讨论】:

  • 。 .您的第二个触发器将使添加新部门变得非常困难。
  • 在您第一次触发时,我们无法调试我们看不到的代码,并且您没有发布整个代码。而且由于您没有费心发布错误消息或“成功但不正确”行为的描述,我们不知道我们应该寻找什么。在您的第二个触发器中,正如@GordonLinoff 暗示的那样,您希望如何将 first 员工添加到新部门?想想“IF EMPLOYEES
  • 在第一个触发器中,您没有显示整个触发器,让我想知道问题可能是什么?此外,P.BOSSP.SALARY 不是有效的标识符名称 - 请尝试 strBOSSnSALARY。第二,你没有指定触发器应该在哪个表上,如果一个部门的员工超过 99 人,你的触发器就会失败。
  • 关于第一个代码的问题是我不知道如何继续,我无法发布整个代码,因为我没有任何其他内容......
  • “我想创建用于练习 PL/SQL 的触发器” - 触发器是一个不好的工具,因为它们的功能非常有限。他们不能在拥有触发器的表上执行 DML(包括选择);尝试这样做会导致ORA-04088 mutating table exception,因为 Oracle 强制执行事务完整性。你的触发器都在 EMP 和查询 EMP 上,所以会犯这个错误。如果你想学习 PL/SQL 我建议你看看the Dev Gym

标签: oracle plsql oracle10g database-trigger


【解决方案1】:

我相信它们很简单

其实这些任务对于触发器来说并不简单。业务逻辑很简单,执行业务逻辑的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;将我上面的代码翻译成他的模板应该很简单。

【讨论】:

  • 非常感谢!!!我什至无法想象如此完整和详细的答案!非常感谢你。问题是,我正在使用 Oracle10g 并且复合触发器在那里不起作用......不过,你帮了我很多;再次谢谢你:)
  • 我已经编辑了我的答案来讨论 10g。如果您发现此答案有帮助,请考虑投票和/或接受它。接受的答案提高了 StackOverflow 作为未来寻求者资源的价值。
【解决方案2】:

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

如果您在应用程序或过程级别处理业务逻辑,那么 DB 服务器将只需要执行 DML 语句;它不必执行TRIGGER——执行触发器涉及处理异常;在该 DML 语句之前,将对正在执行 DML(INSERT 语句-独占共享锁除外)的表进行锁定,直到执行 TRIGGER。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2018-01-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-07-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多