Обновление таблицы с представлением и получение «бесконечная рекурсия, обнаруженная в правилах для отношения «сотрудники»

Ниже приведен код, который я использую в базе данных PostgreSQL 16. Каждый раз, когда я пытаюсь обновить зарплату, я получаю:

ОШИБКА: в правилах для отношения «сотрудники» обнаружена бесконечная рекурсия
Состояние SQL: 42P17"

Используемый объект

create table rowan.employees (emp_id serial primary key, name varchar(100), salary numeric);

Вставка значений

insert into rowan.employees (name, salary) values 
('John Doe', 50000),
('Jane Doe', 60000),
('Rafael Orta',80000);

Создание правила

create or replace rule update_salary  
as on update to rowan.employees
where new.salary > 70000
and pg_trigger_depth() = 0
do instead
  update rowan.employees
  set salary = 70000
  where emp_id = new.emp_id;

Выполнение обновления

update rowan.employees set salary = 80000 where emp_id = 3;

Проверка значений

select * from rowan.employees;

Я пытаюсь выполнить команду ниже:

update rowan.employees set salary = 80000 where emp_id = 3;

Я ожидал, что он обновит зарплату до 70000.

Я не думаю, что pg_trigger_length() будет работать в правиле. Вне триггеров оно просто возвращает 0. Кроме того, правило может просто обнаруживать, что вы заменяете обновление обновлением, что может вызывать повышение флагов бесконечной рекурсии без подробной проверки условий.

Zegarek 28.04.2024 21:48

Это было предложено ИИ как решение моей проблемы, но без этого оно не работает. «pg_trigger_length() — это функция, которая возвращает текущую глубину выполнения триггера. Проверяя, равно ли оно 0, вы гарантируете, что правило применяется только во время начальной операции обновления, а не во время любых последующих обновлений, инициируемых самим правилом».

Rafael Orta 28.04.2024 21:57

Да, это ничего не делает, правила не являются триггерами.

Islingre 28.04.2024 22:01

Так вы сворачиваете новую зарплату в обновлениях. Разве вы не хотели бы того же для вставок? Кроме того, вы упоминаете «представление» в заголовке, но в тексте вопроса его нет?

Erwin Brandstetter 28.04.2024 22:33

Вне триггера pg_trigger_length() всегда равен 0.

Frank Heikens 28.04.2024 22:51
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
6
94
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Возможно, вы захотите использовать для этого триггер, как показано ниже (непроверено):

CREATE FUNCTION cap_salary() RETURNS trigger
AS $$
BEGIN
    IF NEW.salary > 70000
    THEN
        NEW.salary = 70000;
    FI;
    
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER cap_salary_on_update
    BEFORE UPDATE ON rowan.employees
    FOR EACH ROW
    EXECUTE FUNCTION cap_salary();

Вызов системы правил является проблемой, поскольку она сначала переписывает запрос, прежде чем что-либо будет выполнено. Условие WHERE будет частью переписанного запроса, но оно по-прежнему включает UPDATE on rowan.employees, т. е. правило выполняется снова — и снова и снова. Обратите внимание, что WHERE будет включен в переписанный запрос и не будет проверен перед переписыванием запроса (это включает в себя то, что все запросы UPDATE будут переписаны, независимо от того, выполнено ли условие).

Я с вами согласен, но это для класса, и мне нужно продемонстрировать пример использования правил в PostgreSQL. Кроме того, мне трудно понять, что не так с моим кодом; Мне любопытно, почему он выдает мне эту ошибку.

Rafael Orta 28.04.2024 21:55

Затем выберите другой пример, например, из документации (postgresql.org/docs/current/rules-update.html). Повторяю еще раз: правило перезаписывает запрос независимо от того, выполняется условие или нет. Результирующий запрос снова представляет собой UPDATE на rowan.employees, поэтому правило вызывается снова. Это приводит к бесконечному циклу/рекурсии, и именно поэтому PostgreSQL выдает ошибку.

Islingre 28.04.2024 22:00

@RafaelOrta: Если ваш учитель требует от вас написать обычай RULE, он ошибается. Правила в основном существуют для реализации представлений. Не прикасайтесь к правилам, если вы точно не знаете, что делаете. Вряд ли попытка простого смертного даст именно то, что должна.

Erwin Brandstetter 28.04.2024 22:44

@RafaelOrta Посмотрите почему бы и нет и две связанные статьи по этой теме.

Zegarek 29.04.2024 01:04

В PostgreSQL нет хорошего варианта использования правил, кроме реализации представлений. Ваше правило циклично, pg_trigger_depth() бесполезно и, возможно, должно полагаться на человеческий интеллект.

Laurenz Albe 29.04.2024 08:09
Ответ принят как подходящий
  1. Слушайте комментарии и используйте триггеры вместо правил.

  2. Если вы хотите использовать правило, то простой пример:

create table rule_test (id integer, fld_1 integer);
insert into rule_test values (1, 5), (2, 7);
create view rule_test_vw as select * from rule_test ;

create or replace rule update_do_nothing as on update to rule_test_vw do instead nothing;
create or replace rule update_do_something as on update to rule_test_vw where new.fld_1 > 10 do instead update rule_test set fld_1 = 10 where id = new.id;
create or replace rule update_do_passthru as on update to rule_test_vw where new.fld_1 <= 10 do instead update rule_test set fld_1 = new.fld_1 where id = new.id;

update rule_test_vw set fld_1 = 15 where id = 1;
update rule_test_vw set fld_1 = 3 where id = 2;

select * from rule_test;
 id | fld_1 
----+-------
  1 |    10
  2 |     3

По сути, создайте представление над таблицей и используйте do instead против представления, а instead действует против таблицы. Это решит проблему рекурсии.

Курок

PostgreSQL дешевле проверить условие перед вызовом триггерной функции:

CREATE FUNCTION cap_salary() RETURNS trigger AS $f$ 
BEGIN --trigger won't fire unless the salary needs to be reduced
      NEW.salary = 70000;--so no need for an `IF`
      RETURN NEW;
END $f$ LANGUAGE plpgsql;

CREATE TRIGGER cap_salary_on_update
    BEFORE INSERT OR UPDATE OF salary ON rowan.employees 
    FOR EACH ROW 
    WHEN (NEW.salary > 70000) --this limits when the trigger fires
    EXECUTE FUNCTION cap_salary();

Он достигает той же цели, но вы экономите некоторое время выполнения, избегая более дорогостоящих вызовов PL/pgSQL, когда в них нет необходимости.
Вы можете сравнить выполнение триггера time=1114.413 calls=200003 в этой демо с time=334.140 calls=60373 в этой демо.


Правило

Вам нужен только один: демо

begin;
alter table rowan.employees rename to t_employees; 
create view rowan.employees as table rowan.t_employees;

create rule salary_limit as 
  on update to rowan.employees
  do instead update rowan.t_employees 
  set salary = case when new.salary <= 70000 
                      or new.salary is null
                    then new.salary
                    else 70000 end
  where emp_id is not distinct from new.emp_id
  returning *;

commit;
  1. Настоящую таблицу нужно «спрятать» за каким-то другим объектом. Это может быть view, но система правил также может перенаправлять запросы с обычного table. Правило должно перехватывать все запросы, направленные на объект, выдающий себя за таблицу, и перенаправлять их на реальную таблицу.
  2. Обработка ограничения зарплаты в операторе case может быть предпочтительнее, чем использование нескольких условных правил. Наличие условия правила не позволяет вам добавить предложение returning, что, в свою очередь, нарушает все вызовы, которые хотели бы его использовать. Некоторые клиентские приложения, библиотеки и ORM добавляют предложение , возвращающее , по умолчанию, независимо от того, используете вы его или нет — нарушение, которое может привести к трудно отслеживаемым ошибкам.
  3. Хорошо обрабатывать случай null как для поля salary, так и для поля emp_id. Его необходимо добавить к case, а условие where следует переключить с нулевого небезопасного = на is not distinct from - в противном случае правило не будет обрабатывать null зарплаты или обновления, нацеленные на все строки, особенно на те, у которых есть null в emp_id.
    Последнее не так важно в вашем случае, поскольку emp_id - это primary key, что подразумевает, что это not null - это строго необходимо, когда нет явного или подразумеваемого not null ограничения.
  4. Заключаем настройку всего этого в одном: begin;...commit; транзакция позволяет избежать введения временного окна, в течение которого одновременные транзакции
    • временно потерять таблицу из виду сразу после ее переименования
    • получить к нему неограниченный доступ через представление, когда оно появится создано, прежде чем добавлять правило.

Другие вопросы по теме