Триггер для обновления каталога

=> CREATE OR REPLACE FUNCTION update_catalog() RETURNS trigger
AS $$
BEGIN
  INSERT INTO operations(book_id, qty_change)
      VALUES (OLD.book_id, NEW.onhand_qty - coalesce(OLD.onhand_qty,0));
  RETURN NEW;
END;
$$ VOLATILE LANGUAGE plpgsql;
CREATE FUNCTION
=> CREATE TRIGGER update_catalog_trigger
INSTEAD OF UPDATE ON catalog_v
FOR EACH ROW
EXECUTE PROCEDURE update_catalog();
CREATE TRIGGER

Проверка количества книг

Добавляем к таблице книг поле наличного количества. Важно: не добавлять фразу DEFAULT, иначе эта операция будет обновлять все строки таблицы, удерживая блокировку - это долго и не нужно.

=> ALTER TABLE books ADD COLUMN onhand_qty integer;
ALTER TABLE

Триггерная функция для обновления количества (предполагаем, что поле onhand_qty не может быть пустым):

=> CREATE OR REPLACE FUNCTION update_onhand_qty() RETURNS trigger
AS $$
BEGIN
    UPDATE books
    SET onhand_qty = onhand_qty + NEW.qty_change
    WHERE book_id = NEW.book_id;
    RETURN NEW;
END;
$$ VOLATILE LANGUAGE plpgsql;
CREATE FUNCTION

Дальше все происходит внутри транзакции.

=> BEGIN;
BEGIN

Блокируем операции на время транзакции:

=> LOCK TABLE operations;
LOCK TABLE

Начальное заполнение:

=> UPDATE books b
SET onhand_qty = (
    SELECT coalesce(sum(qty_change),0)
    FROM operations o
    WHERE o.book_id = b.book_id
);
UPDATE 6

Теперь, когда поле заполнено, задаем ограничения:

=> ALTER TABLE books ALTER COLUMN onhand_qty SET DEFAULT 0;
ALTER TABLE
=> ALTER TABLE books ALTER COLUMN onhand_qty SET NOT NULL;
ALTER TABLE
=> ALTER TABLE books ADD CHECK(onhand_qty >= 0);
ALTER TABLE

Универсальнее был бы AFTER-триггер, но он дороже, а мы знаем, что qty_change не изменяется.

=> CREATE TRIGGER update_onhand_qty_trigger
BEFORE INSERT ON operations
FOR EACH ROW
EXECUTE PROCEDURE update_onhand_qty();
CREATE TRIGGER

Готово.

=> COMMIT;
COMMIT

Теперь books.onhand_qty обновляется, но представление catalog_v по-прежнему вызывает функцию для подсчета количества. Хоть в исходном запросе обращение к функции синтаксически не отличается от обращения к полю, запрос был запомнен в другом виде:

=> \d+ catalog_v
                 View "bookstore.catalog_v"
    Column    |  Type   | Modifiers | Storage  | Description 
--------------+---------+-----------+----------+-------------
 book_id      | integer |           | plain    | 
 title        | text    |           | extended | 
 onhand_qty   | integer |           | plain    | 
 display_name | text    |           | extended | 
 authors      | text    |           | extended | 
View definition:
 SELECT b.book_id,
    b.title,
    onhand_qty(b.*) AS onhand_qty,
    book_name(b.book_id, b.title) AS display_name,
    authors(b.*) AS authors
   FROM books b
  ORDER BY (book_name(b.book_id, b.title));
Triggers:
    update_catalog_trigger INSTEAD OF UPDATE ON catalog_v FOR EACH ROW EXECUTE PROCEDURE update_catalog()

Пересоздадим представление:

=> CREATE OR REPLACE VIEW catalog_v AS
SELECT b.book_id,
       b.title,
       b.onhand_qty,
       book_name(b.book_id, b.title) AS display_name,
       b.authors
FROM   books b
ORDER BY display_name;
CREATE VIEW

Теперь функцию можно удалить.

=> DROP FUNCTION onhand_qty(books);
DROP FUNCTION

Небольшая проверка:

=> SELECT * FROM catalog_v WHERE book_id = 1;
 book_id |         title         | onhand_qty |            display_name             |          authors           
---------+-----------------------+------------+-------------------------------------+----------------------------
       1 | Сказка о царе Салтане |         19 | Сказка о царе Салтане. Пушкин А. С. | Пушкин Александр Сергеевич
(1 row)

=> INSERT INTO operations(book_id, qty_change) VALUES (1,+10);
INSERT 0 1
=> SELECT * FROM catalog_v WHERE book_id = 1;
 book_id |         title         | onhand_qty |            display_name             |          authors           
---------+-----------------------+------------+-------------------------------------+----------------------------
       1 | Сказка о царе Салтане |         29 | Сказка о царе Салтане. Пушкин А. С. | Пушкин Александр Сергеевич
(1 row)

=> INSERT INTO operations(book_id, qty_change) VALUES (1,-100);
ERROR:  new row for relation "books" violates check constraint "books_onhand_qty_check"
DETAIL:  Failing row contains (1, Сказка о царе Салтане, -71).
CONTEXT:  SQL statement "UPDATE books
    SET onhand_qty = onhand_qty + NEW.qty_change
    WHERE book_id = NEW.book_id"
PL/pgSQL function update_onhand_qty() line 3 at SQL statement