Книга: Firebird РУКОВОДСТВО РАЗРАБОТЧИКА БАЗ ДАННЫХ
Переменные NEW и OLD
Разделы на этой странице:
Переменные NEW и OLD
Контекстные переменные NEW и OLD являются расширениями PSQL, специфичными для триггеров[120]; они позволяют ссылаться на существующие ("старые") и требуемые ("новые") значения каждого столбца. Переменные NEW.* имеют значения в событиях INSERT и UPDATE; переменные OLD.* имеют значения в событиях UPDATE и DELETE, NEW.* в событиях удаления и OLD.* В событиях добавления имеют значение NULL. Применимые значения NEW и OLD доступны для всех столбцов таблицы или просмотра, даже если сами столбцы не указаны в операторе DML.
Значения OLD.* (если доступны) могут использоваться в триггерах как переменные, но изменение значения не влияет на сохраненное старое значение[121]. Значения NEW.* (если доступны) могут использоваться для чтения/записи в фазе BEFORE и только для чтения в фазе AFTER. Если вы хотите манипулировать ими как переменными значениями в триггере AFTER, присвойте их значения локальным переменным и обращайтесь к этим локальным переменным.
Использование NEW и OLD
Для использования мощи триггеров Firebird в разработке баз данных при отслеживании целостности данных, независимо от людей и внешних программ, переменные NEW и OLD являются основным инструментом. Они могут быть использованы для:
* получения допустимых значений по умолчанию в некоторых условиях;
* проверки и при необходимости преобразования входных данных пользователя;
* получения ключей и значений для выполнения автоматических обновлений в других таблицах;
* реализации автоинкрементных ключей средствами генераторов.
Новые значения для строки могут быть изменены только в действиях 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 на деле оказывается беззубым зверем. Он работает только при двух условиях:
* при использовании операции INSERT;
* если этот столбец не включен во входной список оператора.
Поскольку многие современные интерфейсы приложений автоматически создают оператор INSERT, используя выходные столбцы оператора SELECT В качестве входного
списка, то все столбцы помещаются в этот список. Если само приложение не предоставляет значение по умолчанию, то обычным результатом является передача значения NULL. Когда сервер получает значение NULL для такого столбца, он сохраняет в базе данных NULL. Другие ограничения столбца могут вклиниться сюда и вызвать исключение - особенно ограничение NOT NULL - однако значение по умолчанию для столбца никогда не перекроет и не скорректирует любое значение, полученное от клиентского интерфейса.
Вторая проблема связана, конечно, с тем, что значения по умолчанию никогда не применяются, если используется операция изменения.
Короче говоря, триггеры выполняют гораздо более эффективную работу по поддержанию значений по умолчанию, чем это делают атрибуты значения по умолчанию для столбца. Возьмем для примера столбец, основанный на следующем домене:
CREATE DOMAIN MONEY NUMERIC (18, 0)
NOT NULL DEFAULT 0.00;
Триггер BEFORE INSERT OR UPDATE для любого столбца, использующего домен MONEY, реализует значение по умолчанию:
CREATE TRIGGER BI_ACCOUNT FOR ACCOUNT
ACTIVE BEFORE INSERT OR UPDATE
AS
BEGIN
IF (NEW.BALANCE IS NULL) THEN
NEW.BALANCE = 0.00;
END ^
! ! !
СОВЕТ. Вы можете обеспечить поддержание всех значений по умолчанию для таблицы в едином триггерном модуле (версия 1.5 и выше) или в двух параллельных модулях: один для BEFORE INSERT, а другой для BEFORE UPDATE (версия 1.0.x).
. ! .
Автоматическое заполнение
Триггеры полезны для "автоматического заполнения" контекстной информацией столбцов, созданных для подобных целей. Firebird предоставляет множество контекстных переменных, которые вы можете использовать в операциях такого рода. Вы можете также использовать ваши собственные "флаги", которые вы вычисляете или просто поставляете в виде констант в процессе выполнения триггера.
В следующем примере мы используем триггер AFTER для множества событий с целью автоматического заполнения имени пользователя, даты, времени и идентификатора транзакции для помещения в файл протокола вместе с некоторой информацией о событии. Поскольку регистрируется процесс, то, скорее всего, мы захотим выполнить это в самом конце; присвоим триггеру высокий последовательный номер:
CREATE TRIGGER AA_MEMBER FOR MEMBER
ACTIVE AFTER INSERT OR UPDATE OR DELETE
POSITION 99
AS
DECLARE VARIABLE MEM_ID INTEGER;
DECLARE VARIABLE DML_EVENT CHAR(4);
BEGIN
IF (DELETING) THEN
BEGIN
MEM_ID = OLD.MEMBER_ID;
DML_EVENT = 'DEL ';
END
ELSE
BEGIN
MEM_ID = NEW.MEMBER_ID;
IF (UPDATING) THEN
DML_EVENT = 'EDIT';
ELSE
DML_EVENT = 'NEW ';
END
INSERT INTO PROCESS_LOG (
TRANS_ID,
USER_ID,
MEMBER_ID,
DML_EVENT,
TIME_STAMP)
VALUES (
CURRENT_TRANSACTION,
CURRENT_USER,
:MEM_ID,
:DML_EVENT,
CURRENT_TIMESTAMP) ;
END ^
Конечно, вы также можете заполнять ваши новые или редактируемые строки непосредственно в триггере BEFORE.
- Oldest transaction
- Oldest active и Oldest snapshot
- Системные переменные ROWS_AFFECTED, GDSCODE, SQLCODE, TRANSACTIONJD, CONNECTIONJD
- 1.2.3. Константы, переменные и типы
- State NEW packets but no SYN bit set
- 2. How to Apply These Terms to Your New Programs
- Операция new
- 22.3.3. Переменные окружения
- 8.3 Этап 3: переменные с произвольными именами; встроенные функции
- Контекстные переменные даты и времени
- Usenet Network Newsgroups
- The Pan News Client Newsreader