Ошибка преобразования таблиц с внешним ключом при удалении - PullRequest
3 голосов
/ 24 июня 2011

В нашей базе данных у нас есть таблица, в которой записи идентифицируются примерно из 4 других таблиц. Эти «дочерние» таблицы имеют внешний ключ к «основной» таблице со значением «on delete set null». Все таблицы имеют систему мутационных таблиц (то есть: пакет с plsql-таблицей, обрабатывает записи, когда процедура вызывается после триггера оператора). Однако после удаления записи в мастер-таблице дочерняя запись выдает ошибку «таблица является мутирующей». Который я нахожу немного странным, поскольку внешний ключ, кажется, вызывает неявный оператор обновления, который попадает в таблицу plsql.

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

Код для воспроизведения ошибки:

CREATE TABLE master_table (ID NUMBER(5) NOT NULL);
CREATE TABLE child_table (ID NUMBER(5) NOT NULL, master_id NUMBER(5));

alter table master_table add constraint master_pk primary key (ID);

alter table child_table add constraint child_pk primary key (ID);

ALTER TABLE child_table
  add constraint on_delete_master foreign key (master_id)
  references master_table (ID) on delete set null;

CREATE OR REPLACE PACKAGE pkg_child
IS
PROCEDURE init_temp;
PROCEDURE add_temp(i_action IN VARCHAR2, 
                   i_master_old IN child_table.master_id%TYPE, 
                   i_master_new IN child_table.master_id%TYPE);
PROCEDURE process_temp;
END;
/
CREATE OR REPLACE PACKAGE BODY pkg_child IS
   TYPE temp_record IS RECORD(
      action        VARCHAR2(1),
      old_master_id child_table.master_id%TYPE,
      new_master_id child_table.master_id%TYPE);

   TYPE type_temp IS TABLE OF temp_record INDEX BY BINARY_INTEGER;

   tab_temp type_temp;

   PROCEDURE init_temp IS
   BEGIN
      tab_temp.delete;
   END;

   PROCEDURE add_temp(i_action     IN VARCHAR2,
                      i_master_old IN child_table.master_id%TYPE,
                      i_master_new IN child_table.master_id%TYPE) IS
      v_id BINARY_INTEGER;
   BEGIN
      v_id := nvl(tab_temp.last, 0) + 1;
      tab_temp(v_id).action := i_action;
      tab_temp(v_id).old_master_id := i_master_old;
      tab_temp(v_id).new_master_id := i_master_new;
   END;

   PROCEDURE process_temp IS
      v_id    BINARY_INTEGER;
      v_total NUMBER;
   BEGIN
      v_id := tab_temp.first;
      WHILE v_id IS NOT NULL LOOP
         IF tab_temp(v_id).action = 'U' THEN
            SELECT COUNT(1)
              INTO v_total
              FROM child_table;
         END IF;
         v_id := tab_temp.next(v_id);
      END LOOP;
   END;
END;
/
CREATE OR REPLACE TRIGGER child_table_bs
 BEFORE 
 INSERT OR UPDATE OR DELETE
 ON child_table
 REFERENCING OLD AS OLD NEW AS NEW
BEGIN
  pkg_child.init_temp;
END;
/
CREATE OR REPLACE TRIGGER child_table_ar
 AFTER 
 INSERT OR DELETE OR UPDATE
 ON child_table
 REFERENCING OLD AS OLD NEW AS NEW
 FOR EACH ROW 
DECLARE
   v_action VARCHAR2(1);
BEGIN
   IF inserting THEN
      v_action := 'I';
   ELSIF updating THEN
      v_action := 'U';
   ELSIF deleting THEN
      v_action := 'D';
   END IF;
   pkg_child.add_temp(v_action, :old.id, :new.id);
END;
/
CREATE OR REPLACE TRIGGER child_table_as
 AFTER 
 INSERT OR UPDATE OR DELETE
 ON child_table
 REFERENCING OLD AS OLD NEW AS NEW
BEGIN
 pkg_child.process_temp;
END;
/

INSERT ALL 
   INTO master_table (id) VALUES (1) 
   INTO master_table (id) VALUES (2) 
   INTO master_table (id) VALUES (3) 
   INTO master_table (id) VALUES (4)
SELECT * FROM dual;

INSERT ALL
   INTO child_table (id, master_id) VALUES (1, NULL) 
   INTO child_table (id, master_id) VALUES (2, 1) 
   INTO child_table (id, master_id) VALUES (3, 2) 
   INTO child_table (id, master_id) VALUES (4, NULL)
SELECT * FROM dual;

-- error on this delete: mutating tables
-- why?
DELETE FROM master_table
 WHERE id = 2;

Код очистки:

DROP TRIGGER child_table_bs;
DROP TRIGGER child_table_ar;
DROP TRIGGER child_table_as;
DROP PACKAGE pkg_child;
DROP TABLE child_table;
DROP TABLE master_table;

Спасибо

1 Ответ

0 голосов
/ 25 июня 2011

У вас есть один оператор, DELETE из основной таблицы, который может влиять на несколько строк.Из-за ограничения CASCADE каждая удаленная строка будет запускать неявный / рекурсивный оператор UPDATE таблицы CHILD.То есть теоретически вы можете иметь несколько UPDATE дочерней таблицы.

Скажем, вы сделали DELETE FROM master_table WHERE id in (1, 2)

, что сгенерирует два оператора UPDATE child_table.Каждый из них будет пытаться выполнить триггер AFTER UPDATE, чтобы вы получили два выполнения

SELECT COUNT(1)
INTO v_total
FROM child_table

Результаты любого SELECT в одном операторе DELETE должны быть согласованными в определенный момент времени.Но SELECT не происходит в конце DELETE, а выполняется несколько раз во время удаления, каждый раз потенциально с другим результатом.Oracle может определить, какой результат вы хотите / ожидаете, поэтому выдает ошибку таблицы мутаций.

Не зная бизнес-требований, трудно рекомендовать решение.Как и Oracle, мы не знаем, что вы пытаетесь сделать.Возможно, ситуация может быть разрешена с помощью MV ON-COMMIT или DBMS_JOB, который будет выполнен в конце транзакции.

...