Выбрать главу

* получения допустимых значений по умолчанию в некоторых условиях;

* проверки и при необходимости преобразования входных данных пользователя;

* получения ключей и значений для выполнения автоматических обновлений в других таблицах;

* реализации автоинкрементных ключей средствами генераторов.

Новые значения для строки могут быть изменены только в действиях BEFORE. Если триггер, запускаемый в фазе AFTER, попытается присвоить значение столбцу NEW, это не даст никакого результата.

Все значения NEW можно перезаписывать в фазе BEFORE, они немедленно принимают новые назначенные им значения. Новая версия записи получит переназначенные значения, только когда все триггеры BEFORE будут завершены. С этого момента значения NEW становятся значениями только для чтения. Следовательно, если у вас несколько триггеров изменяют одни и те же значения NEW, важно, чтобы все они имели различные номера POSITION, правильно упорядоченные.

Реализация автоинкрементных ключей

Рекомендованное использование в Firebird триггеров BEFORE INSERT - реализация в стиле @IDENTITY автоинкрементных первичных ключей. Эта техника проста, и большинство разработчиков Firebird могут написать такие триггеры во сне. Она включает два шага:

1. Создание генератора для генерации уникальных чисел ключа.

2. Написание триггера BEFORE INSERT для таблицы.

Для иллюстрации этой техники мы реализуем автоинкрементный первичный ключ для таблицы CUSTOMER, у которой первичный ключ CUSTOMER_ID- столбец целого типа BIGINT (версия 1.5) или NUMERIC(18,0) (версия 1.0.x). В диалекте 1 базы данных CUSTOMER_ID должен иметь тип INTEGER.

Во-первых, создадим генератор:

CREATE GENERATOR GEN_PK_CUSTOMER;

Затем создадим триггер:

CREATE TRIGGER BI_CUSTOMER FOR CUSTOMER

ACTIVE BEFORE INSERT POSITION 0

AS

BEGIN

IF (NEW.CUSTOMER_ID IS NULL) THEN

NEW. CUSTOMER_ID = GEN_ID (GEN_PK_CUSTOMER, 1) ;

END ^

COMMIT ^

Когда выполняется добавление, CUSTOMER ID сознательно не указывается во входном списке оператора INSERT:

INSERT INTO CUSTOMER (

LAST_NAME,

FIRST_NAME,

...)

VALUES (?, ?, ...);

Без триггера этот оператор вызовет исключение, потому что первичный ключ не может иметь пустого значения. Однако триггер BEFORE INSERT выполняется до проверки этого ограничения, он контролирует, что CUSTOMER ID имеет пустое значение, и выполняет свое действие.

Зачем проверять NEW.значение на NULL

Если триггер может делать это для меня, то вы можете спросить, зачем нужно выполнять проверку на NULL?

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

Приложения, написанные для Firebird, имеют преимущества, благодаря одной особой характеристике генераторов: они не зависят от пользовательских транзакций. Однажды сгенерированное значение не может быть выдано ни одной другой транзакции и не может быть отменено.

Быстрый запрос в его собственной транзакции[122] возвращает значение

SELECT GEN_ID (GEN_PK_CUSTOMER, 1) AS RESULT

FROM RDB$DATABASE;

Если в вашем триггере опущена проверка на пустое значение и просто выполняется:

. . .

AS

NEW.CUSTOMER_ID = GEN_ID (GEN_PK_CUSTOMER, 1);

END ^

то значение, полученное приложением, будет перекрыто вторым "дерганьем" генератора, что нарушит связь с подчиненными записями.

Эта ситуация не является аргументом в пользу генерации ключей только в триггерах. Наоборот, триггер с проверкой на NULL обеспечивает реализацию бизнес-правил при любых условиях.

! ! !

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

. ! .

Преобразования

Переменная NEW может быть использована для преобразования значения в нечто другое. Общий трюк заключается в использовании триггера (или пары триггеров в версии 1.0.А-) для поддержания "заменителя" столбца для выполнения нечувствительных к регистру поисков по другому столбцу, который может содержать смешанные значения регистра. Триггер читает значение NEW столбца со смешанным регистром, конвертирует его в верхний регистр и записывает в значение NEW столбца поиска. Такой столбец-"заменитель" должен иметь ограничение NOT NULL для гарантии того, что в нем всегда будет значение для поиска:

CREATE TABLE MEMBER (

MEMBER_ID INTEGER NOT NULL PRIMARY KEY,

LAST_NAME VARCHAR (40) NOT NULL,

FIRST_NAME VARCHAR (35),

PROXY_LAST_NAME VARCHAR (40),

MEMBER_TYPE CHAR(3) NOT NULL,

MEMBERSHIP_NUM VARCHAR(13) ,

. . . . );

COMMIT;

/* */

SET TERM ^;

CREATE TRIGGER BA_MEMBER1 FOR MEMBER

ACTIVE BEFORE INSERT OR UPDATE

POSITION 0

AS

BEGIN

. . .

NEW. PROXY_LAST_NAME = UPPER(NEW.LAST_HAME) ;

. . .

END ^

Возможны любые виды преобразований. Предположим, мы хотим получить количество элементов (MEMBERSHIP_NUM), собранное из MEMBER_TYPE, за которым следует строка из десяти цифр, заполненная слева нулями и полученная из сгенерированного первичного ключа таблицы MEMBER. С помощью автоматической генерации в триггере BEFORE INSERT мы можем это осуществить[123]:

CREATE TRIGGER BI_MEMBER2 FOR MEMBER

ACTIVE BEFORE

INSERT POSITION 2

AS

DECLARE VARIABLE ID AS STRING VARCHAR (10);

BEGIN

ID_AS_STRING = CAST (NEW. ID AS VARCHAR (10)) ;

WHILE (NOT (ID_AS_STRING LIKE ' %'))

/* 10-символьная маска */

DO

ID_AS_STRING = '0' || ID_AS_STRING;

NEW.MEMBERSHIP_NUM = NEW.MEMBER_TYPE || ID_AS_STRING;

END ^

Проверка и значения по умолчанию

Триггеры могут улучшить стандартные ограничения SQL, когда они используются для проверки входных данных и применения значений по умолчанию.

Проверка

SQL предоставляет ограничения CHECK для гарантии того, что будут сохраняться только "хорошие" данные. Например, значения столбцов, создаваемых на основе следующего домена, ограничены символами в верхнем регистре и цифрами:

CREATE DOMAIN TYPECODE CHAR(3)

CHECK(VALUE IS NULL OR VALUE = UPPER(VALUE));

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

CREATE TRIGGER BA_ATABLE FOR ATABLE

ACTIVE BEFORE INSERT OR UPDATE

AS

BEGIN

NEW.ATYPECODE = UPPER(NEW.ATYPECODE);

END ^

! ! !

ПРИМЕЧАНИЕ. В настоящий момент Firebird поддерживает триггеры только для таблиц и просмотров. Невозможно создать триггер для домена, однако это было бы элегантным улучшением.

. ! .

Значения по умолчанию

В определениях доменов и столбцов вы можете указать значение по умолчанию: DEFAULT. Кажется хорошей идеей устанавливать значение столбца, не допускающего значение NULL В некоторое значение по умолчанию, однако SQL-атрибут DEFAULT на деле оказывается беззубым зверем. Он работает только при двух условиях:

вернуться

122

Некоторые компоненты интерфейса предоставляют методы или свойства для автоматического выполнения этой маленькой задачи.

вернуться

123

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