Таблица SQL мутирует Ошибка

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

CREATE TRIGGER RoomServiceLimit
BEFORE INSERT OR UPDATE ON RoomServices
FOR EACH ROW
DECLARE
    numService NUMBER;
    CURSOR C1 IS SELECT count(*) AS RoomCount FROM RoomServices WHERE roomNumber = :new.roomNumber;
BEGIN

IF(inserting) THEN
    SELECT count(*) into numService FROM RoomServices WHERE roomNumber = :new.roomNumber;
    if(numService > 2) THEN
        RAISE_APPLICATION_ERROR(-20001,'Room ' || :new.roomNumber || ' will have more than 3 services.');
    END IF;
END IF;

IF(updating) THEN
    FOR rec IN C1 LOOP
        IF(rec.RoomCount > 2) THEN
            RAISE_APPLICATION_ERROR(-20001,'Room ' || :new.roomNumber || ' will have more than 3 services.');
        END IF;
    END LOOP;
END IF;
END;    
/

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

Спасибо!


person rmsantos    schedule 12.12.2013    source источник
comment
Поверьте мне XXX в URL-адресе, результаты для некоторых интернет-провайдеров с фильтрацией для взрослых для фильтрации этого URL-адреса.   -  person Amir Pashazadeh    schedule 13.12.2013
comment
Это триггер Oracle, а не MySql, и сообщение об ошибке также исходит от Oracle, отредактируйте вопрос и замените тег MySql на Oracle.   -  person krokodilko    schedule 13.12.2013
comment
По какой причине вы используете CURSOR для части UPDATE, а не для части INSERT? Кроме того, единственный способ увеличить количество услуг на комнату с 2 до 3 в ОБНОВЛЕНИИ - это обновить roomNumber .... здесь нет решений, просто комментарии.   -  person Nick.McDermaid    schedule 13.12.2013
comment
Попробуйте использовать триггер уровня оператора вместо уровня строки. Удалите FOR EACH ROW и попробуйте снова запустить DML.   -  person Rachcha    schedule 13.12.2013


Ответы (3)


Не существует надежного способа применить такое ограничение с помощью триггеров. Один из возможных подходов — использовать материализованное представление, которое автоматически обновляется при фиксации и имеет проверочное ограничение, обеспечивающее выполнение вашего бизнес-правила:

create table roomservices (
  pk number not null primary key,
  roomnumber number);


create materialized view mv_roomservices  
refresh on commit as
select 
  pk,
  roomnumber,
  count(*) over (partition by roomnumber) as cnt 
from roomservices;

alter table mv_roomservices add constraint 
  chk_max_2_services_per_room check (cnt <= 2);  

Теперь всякий раз, когда вы добавляете более двух сервисов для комнаты и пытаетесь зафиксировать транзакцию, вы получите исключение ORA-12008 (ошибка в пути обновления материализованного представления).

person Frank Schmitt    schedule 13.12.2013
comment
Это не единственный способ. Также мат. просмотр при фиксации очень медленный, и я не был бы счастлив использовать для каждой проверки полные копии моих таблиц - person smnbbrv; 13.12.2013
comment
не работает во второй строке: вставьте в значения roomservices (12,3); вставить в значения roomservices(12,4); вставить в значения roomservices(12,5); - person smnbbrv; 13.12.2013
comment
Если вы попытаетесь вставить более двух строк в одну и ту же транзакцию - да. Вам придется откатиться и повторить попытку. - person Frank Schmitt; 13.12.2013
comment
Способ довольно интересный. Но значит ли это, что я не могу делать и массовые операции? Как обновить все строки или вставить из набора данных запроса? - person smnbbrv; 13.12.2013

Я предполагаю, что RoomServices:

  • а) представляет собой небольшую таблицу, которая не подвергается интенсивной модификации
  • б) никогда не будет комнаты с более чем 3 услугами

Примечание: вы говорите "более 3 служб", но ваш код говорит "более 2 служб". Поэтому я буду использовать «более двух сервисов».

Тогда как насчет использования триггера оператора?

CREATE OR REPLACE TRIGGER RoomServiceLimit
  AFTER INSERT OR UPDATE ON RoomServices
DECLARE
    badRoomsCount NUMBER;
    badRoomsList VARCHAR2(32767); -- adjust the varchar2 size according to your requirements
BEGIN
    SELECT COUNT(*), LISTAGG(roomNumber, ', ') WITHIN GROUP (ORDER BY 1) 
      INTO badRoomsCount, badRoomsList
      FROM (SELECT roomNumber FROM RoomServices GROUP BY roomNumber HAVING COUNT(*) > 2);
    IF (badRoomsCount > 0) THEN
        RAISE_APPLICATION_ERROR(-20001,'Room/s '||badRoomsList||' will have more than 2 services.');
    END IF;
END;
/

Если RoomServices мал, но имеет слишком много изменений (вставок или обновлений), вы можете рассмотреть возможность создания индекса для RoomNumber.

Если мои предположения ложны, попробуйте что-то вроде:

CREATE GLOBAL TEMPORARY TABLE RoomServicesAux as SELECT roomNumber FROM RoomServices WHERE 1=0;
/

CREATE OR REPLACE TRIGGER PreRoomServiceLimit
  BEFORE INSERT OR UPDATE ON RoomServices
BEGIN
  DELETE FROM RoomServicesAux;
END;
/

CREATE OR REPLACE TRIGGER RowRoomServiceLimit
  BEFORE INSERT OR UPDATE OF roomNumber ON RoomServices FOR EACH ROW
BEGIN
  INSERT INTO RoomServicesAux VALUES (:NEW.roomNumber);
END;
/

CREATE OR REPLACE TRIGGER RoomServiceLimit
  AFTER INSERT OR UPDATE ON RoomServices
DECLARE
    badRoomsCount NUMBER;
    badRoomsList VARCHAR2(32767); -- adjust the varchar2 size according to your requirements      
BEGIN
    SELECT COUNT(*), LISTAGG(roomNumber, ', ') WITHIN GROUP (ORDER BY 1) 
      INTO badRoomsCount, badRoomsList
      FROM (
          SELECT roomNumber 
            FROM RoomServices 
            WHERE roomNumber in (SELECT roomNumber FROM RoomServicesAux) 
            GROUP BY roomNumber 
            HAVING COUNT(*) > 2
           );
    DELETE FROM RoomServicesAux;
    IF (badRoomsCount > 0) THEN
        RAISE_APPLICATION_ERROR(-20001,'Room/s '||badRoomsList||' will have more than 2 services.');
    END IF;
END;
/

Или, если у вас есть Oracle 11g или выше, вы можете использовать составной триггер:

CREATE OR REPLACE TYPE RoomsListType IS TABLE OF INTEGER; -- change to the type of RoomServices.rowNumber
/

CREATE OR REPLACE TRIGGER RoomServiceLimit
  FOR INSERT OR UPDATE OF roomNumber ON RoomServices
COMPOUND TRIGGER
  RoomsList RoomsListType := RoomsListType();
  badRoomsCount NUMBER;
  badRoomsList VARCHAR2(32767); -- adjust the varchar2 size according to your requirements      
AFTER EACH ROW IS 
  BEGIN
    RoomsList.EXTEND;
    RoomsList(RoomsList.COUNT) := :NEW.roomNumber;
  END AFTER EACH ROW;
AFTER STATEMENT IS
  BEGIN
    SELECT COUNT(*), LISTAGG(roomNumber, ', ') WITHIN GROUP (ORDER BY 1) 
      INTO badRoomsCount, badRoomsList
      FROM (
          SELECT roomNumber 
            FROM RoomServices 
            WHERE roomNumber in (SELECT * FROM table(RoomsList))
            GROUP BY roomNumber 
            HAVING COUNT(*) > 2
           );        
    IF (badRoomsCount > 0) THEN
        RAISE_APPLICATION_ERROR(-20001,'Room/s '||badRoomsList||' will have more than 2 services.');
    END IF;
  END AFTER STATEMENT;
END;
/
person acesargl    schedule 13.12.2013
comment
в чем разница? вы все еще пытаетесь получить доступ к строкам, которые, вероятно, обновлены, в той же таблице - это снова вызовет мутацию - person smnbbrv; 13.12.2013
comment
Проблема заключается в попытке доступа к таблице с помощью триггера строки. Триггер состояния может получить доступ к своей таблице. - person acesargl; 13.12.2013
comment
РСУБД пытаются обеспечить 4 основных принципа, известных как принципы ACID (атомарность, непротиворечивость, изоляция и надежность). На уровне оператора атомарность означает, что вы видите вставку/удаление/обновление как одно изменение. Если вы выполняете один оператор, который изменяет атомарность многих строк, убедитесь, что вы можете видеть таблицу до первого изменения и после последнего изменения, но никто не должен видеть таблицу между первым и последним изменением. Проблема изменяющейся таблицы обычно, но не исключительно, связана с триггерами строк, которые обращаются к ее базовой таблице, и с многострочными операторами вставки/обновления/удаления. - person acesargl; 13.12.2013

Кажется, вы не можете решить эту проблему без некоторых обходных путей. Если вы не можете найти ничего лучше, проверьте это:

Я думаю, у вас есть таблица Room, в противном случае создайте ее:

alter table Room add (
  servicesCount integer default 0 not null check (servicesCount <= 3)
);

Затем обновите это число текущими значениями (не уверен, что утверждение верно, здесь это не ключевой момент)

update Room r
set servicesCount = (select count(*)
                     from RoomServices s
                     where r.roomNumber = s.roomNumber);

затем в вашем триггере

create trigger RoomServiceLimit
before insert or update on RoomServices
for each row
begin
  update Room
  set servicesCount = servicesCount + 1
  where roomNumber = :new.roomNumber;
end;

Выглядит довольно некрасиво, но, как я уже сказал, я не уверен, что с триггером можно найти что-то лучше.

EDIT Полный рабочий пример

drop table Room;
drop table RoomServices;

create table Room (
  roomNumber integer primary key,
  servicesCount integer default 0 not null check (servicesCount <= 3)
);

create table RoomServices (
  roomNumber integer,
  service varchar2(100),
  comments varchar2(4000)
);

create trigger RoomServiceLimit
before insert or update or delete on RoomServices
for each row
begin
  if inserting then
    update Room
    set servicesCount = servicesCount + 1
    where roomNumber = :new.roomNumber;
  elsif updating and :old.roomNumber != :new.roomNumber then
    update Room
    set servicesCount = servicesCount + 1
    where roomNumber = :new.roomNumber;

    update Room
    set servicesCount = servicesCount - 1
    where roomNumber = :old.roomNumber;
  elsif deleting then
    update Room
    set servicesCount = servicesCount - 1
    where roomNumber = :old.roomNumber;
  end if;
end;
/

insert into Room(roomNumber) values (1);
insert into Room(roomNumber) values (2);

insert into RoomServices(roomNumber,service,comments) values (1,'cleaning','first');
insert into RoomServices(roomNumber,service,comments) values (1,'drying','second');
insert into RoomServices(roomNumber,service,comments) values (1,'watering','third');
insert into RoomServices(roomNumber,service,comments) values (1,'something','third'); -- error

select * from room;

insert into RoomServices(roomNumber,service,comments) values (2,'something','2: first'); 

update RoomServices
set comments = null
where roomNumber = 2;

select * from room;

update RoomServices -- error
set roomNumber = 1
where roomNumber = 2;

select * from room;

delete from RoomServices where roomNumber = 1;

select * from room;
person smnbbrv    schedule 13.12.2013
comment
Извините, но это полностью сломано. Вы увеличиваете счетчик при каждом обновлении, даже не проверяя, какие поля изменились в обновлении. Поэтому, когда у меня есть столбец комментариев в roomservices и я дважды изменяю этот комментарий, этот триггер вызовет ошибку. Это также не может обрабатывать любые удаления (где количество служб должно быть уменьшено). - person Frank Schmitt; 13.12.2013
comment
да, но я не претендовал на полное решение. Все, что вы здесь рассказали, бесполезно и не содержит неразрешимой проблемы. Основная проблема темы - решить проблему с мутирующей таблицей. Я показал идею. - person smnbbrv; 13.12.2013
comment
Справедливо. Но даже если бы вы разместили полный триггер, он не работал бы надежно, потому что вы не можете решить проблему изменяющейся таблицы с помощью триггера. Это просто невозможно в Oracle. См. asktom.oracle.com/ pls/asktom/ и связанные темы о многопользовательском параллелизме. - person Frank Schmitt; 13.12.2013
comment
Действительно? Я не могу решить это, и это не работает? Смотрите обновленную версию и запускайте скрипт. Невозможно? Нет ничего невозможного! - person smnbbrv; 13.12.2013
comment
конечно, многопользовательский параллелизм - это проблема, но это проблема всего вопроса, потому что автор все равно пытается решить ее с помощью триггера. - person smnbbrv; 13.12.2013
comment
Это было именно то, о чем я говорил ранее — триггеры просто не подходят для этой работы. - person Frank Schmitt; 13.12.2013
comment
да, но, похоже, для этого нет действительно хорошего решения. Я много думал об этой проблеме раньше и, кажется, есть единственное решение: какая-то очередь, которая использует только одну сессию, которая проверяет каждый запрос и только потом вставляет сама. Но в любом случае сделать его асинхронным — реальная проблема, иначе все пользователи ждут одну сессию. - person smnbbrv; 13.12.2013