ORA-04091: таблица [blah] мутирует, триггер/функция может ее не видеть

Недавно я начал работать над большим сложным приложением, и мне только что была назначена ошибка из-за этой ошибки:

ORA-04091: table SCMA.TBL1 is mutating, trigger/function may not see it
ORA-06512: at "SCMA.TRG_T1_TBL1_COL1", line 4
ORA-04088: error during execution of trigger 'SCMA.TRG_T1_TBL1_COL1'

Рассматриваемый триггер выглядит так

    create or replace TRIGGER TRG_T1_TBL1_COL1
   BEFORE  INSERT OR UPDATE OF t1_appnt_evnt_id ON TBL1
   FOR EACH ROW
   WHEN (NEW.t1_prnt_t1_pk is not  null)
   DECLARE
        v_reassign_count number(20);
   BEGIN
       select count(t1_pk) INTO v_reassign_count from TBL1
              where  t1_appnt_evnt_id=:new.t1_appnt_evnt_id and t1_prnt_t1_pk is not null;
       IF (v_reassign_count > 0) THEN
           RAISE_APPLICATION_ERROR(-20013, 'Multiple reassignments not allowed');
       END IF;
   END;

В таблице есть первичный ключ "t1_pk", "идентификатор события встречи" t1_appnt_evnt_id и еще один столбец "t1_prnt_t1_pk", который может содержать или не содержать t1_pk другой строки.

Похоже, что триггер пытается убедиться, что никто другой с тем же t1_appnt_evnt_id не ссылается на тот же самый, на который ссылается эта строка, ссылку на другую строку, если этот ссылается на другую строку.

В комментарии к отчету об ошибке от администратора базы данных говорится: «Удалите триггер и выполните проверку в коде», но, к сожалению, у них есть проприетарная среда генерации кода, наложенная поверх Hibernate, поэтому я даже не могу понять, где она на самом деле записывается, поэтому я надеюсь, что есть способ заставить этот триггер работать. Здесь?


person Paul Tomblin    schedule 17.12.2008    source источник
comment
Применять подобные правила только в коде — плохая идея, так как с несколькими одновременными обновлениями сложно справиться. Если вы синхронизируете свой код, вы можете получить грязные взаимоблокировки между этим и блокировками базы данных.   -  person WW.    schedule 18.12.2008
comment
Итог - триггеры Oracle отстой. Избегайте их, как чумы, за исключением таких простых вещей, как обновление значений последовательности или полей типа updated_by. Их триггеры были отстойными в 90-х и отстойными сейчас.   -  person Rich Bianco    schedule 12.04.2017


Ответы (4)


Думаю, я не согласен с вашим описанием того, что пытается сделать триггер. Мне кажется, что это предназначено для обеспечения соблюдения этого бизнес-правила: для заданного значения t1_appnt_event только одна строка может иметь ненулевое значение t1_prnt_t1_pk за раз. (Не имеет значения, имеют ли они одинаковое значение во втором столбце или нет.)

Интересно, что он определен для UPDATE OF t1_appnt_event, но не для другого столбца, поэтому я думаю, что кто-то может нарушить правило, обновив второй столбец, если для этого столбца нет отдельного триггера.

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

  • Таблица имеет числовой первичный ключ
  • Первичный ключ и t1_prnt_t1_pk всегда являются положительными числами.

Если эти предположения верны, вы можете создать такую ​​функцию:

dev> create or replace function f( a number, b number ) return number deterministic as
  2  begin
  3    if a is null then return 0-b; else return a; end if;
  4  end;

и такой индекс:

CREATE UNIQUE INDEX my_index ON my_table
  ( t1_appnt_event, f( t1_prnt_t1_pk, primary_key_column) );

Таким образом, строки, в которых столбец PMNT имеет значение NULL, будут отображаться в индексе с инверсией первичного ключа в качестве второго значения, поэтому они никогда не будут конфликтовать друг с другом. Строки, в которых это не NULL, будут использовать фактическое (положительное) значение столбца. Единственный способ получить нарушение ограничения — это если две строки имеют одинаковые значения, отличные от NULL, в обоих столбцах.

Возможно, это слишком «умно», но это может помочь вам обойти вашу проблему.

Обновление от Пола Томблина: я обновил первоначальную идею, которую Игорь поместил в комментариях:

 CREATE UNIQUE INDEX cappec_ccip_uniq_idx 
 ON tbl1 (t1_appnt_event, 
    CASE WHEN t1_prnt_t1_pk IS NOT NULL THEN 1 ELSE t1_pk END);
person Dave Costa    schedule 17.12.2008
comment
Ну что ж. Вы на мгновение оправдали мои надежды. :-) - person Paul Tomblin; 18.12.2008
comment
Такой подход может работать, вам просто нужно заставить функцию принимать два параметра. создайте уникальный индекс для F(CAPPEC_APPE_ID, CAPPEC_CAPPEC_ID_PMNT). Если вы возвращаете NULL из функции, она не индексируется. В противном случае вернуть CAPPEC_CAPPEC_ID_PMNT - person WW.; 18.12.2008
comment
@WW - разве это не предотвратит появление двух строк с разными значениями идентификатора, но с одним и тем же значением PMNT? - person Dave Costa; 18.12.2008
comment
Кстати, я переписал свой ответ, потому что придумал один из возможных способов заставить его работать. - person Dave Costa; 18.12.2008
comment
@ Дэйв Коста - Может быть, я неправильно понял функцию (очень быстро прочитал код триггера), но я думаю, что концепция верна. - person WW.; 18.12.2008
comment
А если бы функция вернула первичный ключ или 1, если значение cappec_cappec_id_pmnt не было нулевым? Я могу гарантировать, что первичный ключ не равен 1. - person Paul Tomblin; 18.12.2008
comment
Это сработает, Пол. Даже не нужна определяемая пользователем функция. УНИКАЛЬНЫЙ ИНДЕКС включен (cappec_appe_id, случай, когда CAPPEC_CAPPEC_ID_PMNT равен нулю, а затем 1, иначе заканчивается первичный ключ) - person Gary Myers; 18.12.2008

Я согласен с Дейвом в том, что желаемый результат, вероятно, может и должен быть достигнут с использованием встроенных ограничений, таких как уникальные индексы (или уникальные ограничения).

Если вам действительно нужно обойти ошибку изменяющейся таблицы, обычный способ сделать это — создать пакет, который содержит переменную области пакета, представляющую собой таблицу чего-то, что можно использовать для идентификации измененных строк (я думаю, что ROWID — это возможно, в противном случае вам придется использовать ПК, в настоящее время я не использую Oracle, поэтому я не могу его протестировать). Затем триггер FOR EACH ROW заполняет эту переменную всеми строками, измененными оператором, а затем существует триггер AFTER каждого оператора, который считывает строки и проверяет их.

Что-то вроде (синтаксис, вероятно, неправильный, я не работал с Oracle несколько лет)

CREATE OR REPLACE PACKAGE trigger_pkg;
   PROCEDURE before_stmt_trigger;
   PROCEDURE for_each_row_trigger(row IN ROWID);
   PROCEDURE after_stmt_trigger;
END trigger_pkg;

CREATE OR REPLACE PACKAGE BODY trigger_pkg AS
   TYPE rowid_tbl IS TABLE OF(ROWID);
   modified_rows rowid_tbl;

   PROCEDURE before_stmt_trigger IS
   BEGIN
      modified_rows := rowid_tbl();
   END before_each_stmt_trigger;

   PROCEDURE for_each_row_trigger(row IN ROWID) IS
   BEGIN
      modified_rows(modified_rows.COUNT) = row;
   END for_each_row_trigger;

   PROCEDURE after_stmt_trigger IS
   BEGIN
      FOR i IN 1 .. modified_rows.COUNT LOOP
         SELECT ... INTO ... FROM the_table WHERE rowid = modified_rows(i);
         -- do whatever you want to
      END LOOP;
   END after_each_stmt_trigger;
END trigger_pkg;

CREATE OR REPLACE TRIGGER before_stmt_trigger BEFORE INSERT OR UPDATE ON mytable AS
BEGIN
   trigger_pkg.before_stmt_trigger;
END;

CREATE OR REPLACE TRIGGER after_stmt_trigger AFTER INSERT OR UPDATE ON mytable AS
BEGIN
   trigger_pkg.after_stmt_trigger;
END;

CREATE OR REPLACE TRIGGER for_each_row_trigger
BEFORE INSERT OR UPDATE ON mytable
WHEN (new.mycolumn IS NOT NULL) AS
BEGIN
   trigger_pkg.for_each_row_trigger(:new.rowid);
END;
person erikkallen    schedule 17.12.2008

В любом решении на основе триггера (или на основе кода приложения) вам необходимо установить блокировку, чтобы предотвратить повреждение данных в многопользовательской среде. Даже если ваш триггер работал или был переписан, чтобы избежать проблемы с изменяющейся таблицей, это не помешало бы двум пользователям одновременно обновить t1_appnt_evnt_id до одного и того же значения в строках, где t1_appnt_evnt_id не равен нулю: предположим, что в настоящее время нет строк, где t1_appnt_evnt_id=123 и t1_prnt_t1_pk не равен нулю:

Session 1> update tbl1 
           set t1_appnt_evnt_id=123 
           where t1_prnt_t1_pk =456;
           /* OK, trigger sees count of 0 */

Session 2> update tbl1
           set t1_appnt_evnt_id=123
           where t1_prnt_t1_pk =789;
           /* OK, trigger sees count of 0 because 
              session 1 hasn't committed yet */

Session 1> commit;

Session 2> commit;

Теперь у вас есть поврежденная база данных!

Способ избежать этого (в коде триггера или приложения) состоит в том, чтобы заблокировать родительскую строку в таблице, на которую ссылается t1_appnt_evnt_id=123, перед выполнением проверки:

select appe_id 
into   v_app_id
from parent_table
where appe_id = :new.t1_appnt_evnt_id
for update;    

Теперь триггер сеанса 2 должен дождаться фиксации или отката сеанса 1, прежде чем он выполнит проверку.

Было бы намного проще и безопаснее реализовать индекс Дэйва Косты!

Наконец, я рад, что никто не предложил добавить PRAGMA AUTONOMOUS_TRANSACTION в ваш триггер: это часто предлагается на форумах и работает, поскольку исчезает проблема изменяющейся таблицы, но это еще больше усугубляет проблему целостности данных! Так что просто не...

person Tony Andrews    schedule 18.12.2008
comment
На самом деле, я получаю, что таблица мутирует ошибку при любой попытке обновить столбец cappec_appe_id. - person Paul Tomblin; 18.12.2008
comment
Извините, я не понимаю вашего комментария? - person Tony Andrews; 19.12.2008

У меня была аналогичная ошибка с Hibernate. И промывка сеанса с помощью

getHibernateTemplate().saveOrUpdate(o);
getHibernateTemplate().flush();

решил эту проблему для меня. (Я не публикую свой блок кода, так как был уверен, что все написано правильно и должно работать, но это не сработало, пока я не добавил предыдущий оператор flush()). Может быть, это может помочь кому-то.

person Zaur Guliyev    schedule 17.01.2014