Преобразование триггера DB2 в триггеры Oracle вызывает ошибку ORA-04091.

Итак, у меня есть две таблицы: JOBS и TASKS.

В таблице TASKS есть столбец TAKS_STATUS, в котором хранится последовательность состояний (например, «Отправлено», «Отправлено», «Авторизовано», «Завершено»).

В таблице JOBS есть столбец JOB_STATUS, который представляет собой сводку (то есть минимальное состояние) столбцов TASK_STATUS в таблице TASKS. Таблица JOBS также имеет значение TASK_COUNT, которое содержит количество ЗАДАЧ, связанных с заданием.

Задания могут иметь одну или несколько задач: JOB_ID в каждой таблице их связывает.

В DB2 у меня есть ряд простых триггеров для свертки этого состояния; вот один посередине:

create or replace trigger JOB_AUTHORED
  after update of TASK_STATUS on TASKS
  referencing NEW as N
  for each row
  when (TASK_STATUS = 'Authored')
    update JOBS
      set JOB_STATUS = 'Authored'
      where JOBS.JOB_ID = N.JOB_ID
        and TASK_COUNT=(
          select count(0) from TASKS
            where TASKS.JOB_ID = N.JOB_ID
              and TASKS.TASK_STATUS in ('Authored','Completed'))

Это прекрасно работает в DB2, поскольку триггер выполняется в той же рабочей единице, что и триггерное событие, и, таким образом, он может видеть незафиксированные изменения рабочей единицы и может подсчитывать только что произошедшее изменение TASK_STATUS без блокировки строки. .

Вот переведенный триггер в Oracle:

create or replace trigger JOB_AUTHORED
  after update of TASK_STATUS on TASKS
  for each row
  when (NEW.TASK_STATUS = 'Authored')
  BEGIN
    update JOBS
      set JOB_STATUS='Authored'
      where JOBS.JOB_ID = :NEW.JOB_ID and TASK_COUNT=(
        select count(0) from TASKS
          where TASKS.JOB_ID = :NEW.JOB_ID
            and TASKS.TASK_STATUS in ('Authored','Completed'));
  END;

В Oracle это не удается:

ORA-04091: table MYSCHEMA.TASKS is mutating, trigger/function may not see it#012ORA-06512: at "MYSCHEMA.JOB_AUTHORED", line 1#012ORA-04088: error during execution of trigger 'MYSCHEMA.JOB_AUTHORED'#012] [query: UPDATE TASKS SET TASK_STATUS=:1 where TASK_ID=:2

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

Думаю, я мог бы изменить триггер AFTER на триггер INSTEAD OF и обновить TASK_STATUS (а также JOB_STATUS) внутри триггера (чтобы обновление задания могло видеть обновление задачи), но столкнусь ли я с той же ошибкой? Может быть, не первое обновление задачи, но что, если запускающая программа обновляет кучу ЗАДАЧ перед фиксацией: что происходит, когда обновляется вторая задача?

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

Какова наилучшая практика с чем-то подобным в Oracle?


person kjpires    schedule 22.05.2015    source источник
comment
Не могли бы вы объяснить, почему вы сравниваете JOBS.TASK_COUNT с текущим количеством строк в таблице TASKS? Я могу предположить, что по каким-то причинам (ошибки приложения? кто-то вставил/удалил некоторые строки непосредственно в/из таблицы?) количество в JOBS не будет соответствовать количеству связанных строк в таблице TASKS, а данные в таблицах не в согласованном состоянии. В этом случае ваш триггер молча выйдет из строя и не выдаст ожидаемых причин. Как вы обеспечиваете соблюдение правила JOBS.TASK_COUNT must always match the number of rows in TASKS table ?   -  person krokodilko    schedule 22.05.2015
comment
Я подсчитываю количество ЗАДАЧ, которые находятся в определенном состоянии или позже, чтобы увидеть, соответствует ли оно TASK_COUNT, и, таким образом, знаю, что все ЗАДАЧИ дошли до определенного момента (в этом примере: Автор). Итак, для дальнейшего примера, предположим, что задание создано с 20 ЗАДАЧАМИ в статусе отправки, и они отключаются и проходят через различные состояния. Когда первая задача получает статус «Авторство», этот триггер проверяет, есть ли 20 ЗАДАНИЙ в СТАТУСЕ «Авторизовано» или «Завершено», и если есть, задание переходит в СТАТУС «Авторизовано». Поскольку это была первая «авторская» задача, счетчик будет равен 1 и, следовательно, не будет смены задания.   -  person kjpires    schedule 22.05.2015
comment
Когда 20-я задача достигает СТАТУСА «Авторизовано», триггер, где с количеством выбранных, становится истинным, и СТАТУС задания устанавливается на «Авторизовано». На этом этапе некоторые задачи могут находиться в состоянии «Авторизовано», а некоторые могут находиться в более позднем состоянии (например, «Завершено»). Триггер срабатывал ровно 20 раз, при этом счет шел от 1 до 20. Если некоторые задачи перешли в СТАТУС «Ошибка» до или после, задание who никогда не получит статус «Авторизовано» (и другой триггер приводит к тому, что задание перейдет в СТАТУС «Ошибка»).   -  person kjpires    schedule 22.05.2015


Ответы (3)


Лучше всего по возможности избегать триггеров.
Посмотрите эти ссылки, чтобы узнать, почему не следует использовать триггеры:
http://www.oracle.com/technetwork/testcontent/o58asktom-101055.html
http://rwijk.blogspot.com/2007/09/database-triggers-are-evil.html

Используйте процедуру (API ) вместо триггеров - можно создать пакет с несколькими процедурами вроде add_new_job, add_new_task, change_task_status и т.д., и поместить всю логику в этих процедурах (проверка, изменение состояний задач, изменение состояний заданий и т.д.) в одном месте. Легко понять, легко поддерживать, легко отлаживать и легко отслеживать ошибки.


Если вы настаиваете на использовании триггеров, вы можете создать compound trigger, как Том Кайт упомянул в первой ссылке выше как workaround, например:

create or replace TRIGGER JOB_AUTHORED
FOR UPDATE OF TASK_STATUS on TASKS
COMPOUND TRIGGER

  TYPE JOB_ID_TABLE_TYPE IS TABLE OF PLS_INTEGER INDEX BY PLS_INTEGER;  
  JOB_ID_TABLE JOB_ID_TABLE_TYPE;
  dummy CHAR;

    AFTER EACH ROW IS
     BEGIN
       -- SELECT null INTO dummy FROM JOBS WHERE job_id = :NEW.JOB_ID FOR UPDATE;
        JOB_ID_TABLE( :NEW.JOB_ID ) := :NEW.JOB_ID;
     END AFTER EACH ROW;

     AFTER STATEMENT IS
     BEGIN
       FORALL x IN INDICES OF JOB_ID_TABLE
         UPDATE jobs set JOB_STATUS='Authored'
         WHERE JOBS.JOB_ID = JOB_ID_TABLE( x )
           and TASK_COUNT=(
                  select count(0) from TASKS
                  where TASKS.JOB_ID = JOBS.JOB_ID
                    and TASKS.TASK_STATUS in ('Authored','Completed')
          );
     END AFTER STATEMENT;

END JOB_AUTHORED;

но .....
Я не уверен, что в этом примере нет каких-либо подводных камней, о которых мы пока не знаем.

Например, в этом сценарии есть одна ловушка:
Предположим, что есть 18 задач со статусом Authored

  • В момент времени X пользователь A запускает UPDATE TASK SET со статусом «Авторизовано», WHERE task_id = 2. Триггер срабатывает и видит 18+1 зафиксированную задачу со статусом Authored
  • В момент времени X+10 мс пользователь B запускает UPDATE TASK1 SET status ' Authored' task_id = 4. Триггер срабатывает и видит 18+1 зафиксированную задачу со статусом Authored
  • В момент времени X+20 мс пользователь А совершает коммит
  • В момент времени X+30 мс пользователь Б совершает фиксацию
  • В итоге имеем 21 задачу со статусом authored. но статус задания не был изменен на Authored (но его следует изменить на Authored, если количество задач = 20).

Чтобы избежать этой ловушки, вы можете использовать SELECT null INTO dummy FROM JOBS WHERE job_id = :NEW.JOB_ID FOR UPDATE; в части after each row триггера, чтобы поместить блокировку на соответствующую запись в таблице JOBS, чтобы сериализовать доступ (я прокомментировал это в примере выше).
Но я все еще не уверен, что это правильное решение - это, в свою очередь, может вызвать некоторые взаимоблокировки в сценарии, который я не могу себе представить и предсказать в настоящее время.

person krokodilko    schedule 25.05.2015
comment
Странно, что мы ответили одновременно через несколько дней. ;-) - person Ponder Stibbons; 26.05.2015
comment
Я хотел бы избежать триггеров, но я не упомянул, что мы хотели бы еще больше избежать обновления всех агентов, изменяющих TASK_STATUS (который был создан за несколько лет до таблицы JOBS). Итак, я думаю, что я выберу ваше составное триггерное решение с SELECT ... FOR UPDATE row-lock или отдельную программу сканирования, которая сканирует активные JOBS и проверяет минимальный TASK_STATUS всех своих TASKS. - person kjpires; 26.05.2015
comment
Кстати, я рассмотрел триггер DB2 в свете того, что вы упомянули здесь с Oracle, но я считаю, что он работает в DB2: DB2 блокирует строку TASKS перед запуском триггера и, таким образом, блокирует выбор в триггере, который выполняется в другой рабочей единице с момента завершения до фиксации первой рабочей единицы. Хотя это обеспечивает согласованное поведение, это увеличивает вероятность взаимоблокировки в зависимости от того, насколько плохо вы справляетесь с РАБОТАМИ и ЗАДАЧАМИ в других местах. - person kjpires; 26.05.2015
comment
Хотя оператор FORALL имеет правильный синтаксис, я не могу заставить его скомпилироваться. Если я удалю оператор FORALL и заменю его простым обновлением (например, UPDATE jobs set JOB_STATUS='Authored' WHERE 0=1), триггер скомпилируется. Когда я добавляю обратно FORALL x IN INDICES OF JOB_ID_TABLE (перед манекеном UPDATE), это не удается. Так как значения коллекции совпадают с индексами, я даже попробовал VALUES OF вместо INDICES OF, но все равно не в восторге. Я также пробовал JOBS.JOB_ID = JOB_ID_TABLE( JOBS.JOB_ID ) вместо FORALL, но это тоже не удалось. Идеи? - person kjpires; 01.06.2015

Вкратце - в Oracle в триггерах нельзя select from таблицу, по которой строится триггер. В противном случае вы можете/вы получите ошибку изменяющейся таблицы.

У вас есть несколько вариантов:

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

create or replace view v_jobs_done as 
  select * from jobs where not exists 
    select 1 from tasks 
      where TASKS.JOB_ID = jobs.JOB_ID
        and TASKS.TASK_STATUS not in ('Authored','Completed')

2) Вместо увеличения значения используйте уменьшающееся значение, поэтому, когда jobs.tasks_count достигает нуля, вы знаете, что все сделано. В этом случае вам нужно создать/перестроить другие триггеры,

3) Предложение близко к вашему - вы можете использовать современный составной триггер - тут я сомневаюсь в производительности, но работает:

create or replace trigger Job_Authored
for update of task_status on tasks compound trigger

  type t_ids   is table of tasks.job_id%type;
  type t_cnts  is table of number;
  type t_job_counts is table of number index by varchar2(10);
  v_ids        t_ids;
  v_cnts       t_cnts;
  v_job_counts t_job_counts;

before statement is
  begin
    select job_id, count(1) 
      bulk collect into v_ids, v_cnts
      from tasks where tasks.task_status in ('Authored','Completed')
      group by job_id;

    for i in 1..v_ids.count() loop
      v_job_counts(v_ids(i)) := v_cnts(i);
    end loop;
  end before statement;

after each row is
  begin
    if :new.task_status = 'Authored' then 
      update jobs set job_status='Authored'
        where job_id = :new.job_id
          and task_count = v_job_counts(:new.job_id);
    end if;
  end after each row;
end Job_Authored;
person Ponder Stibbons    schedule 25.05.2015

Лучше всего по возможности избегать триггеров.
См. эти ссылки для ответа, почему нельзя использовать триггеры:
http://www.oracle.com/technetwork/testcontent/o58asktom-101055.html
http://rwijk.blogspot.com/2007/09/database-triggers-are-evil.html

Используйте процедуру (API ) вместо триггеров - можно создать пакет с несколькими процедурами вроде add_new_job, add_new_task, change_task_status и т.д., и поместить всю логику в этих процедурах (проверка, изменение состояний задач, изменение состояний заданий и т.д.) в одном месте. Легко понять, легко поддерживать, легко отлаживать и легко отслеживать ошибки.


Если вы настаиваете на использовании триггеров, вы можете создать compound trigger, как Том Кайт упомянул в первой ссылке выше как workaround, например:

create or replace TRIGGER JOB_AUTHORED
FOR UPDATE OF TASK_STATUS on TASKS
COMPOUND TRIGGER

  TYPE JOB_ID_TABLE_TYPE IS TABLE OF PLS_INTEGER INDEX BY PLS_INTEGER;  
  JOB_ID_TABLE JOB_ID_TABLE_TYPE;
  dummy CHAR;

    AFTER EACH ROW IS
     BEGIN
       -- SELECT null INTO dummy FROM JOBS WHERE job_id = :NEW.JOB_ID FOR UPDATE;
        JOB_ID_TABLE( :NEW.JOB_ID ) := :NEW.JOB_ID;
     END AFTER EACH ROW;

     AFTER STATEMENT IS
     BEGIN
       FORALL x IN INDICES OF JOB_ID_TABLE
         UPDATE jobs set JOB_STATUS='Authored'
         WHERE JOBS.JOB_ID = JOB_ID_TABLE( x )
           and TASK_COUNT=(
                  select count(0) from TASKS
                  where TASKS.JOB_ID = JOBS.JOB_ID
                    and TASKS.TASK_STATUS in ('Authored','Completed')
          );
     END AFTER STATEMENT;

END JOB_AUTHORED;

но .....
Я не уверен, что в этом примере нет каких-либо подводных камней, о которых мы пока не знаем.

Например, в этом сценарии есть одна ловушка:
Предположим, что есть 18 задач со статусом Authored

  • В момент времени X пользователь A запускает UPDATE TASK SET со статусом «Авторизовано», WHERE task_id = 2. Триггер срабатывает и видит 18+1 зафиксированную задачу со статусом Authored
  • В момент времени X+10 мс пользователь B запускает UPDATE TASK1 SET status ' Authored' task_id = 4. Триггер срабатывает и видит 18+1 зафиксированную задачу со статусом Authored
  • В момент времени X+20 мс пользователь А совершает коммит
  • В момент времени X+30 мс пользователь А совершает коммит
  • В итоге имеем 21 задачу со статусом authored. но статус задания не был изменен на Authored (но его следует изменить на Authored, если количество задач = 20).

Чтобы избежать этой ловушки, вы можете использовать SELECT null INTO dummy FROM JOBS WHERE job_id = :NEW.JOB_ID FOR UPDATE; в части after each row триггера, чтобы поместить блокировку на соответствующую запись в таблице JOBS, чтобы сериализовать доступ (я прокомментировал это в примере выше).
Но я все еще не уверен, что это правильное решение - это может привести к взаимоблокировкам в сценарии, который я сейчас не могу представить и предсказать.

person krokodilko    schedule 25.05.2015