Hosted by uCoz
ORA-04091 или как я боролся с мутирующими таблицами
 
	   ORA-04091 или как я боролся с мутирующими таблицами.
	
	Наверное распростаненная ситуация, когда необходимо решить следующую задачу.
	Есть таблица
	
	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_obj
	BEFORE UPDATE
	ON obj
	REFERENCING NEW AS NEW OLD AS OLD
	FOR EACH ROW
	declare
	temp_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_obj
	AFTER UPDATE
	ON obj
	REFERENCING NEW AS NEW OLD AS OLD
	declare
	    -- объявляем параметризованный курсор, 
	    -- в котором выберем все строки, кот. сделаны текущей транзакцией
	    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
	        -- удалим из пром. таблицы строку с текущим primary
	        delete 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;
	/
	
	Я надеюсь, что мое сбивчивое повествование поможет Вам в борьбе с мутантами.
	Вопросы, предложения и пожелания буду рад услышать по адресу - gsa@baltros.ru
	
	С уважением.
	Сергей Герасимов.