Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Базы данных. Практический курс. В 2 частях. Ч.1. Объектно-реляционные базы данных на примере PostgreSQL 9.5. Учебное пособие.pdf
Скачиваний:
0
Добавлен:
06.09.2026
Размер:
2 Мб
Скачать
Глава 4. Применение SQL 4.4. Триггеры
пользователю об ошибке, разработчик должен воспользоваться вы­ходным параметром или сгенерировать исключение.
Оставшаяся часть процедуры в листинге 4.1 вставляет данные о новом клиенте и затем перебирает всех художников выбранной национальности. Обратите внимание на использование специаль­ной конструкции PL/pgSQL FOR artist IN artistcursor LOOP. Эта конструкция выполняет несколько задач. Прежде всего, она откры­вает курсор и считывает первую строку. Затем она последовательно обрабатывает все строки под курсором и по окончании обработки передает управление следующему после FOR оператору. Заметьте также, что обращение к столбцу ArtistID текущей строки происхо­дит с использованием синтаксиса artist.ArtistID, где artist – это имя переменной цикла FOR, а не курсора.
После того как процедура написана, ее необходимо скомпили­ровать и сохранить в базе данных. Если текст процедуры сохранен в файле, то процедура будет скомпилирована и сохранена в базе данных автоматически после ввода команды psql
\i путь< до sql файла с текстом процедуры>
Если вы что-то ввели неправильно, у вас могут возникнуть ошибки компиляции. Если синтаксических ошибок не было, вы получите сообщение «CREATE FUNCTION» (Процедура создана). Теперь вы можете вызвать эту процедуру с помощью запроса.
Например:
SELECT customer_insert(
’Michael Bench’, ’203’, ’555-2014’, ’US’);

4.4 Триггеры

Триггер (trigger) – это специальная программа, назначаемая таблице или представлению. Триггер вызывается СУБД, когда поль­зователь запрашивает вставку, обновление или удаление строки из таблицы или представления, которому принадлежит данный триг­гер. С каждым триггером связана собственная триггерная функция.
PostgreSQL поддерживает три вида триггеров: предваряющие (BEFORE), замещающие (INSTEAD OF) и завершающие (AFTER). Как и логично было бы ожидать, предваряющие триггеры вызываются
81
4.4. Триггеры Глава 4. Применение SQL
перед обработкой запроса на вставку, обновление, удаление или опустошение, замещающие – вместо него, а завершающие – после обработки запроса. Всего имеется двенадцать возможных типов триггеров: предваряющий триггер вставки, обновления, удаления и опустошения, замещающий триггер вставки, обновления, удале­ния и опустошения и завершающий триггер вставки, обновления, удаления и опустошения.
При запуске триггера СУБД предоставляет доступ к вставляе­мым, обновляемым или удаляемым из тела триггера данным. В слу­чае вставки триггеру доступны значения столбцов новой строки, в случае удаления – значения столбцов удаляемой строки, а в случае обновления триггер может оперировать как новыми, так и старыми значениями.
В PostgreSQL для получения нового состояния строки необхо­димо использовать переменную NEW. Так, например, при вставке в таблицу CUSTOMER переменная NEW.Name содержит значение столбца
Name вставляемой строки. В случае обновления переменная NEW.Name
содержит значение, которое будет иметь столбец Name после выпол­нения запроса. Аналогичным образом, для получения старых зна­чений необходимо использовать переменную OLD. Например, при удалении переменная OLD.Name содержит значение столбца Name удаляемой строки. В случае обновления эта переменная содержит значение столбца Name до выполнения запроса.
Триггеры имеют множество применений. Основные из них: проверка допустимости вводимых данных, присваивание значений по умолчанию, обновление представлений и обеспечение ссылоч­ной целостности.

4.4.1 Использование триггеров для проверки допустимости вводимых данных

Предположим, у галереи (см. описание практического при­мера в гл. 2) есть правило, что ни одна работа не может быть продана менее чем за 90% от запрошенной цены. Чтобы обеспечить выполнение этого правила, можно написать триггер обновления для таблицы TRANSACTIO N, сравнивающий значения AskingPrice и
SalesPrice. Если правило нарушается, в столбец SalesPrice ставит-
82
Глава 4. Применение SQL 4.4. Триггеры
ся значение столбца AskingPrice, а в столбец AskingPrice исходное значение.
Можно использовать две стратегии. Одна заключается в том, чтобы написать предваряющий триггер, который проверяет и пе­реустанавливает, если необходимо, значение столбца SalesPrice до выполнения обновления. Вторая стратегия – написать заверша­ющий триггер, проверяющий и переписывающий строку таблицы
TRANSACTION после обновления.
Листинг 4.2 соответствует второй стратегии. Если есть необхо­димость в комментариях, то они помещаются в скобки вида ’/* ... */’ либо перед текстом комментария ставится символ ’--’.
Листинг 4.2.
CREATE OR REPLACE FUNCTION sales_price_check()
RETURNS trigger AS $sales_price_check$
BEGIN
IF NEW.SalesPrice < 0.9 * OLD.AskingPrice THEN
NEW.SalesPrice = OLD.AskingPrice; NEW.AskingPrice = OLD.AskingPrice;
END IF;
RETURN NEW; END; $sales_price_check$ LANGUAGE plpgsql;
CREATE TRIGGER sales_price_check
BEFORE UPDATE ON TRANSACTION
FOR EACH ROW EXECUTE PROCEDURE sales_price_check();
Логика работы триггера очевидна. Если новая продажная це­на составляет менее 90% от запрашиваемой цены, продажная цена устанавливается равной запрашиваемой цене. Обратите внимание, что новая продажная цена сравнивается со старой запрашиваемой ценой; в противном случае можно было бы, изменив обе цены, успешно совершить обновление, нарушающее данное ограничение. На тот случай, если именно так и произошло, столбец AskingPrice в операторе UPDATE устанавливается равным OLD.AskingPrice.
83
4.4. Триггеры Глава 4. Применение SQL
4.4.2 Использование триггеров для присвоения значений по умолчанию
Столбцам таблицы могут присваиваться значения по умолча­нию с помощью квалификатора DEFAULT. В качестве таких значений можно задавать константы или результаты вычисления простых вы­ражений. Если же задание значения по умолчанию требует более сложной логики, необходимо использовать триггер.
В случае нашего примера предположим, что у галереи имеет­ся правило, согласно которому запрашиваемая цена произведения устанавливается равной удвоенной стоимости его приобретения или сумме общей стоимости приобретения и чистой выручки от продажи этого произведения в прошлом. Это правило реализуется с помощью завершающего триггера, показанного в листинге 4.3. Представление, которое используется в рассматриваемом тригге­ре, имеет следующий вид:
CREATE VIEW ArtistWorkNet AS SELECT W.WorkID, Name, Title, Copy, AcquisitionPrice, SalesPrice, (SalesPrice - AcquisitionPrice) AS NetPrice FROM TRANSACTION T JOIN WORK W
ON T.WorkID = W.WorkID
JOIN ARTIST A
ON W.ArtistID = A.ArtistID;
Листинг 4.3.
CREATE OR REPLACE FUNCTION set_asking_price()
RETURNS trigger AS $set_asking_price$
DECLARE
avgNetPrice numeric(8,2); newPrice numeric(8,2); rowcount integer;
BEGIN
SELECT Count(*) INTO rowcount FROM TRANSACTION WHERE WorkID = NEW.WorkID;
IF rowcount = 0 THEN
NEW.AskingPrice = 2 * (NEW.AcquisitionPrice);
ELSE
SELECT AVG(NetPrice) INTO avgNetPrice
84
Глава 4. Применение SQL 4.4. Триггеры
FROM ArtistWorkNet AW WHERE AW.WorkID = NEW.WorkID GROUP BY AW.WorkID;
newPrice = avgNetPrice + NEW.AcquisitionPrice; IF newPrice > 2 * (NEW.AcquisitionPrice) THEN
NEW.AskingPrice = newPrice;
ELSE
NEW.AskingPrice = 2 * (NEW.AcquisitionPrice);
END IF;
END IF;
RETURN NEW; END; $set_asking_price$ LANGUAGE plpgsql;
CREATE TRIGGER set_asking_price
BEFORE INSERT ON TRANSACTION
FOR EACH ROW EXECUTE PROCEDURE set_asking_price();
Триггер сначала подсчитывает количество строк в таблице
TRANSACTION, в которых значение WorkID равно NEW.WorkID. Поскольку
это предваряющий триггер, произведение еще не добавлено в базу данных, и количество будет равным нулю, если это произведение не появлялось в галерее ранее. В этом случае NEW.AskingPrice уста­навливается равным удвоенному значению AcquisitionPrice.
Если произведение появлялось в галерее в прошлом, рас­считывается средняя чистая прибыль от его продажи с помощью представления ArtistWorkNet. После этого вычисляется перемен­ная newPrice как сумма средней чистой прибыли и стоимости приоб­ретения. Наконец, NEW.AskingPrice присваивается большее из двух значений – newPrice или удвоенное значение AcquisitionPrice. Так как триггер предваряющий, для усреднения можно использовать встроенную функцию AVG: новая строка еще не добавлена в таблицу
WORK, поэтому она не будет учтена при расчете среднего значения.
Следует обратить внимание на один нюанс: если в какой-либо из строк представления ArtistWorkNet столбец SalesPrice или
AcquisitionPrice является пустым, это может вызвать проблемы
при вычислениях в триггере.
85
4.4. Триггеры Глава 4. Применение SQL
Рассмотренный триггер выполняет полезную функцию, избав­ляя персонал галереи от значительного количества ручной работы, а также повышая точность результатов.

4.4.3 Триггер, обновляющий представление

Обновление представлений в ряде случаев может оказаться затруднительным. Одна из таких проблем касается представлений, созданных при помощи операции соединения: как правило, СУБД не способна обновлять таблицы, лежащие в основе таких представ­лений. Однако, зная специфику конкретного приложения, можно определить, как следует интерпретировать запрос на обновление соединенного представления.
Рассмотрим представление CustomerInterests:
CREATE VIEW CustomerInterests AS SELECT C.Name AS Customer, A.Name AS Artist FROM CUSTOMER C JOIN CUSTOMER_ARTIST_INT CI
ON C.CustomerID = CI.CustomerID
JOIN ARTIST A
ON CI.ArtistID = A.ArtistID;
Данное представление содержит строки таблиц CUSTOMER и ARTIST, соединенные через таблицу пересечения. Столбцу
CUSTOMER.Name дан псевдоним Customer, а столбцу ARTIST.Name – Artist.
Запрос на изменение имени клиента в представлении
CustomerInterests можно интерпретировать как запрос на
изменение столбца Name в таблице CUSTOMER. Такой запрос, однако, может быть обработан лишь в том случае, если это имя является уникальным в таблице CUSTOMER. В противном случае невозможно будет определить, какую из строк следует обновлять.
В листинге 4.4 приведен текст замещающего триггера, кото­рый обновляет имя клиента, если это имя является уникальным в базе данных. Вместо того чтобы подсчитывать количество строк с данным именем клиента и выполнять обновление только в том случае, если такая строка всего одна, триггер обусловливает обнов­ление ключевым словом NOT EXISTS. Такая конструкция триггера
86
Глава 4. Применение SQL 4.4. Триггеры
позволяет PostgreSQL оптимизировать SQL-запрос и приводит к лучшей производительности.
Листинг 4.4.
CREATE OR REPLACE FUNCTION customer_interests_update()
RETURNS trigger AS $customer_interests_update$
BEGIN
UPDATE CUSTOMER C1 SET Name = NEW.Customer WHERE C1.Name = OLD.Customer AND NOT EXISTS (
SELECT * FROM CUSTOMER C2 WHERE C2.Name = C1.Name AND C2.CustomerID <> C1.CustomerID
);
RETURN NEW; END; $customer_interests_update$ LANGUAGE plpgsql;
CREATE TRIGGER customer_interests_update
INSTEAD OF UPDATE ON CustomerInterests
FOR EACH ROW EXECUTE PROCEDURE customer_interests_update();

4.4.4 Триггер, обеспечивающий ссылочную целостность

Согласно рис. 2.1, таблица WORK рассматриваемого в пособии примера имеет обязательного потомка в таблице TRANSACTION. Это означает, что всякий раз, когда в таблицу WORK вставляется стро­ка, необходимо вставить соответствующую ей строку в таблицу
TRANSACTION.
Реализовать такие ограничения ссылочной целостности непросто. Первое, что приходит в голову, это создать завершающий триггер для таблицы WORK, который бы проверял, что приложение создало требуемую строку в таблице TRANSACTION. Однако любой такой триггер будет запускаться непосредственно после вставки строки в таблицу WORK, то есть до того как приложение получит шанс вставить новую строку в таблицу
87
4.4. Триггеры Глава 4. Применение SQL
TRANSACTION. Таким образом, триггер не сможет увидеть строку,
которую собирается вставить приложение.
Другое решение – потребовать от приложения создать строку в таблице TRANSACTION, перед тем как вставить новую строку в таблицу WORK. Но в этом случае мы переопределяем связь так, что теперь строка в таблице TRANSACTION требует строки в таблице WORK. Поэтому СУБД откажется вставлять в таблицу TRANSACTION строку, не имеющую родителя, и сделает она это еще до того, как будет вызван какой-либо триггер таблицы TRANSACTION. Таким образом, триггер, определенный для таблицы TRANSACTION, никогда не полу­чит возможности создать требуемую строку в таблице WORK.
Одно возможное, хотя и не очень красивое, решение заклю­чается в том, чтобы создать пару триггеров. Первый из них будет создавать строку в таблице TRANSACTION при вставке строки в таб­лицу WORK. Второй триггер будет удалять строку, вставленную по умолчанию, когда приложение захочет вставить строку в таблицу
TRANSACTION. При таком подходе, если приложение не создаст требу-
емой строки, будет использоваться строка, вставленная триггером. Если приложение создаст новую строку в таблице TRANSACTION, то вставленная по умолчанию строка будет предварительно удалена.
Эта пара триггеров представлена в листингах 4.5 и 4.6. Пер­вый триггер (листинг 4.5) создает строку по умолчанию в таблице
TRANSACTION. Сначала он проверяет, нет ли в этой таблице существу-
ющей строки с таким WorkID; если нет, он вставляет новую строку. Второй триггер (листинг 4.6) удаляет строку, созданную первым триггером, когда пользователь вставляет соответствующую строку в таблицу TRANSACTION.
Листинг 4.5.
CREATE OR REPLACE FUNCTION enforce_trans_child()
RETURNS trigger AS $enforce_trans_child$
DECLARE
rowcount int; NewID int;
BEGIN
NewID = NEW.WorkID;
SELECT Count(*) INTO rowcount
88
Глава 4. Применение SQL 4.4. Триггеры
FROM TRANSACTION WHERE WorkID = NewID AND CustomerID IS NULL;
IF rowcount = 0 THEN
INSERT INTO TRANSACTION
(TransactionID, DateAcquired, WorkID) VALUES (nextval(’seq_transaction’), NOW(), NewID);
END IF;
RETURN NEW; END; $enforce_trans_child$ LANGUAGE plpgsql;
CREATE TRIGGER enforce_trans_child
AFTER INSERT ON WORK
FOR EACH ROW EXECUTE PROCEDURE enforce_trans_child();
Листинг 4.6.
CREATE OR REPLACE FUNCTION remove_dup_trans()
RETURNS trigger AS $remove_dup_trans$ BEGIN
DELETE FROM TRANSACTION
WHERE WorkID = NEW.WorkID
AND CustomerID IS NULL
AND TransactionID <> NEW.TransactionID;
RETURN NEW; END; $remove_dup_trans$ LANGUAGE plpgsql;
CREATE TRIGGER remove_dup_trans
BEFORE INSERT ON TRANSACTION
FOR EACH ROW EXECUTE PROCEDURE remove_dup_trans();
Тем не менее это решение нельзя назвать удовлетворитель­ным. Еще одна альтернатива, более сложная в реализации, но эс­тетически более привлекательная, – потребовать от приложения, чтобы новые произведения добавлялись в базу данных через пред­ставление, содержащее все необходимые данные для таблицы WORK и TRANSACTION. В этом случае вставку данных из представления
WorkTransaction в таблицы WORK и TRANSACTION осуществлял бы триг-
гер, а прямую вставку строк в эти таблицы следовало бы запретить.
89
4.5. Правила Глава 4. Применение SQL
4.5 Правила
Правила в PostgreSQL используются для изменения дерева за­проса. Перед началом выполнения запроса PostgreSQL формирует так называемое абстрактное синтаксическое дерево данного за­проса, в котором ветви сопоставлены с SQL-операторами, а листья – с соответствующими значениями.
Рассмотрим пример использования правил для решения зада­чи обновления представления (см. пункт 4.4.3). В данном случае правило работает аналогично триггеру из пункта 4.4.3 и «объясня­ет» PostgreSQL, каким образом применять оператор UPDATE к пред­ставлению CustomerInterests. Правила в PostgreSQL создаются с помощью оператора CREATE RULE (см. листинг 4.7).
Листинг 4.7.
CREATE RULE update_customer_interests AS ON UPDATE TO CustomerInterests DO INSTEAD
UPDATE CUSTOMER C1 SET Name = NEW.Customer WHERE C1.Name = OLD.Customer AND NOT EXISTS (
SELECT * FROM CUSTOMER C2 WHERE C2.Name = C1.Name AND C2.CustomerID <> C1.CustomerID
);

4.6 Модуль TABLEFUNC

Модуль TABLEFUNC содержит методы для формирования свод­ных таблиц.
Рассмотрим пример формирования сводной таблицы для от­слеживания количества продаж картин в каждом месяце по годам:
SELECT YEAR, SUM(JAN) AS JAN, SUM(FEB) AS FEB, SUM(MAR) AS MAR, SUM(APR) AS APR, SUM(MAY) AS MAY, SUM(JUN) AS JUN, SUM(JUL) AS JUL, SUM(AUG) AS AUG, SUM(SEP) AS SEP, SUM(OCT) AS OCT, SUM(NOV) AS NOV, SUM(DEC) AS DEC FROM crosstab(
90
Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]