Вложенные и рекурсивные вызовы триггеров
Если триггер обновляет таблицу, это обновление может вызвать выполнение другого триггера или даже того же самого. По умолчанию SQL Server допускает до 32 вложенных не рекурсивных вызовов триггеров, а рекурсивные вызовы запрещает. Мы же рекомендуем разрешить рекурсивное выполнение триггеров и пользоваться им по мере необходимости. Опция, разрешающая рекурсивное выполнение триггеров, устанавливается с помощью инструкции ALTER DATABASE или системной хранимой процедуры sp_dboption:
ALTER DATABASE AppDta
SET RECURSIVE_TRIGGERS ON
или
sp_dboption AppDta ‘recursive triggers’, ‘true’
Хотя мы не рекомендуем так поступать, но учтите, что с помощью следующей инструкции можно запретить все вложенные вызовы триггеров во всех базах данных:
sp_configure ‘nested triggers’, 0
Вывод информации о триггерах
Информацию о триггерах возвращают четыре системные хранимые процедуры:
ü sphelp имя-триггера — возвращает имя владельца триггера, а также дату и время его создания;
ü sphelptext имя-триггера — возвращает исходный код триггера (если он не зашифрован);
ü sp_depends имя-триггера — возвращает список объектов, на которые в триггере имеются ссылки;
ü sphelptrigger имя-таблицы — возвращает список триггеров, определенных для заданной таблицы.
Процедура sphelptext выручит вас, если вы потеряли исходный код одного из триггеров.
Программирование триггеров
В основном программирование триггеров ничем не отличается от программирования хранимых процедур, однако некоторые ограничения все же имеются. Прежде всего, в триггерах нельзя использовать следующие инструкции:
ALTER DATABASE CREATE DATABASE DISK INIT
DISK RESIZE DROP DATABASE LOAD DATABASE
LOAD LOG RECONFIGURE RESTORE DATABASE
RESTORE LOG
Кроме того, триггер не может возвращать вызывающей программе результирующие наборы строк, т.е. в нем не должны выполняться самостоятельные инструкции SELECT.
Скалярные вложенные запросы, значения которых присваиваются переменным триггера, можно выполнять совершенно свободно, поскольку они не создают результирующих наборов строк. В начале триггера желательно выполнять инструкцию SET NOCOUNT ON, которая отключает отправку сообщений, указывающих, сколько строк обработано каждой SQL-инструкцией.
Примечание. Если в триггере (или в обычной хранимой процедуре) используется инструкция SET, после завершения работы триггера устанавливаемые ею опции возвращаются в исходное состояние.
Одно из назначений триггеров состоит в том, чтобы дополнить стандартные средства обеспечения реляционной целостности (т.е. первичные ключи, внешние ключи, ограничения уникальности и ограничения на значения) пользовательскими средствами, учитывающими специфику конкретной базы данных. После проверки определенных условий в триггере можно отменить всю транзакцию, выполнив инструкцию ROLLBACK. Эту возможность демонстрирует следующий триггер, отменяющий назначение скидки клиенту, когда эту операцию попытался выполнить пользователь, не имеющий на это права:
CREATE TRIGGER CheckDiscountUpdates
ON AppDta.dbo.Customer FOR INSERT, UPDATE
AS
-- Проверяем, задан ли в инструкции столбец Discount
IF UPDATE(Discount) BEGIN
DECLARE @User VARCHAR(256) SET NOCOUNT ON
-- Проверяем, действительно ли задано значение столбца
-- Discount
IF (EXISTS (SELECT * FROM INSRETED WHERE Discount > 0))
BEGIN
-- Убеждаемся, что пользователь имеет право назначать
-- скидки
IF (NOT EXISTS (SELECT * FROM Employee
WHERE UserName = CURRENT_USER
AND DiscountAuthority = ‘Y’))
BEGIN
-- Отправляем сообщение и отменяем транзакцию
SET @User = CURRENT_USER
RAISERROR(‘User %s not approved to assign dis-
counts.’, 16, 1, @User )
ROLLBACK
END
END
END
Триггер вызывается один раз после выполнения инструкции, с которой он связан, и только в том случае, если эта инструкция не нарушает ограничений первичного и внешнего ключей, уникальности и целостности. К моменту вызова триггера результаты выполнения инструкции уже отражены в таблице, с которой он связан, как и во временных таблицах insreted и deleted, но соответствующая транзакция еще не сохранена, и все эти изменения можно отменить.
Поскольку инструкция должна удовлетворять всем ограничениям таблицы еще до вызова триггера, триггеры нельзя использовать для обеспечения выполнения этих ограничений. Предположим, например, что для таблицы Sale определено ограничение внешнего ключа без каскадного удаления. Это ограничение препятствует удалению строки таблицы Customer, если с ней связана хоть одна строка таблицы Sale. Возможно, вы захотите создать DELEТЕ-триггер для таблицы Customer, удаляющий из таблицы Sale все заказы удаляемого клиента, сделанные в прошлом году, и тем самым разрешающий удаление строки таблицы Customer, если больше заказов не осталось. Но это не так просто. Поскольку, как уже говорилось, ограничение внешнего ключа проверяется еще до вызова триггера, SQL Server 2000 просто отменит операцию удаления клиента, имеющего хоть один заказ. В итоге ваш триггер даже не будет вызван. Впрочем, у этой проблемы все же имеется решение. Таким решение является использование замещающих триггеров, о которых рассказывается в следующем разделе.
Триггер может выполнять различные операции с таблицей, с которой он связан, в частности, выполнять дополнительную обработку добавленных или обновленных строк. Но если вы решите воспользоваться этой возможностью, подумайте, что делать с рекурсивными вызовами триггера.
Замещающие триггеры
До сих пор мы говорили об обычных триггерах (after-триггерах), т.е. триггерах, выполняемых после операции, с которой они связаны, и после проверки всех относящихся к этой операции ограничений. Триггеры другого типа, называемые замещающими триггерами (instead of triggers), выполняются вместо операции, с которой они связаны. Замещающие триггеры могут выполнять множество действий, причем не обязательно связанных с исходной операцией (например, добавлением строк в таблицу). И если обычные триггеры определяются исключительно для базовых таблиц (поскольку именно там модифицированы данные, с которыми они работают), то замещающие триггеры можно определять и для представлений, причем даже для тех представлений, которые без триггеров были бы доступны только для чтения.
Для создания замещающего триггера ключевое слово for (или after) в инструкции create trigger нужно заменить ключевым словом instead of. Ниже приведен пример замещающего триггера, который решает описанную в предыдущем разделе проблему: автоматически удаляет зависимые строки таблицы Sale при удалении родительской строки таблицы Customer:
CREATE TRIGGER CustomerDelete ON Customer INSTEAD OF DELETE
AS SET NOCOUNT ON
-- Проверяем, имеются ли в таблице Sale прошлогодние заказы
-- удаляемых клиентов
IF (EXISTS (SELECT * FROM DELETED JOIN Sale
ON DELETED.Custld = Sale.Custld
WHERE DATEDIFF (DAY, SaleDate,
CURRENT__TIMESTAMP) < 365)) BEGIN
RAISERROR(‘One or more customers have recent sales’, 16, 1)
RETURN
END
-- Удаляем соответствующие строки таблицы Sale
-- (все они устарели)
DELETE FROM Sale WHERE Custld IN (SELECT Custld FROM DELETED)
IF (@@ERROR > 0) BEGIN
ROLLBACK
RAISERROR (‘Could not delete all old sales’, 16, 2) RETURN
END
-- Удаляем клиентов
DELETE FROM Customer WHERE Custld IN
(SELECT Custld FROM DELETED )
-- В случае неудачи отменяем все изменения
IF (@@ERROR > 0) BEGIN
ROLLBACK
RAISERROR(‘Could not delete all old customers’, 16, 3)
RETURN
END
RETURN
Обратите внимание, что в этом примере используется ключевое слово DELETED. Это уже знакомая вам ссылка на временную таблицу, создаваемую SQL Server специально для выполняемого триггера. Только в отличие от обычного триггера эта таблица содержит не удаленные строки, а строки, которые были бы удалены, если бы SQL Server выполнил исходную инструкцию.
На самом же деле к моменту вызова замещающего триггера в связанную с ним таблицу не внесено никаких изменений. Как показывает приведенный пример, для выборки тех строк, которые были бы обработаны исходной инструкцией, можно использовать первичные ключи строк таблицы DELETED (или INSERTED).
В отличие от обычных триггеров с каждым типом инструкций, которые могут быть выполнены для таблицы, можно связать только один замещающий триггер. Однако наряду с замещающим триггером для этой инструкции может быть определено и несколько обычных триггеров. Если замещающий триггер выполняет ту же SQL-инструкцию, для которой он был вызван, рекурсивного вызова этого триггера не происходит. Вместо этого просто выполняется соответствующая операция, и после нее вызываются обычные триггеры (если таковые имеются).
Задание к работе
Создать триггеры с помощью Enterprise Manager, Query Analyzer и MS Access для ведения учета изменения данных в таблицах.