Как написать триггеры для обеспечения соблюдения бизнес-правил? - PullRequest
1 голос
/ 26 мая 2019

Я хочу создать триггеры для отработки 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 голосов
/ 03 июня 2019

Пожалуйста, обрабатывайте такого рода проверки / бизнес-логику в приложении или на уровне БД, используя процедуры / функции вместо использования триггеров, которые в большинстве случаев замедляют операции / операторы DML, на которых основаны триггеры.

Если вы обрабатываете бизнес-логику на уровне приложения или процедуры, сервер БД должен будет выполнять только операторы DML;он не должен выполнять триггер, выполняющий TRIGGER, включает обработку исключений;до этого оператор DML будет блокировать таблицу, в которой выполняется DML (за исключением общей блокировки INSERT, исключающей оператор), до тех пор, пока не будет выполнен TRIGGER.

0 голосов
/ 27 мая 2019

что я уверен, что они простые

На самом деле эти задачи не являются простыми для триггеров. Бизнес-логика проста, и 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% своего босса

Составные триггеры очень аккуратны. Они позволяют нам совместно использовать программные конструкции для всех событий триггера. Это означает, что мы можем хранить значения из событий уровня строки в коллекции, которую мы можем использовать для передачи некоторого 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

Можно было бы оценить все затронутые строки, спрятать нарушающие строки в другой коллекции, а затем отобразить все строки в коллекции. Еще одно упражнение для читателя.

Наконец, обратите внимание, что использование двух триггеров для одного и того же события на одном столе не является хорошей практикой. Как правило, лучше (эффективнее, легче отлаживать) иметь один триггер, который делает все.


Послышание. Что происходит с правилом № 1, если одна сессия увеличивает зарплату сотрудника, в то время как другая сессия уменьшает зарплату босса? Триггер пропустит оба обновления, но мы можем нарушить правило. Это неизбежное следствие того, как триггеры работают с согласованностью транзакций чтения и фиксации Oracle. Нет способа избежать этого, кроме как с помощью пессимистичной стратегии блокировки и упреждающей блокировки всех строк, на которые может повлиять изменение. Это может не масштабироваться и определенно сложно реализовать с использованием чистого SQL: для этого нужны хранимые процедуры. Это еще одна причина, по которой триггеры не подходят для применения бизнес-правил.


Я использую Oracle10g

Это прискорбно. Oracle 10g устарел уже почти десять лет. Даже 11 г не рекомендуется. Однако, если у вас действительно нет выбора, кроме как придерживаться 10g, у вас есть несколько вариантов.

Во-первых, нужно перебрать всю таблицу, выполняя поиск каждого босса для каждого сотрудника. Это почти терпимо для игрушечного стола, такого как EMP, но, вероятно, будет настоящей катастрофой для производительности в реальной жизни.

Лучший вариант - подделать составные триггеры, используя тот же обходной путь, который мы все применяли: написать пакет. Мы полагаемся на глобальные переменные - коллекции - для поддержания состояния при обращении к пакетным процедурам, и у нас есть разные триггеры для выполнения этих вызовов. В основном вам нужен один вызов процедуры для каждого триггера и один триггер для каждого шага в составном триггере. @JustinCave разместил пример того, как это сделать по другому вопросу ; должно быть просто перевести мой код выше в его шаблон.

...