Сергей Герасимов
НПО "Балтрос"
ORA-04091 или как я боролся с мутирующими таблицами.
Источник: http://pbl.narod.ru/mut.txt
Наверное, распростаненная ситуация, когда необходимо решить следующую задачу. Есть таблица, в которой реализован стандартный подход к посторению иерархического хранилища:
create table test_table (id number(10) constraint pk_id primary key, id_parent number(10) constraint fk_id references test_table(id), prop1 varchar2(10), prop2 varchar2(10) );
В строках таблицы хряняться сущности, имеющие свои свойства, и организованные в иерархию.
Часть свойств у каждой сущности индивидуален, а часть - это свойство сущности верхнего уровня.
Например:
Здание
Часть здания Помещение Часть помещения
У всех дочерних записей свойства одинаковое свойство родителя - адрес. Встала задача - при изменении общего для родителя свойства - изменять его у всех дочерних триггером. Естественно, в прямую мои попытки это сделать потерпели полное фиаско:
ORA-04091 или table name is mutating, trigger/function may not see it.
Вот мой путь решения данной проблемы:
использование
"универсальной" дополнительной таблицы и двух триггеров;
один из них before update for each rows,
другой after update for
statement.
Универсальная в кавычках, потому что в данном случае это приемлимо только для таблиц с одиночным primary key. Для составного надо накручивать логику и дополнять поля под количество полей в primary key. Работают они так:
При
изменении триггер before для каждой измененной строки проверяет, а изменились
ли в данной записи общие свойства.
Если да,
то записывает в доп. таблицу значение первичного ключа в одно поле и id
транзакции в другое.
Id транзакции
необходим, чтобы разделить изменения разных пользователей при соответствующем
уровне изоляции.
При
необходимости можно добавить еще одно поле, куда записывать имя таблицы, в
которой производятся изменения, для того, чтобы в одной транзакции изменять
несколько "ушастых таблиц"
Дальше
триггер after анализирует дополнительную таблицу и производит изменения в
исходной таблице, зная, какие первичные ключи подверглись изменениям.
Данный механизм работает каскадно. Важно оценить возможную степень вложенности и объемы изменяемой информации для того, чтобы корректно установить значение OPEN_CURSORS в файле init.ora. По умолчанию = 50 открытых курсоров на сессию.
Примеры триггеров.
CREATE OR REPLACE TRIGGER lau_objBEFORE UPDATEON objREFERENCING NEW AS NEW OLD AS OLDFOR EACH ROWdeclaretemp_local_tran_id varchar2(100); Begin
-- проверка изменения общего свойства
if <условие> then
temp_local_tran_id:=dbms_transaction.local_transaction_id; -- запись первичного ключа в доп. таблицуinsert into temp_ehcap_obj
values(:new.obj_id, temp_local_tran_id);end if;End;/
-- разбор доп. таблицы в о втором триггере
CREATE OR REPLACE TRIGGER lsu_objAFTER UPDATEON objREFERENCING NEW AS NEW OLD AS OLDdeclare
-- объявляем параметризованный курсор, -- в котором выберем все строки, -- которые сделаны текущей транзакциейCURSOR my_tran (par_local_tran_id IN varchar2) IS
select obj_id, local_tran_id from temp_ehcap_obj where local_tran_id = par_local_tran_id ; temp_local_tran_id varchar2(100); temp_1 varchar2(30); temp_2 date;Begin
-- получим текущий id транзакцииtemp_local_tran_id:= dbms_transaction.local_transaction_id;
-- цикл по курсору
FOR my_tran_rec IN my_tran(temp_local_tran_id) LOOP -- выберем из основной таблицы те свойства, -- которые хотим поменять у дочерних записейselect prop1, prop2
into temp_1, temp_2 from obj where obj_id = my_tran_rec.obj_id; -- ВАЖНО!!!!!! -- так как подразумевается рекурсия, -- то операцию удаления необходимо делать -- здесь, до UPDATE, который опять вызовет -- срабатывание триггера for each rows -- если не удалим - окажемся в бесконечю цикле, -- но ORACLE нас спасет и выкинет при достижении -- количества открытых курсоров, --определенных параметров OPEN_CURSOR -- удалим из пром. таблицы строку с текущим primarydelete from temp_ehcap_obj
where local_tran_id = temp_local_tran_id and obj_id = my_tran_rec.obj_id ; -- меняем основную таблицу, -- изменяем общие свойства для дочек. -- в моем случае obj_whole_id - ссылка на родителяupdate obj
set prop1 = temp_1, prop2 = temp_2 where obj_whole_id = my_tran_rec.obj_id; -- конец. ВСЕМ СПАСИБО.END LOOP;
End;/
Я надеюсь, что мое сбивчивое повествование поможет Вам в борьбе с мутантами. Вопросы, предложения и пожелания буду рад услышать по адресу mailto:gsa@baltros.ru
С уважением,
Сергей Герасимов.