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