Вопрос

I'm attempting to write a trigger that will disallow any room in a hospital to have more than 3 services. The table RoomServices has a room number and a service that it has. So the only way to determine this is to group the rooms by room number and count the services. I have tried the code:

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;    
/

I've tried running each method separately with insert and update, and inserting always works and updating will always give me the mutating table error. I don't know how else to go about solving this problem, so any advice would be greatly appreciated.

Thanks!

Это было полезно?

Решение

There is no reliable way to enforce this kind of constraint using triggers. One possible approach is to use a materialized view that automatically refreshes on commit and has a check constraint enforcing your business rule:

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);  

Now, whenever you add more than two services for a room and try to commit your transaction, you will get a ORA-12008 exception (error in materialized view refresh path).

Другие советы

I assume that RoomServices:

  • a) is a small table that is not intensively modified
  • b) there will never exist a room with more than 3 services

Note: you say "more than 3 services" but your code says "more than 2 services". So I will use "more than 2 services".

Then, what about using a statement trigger?

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;
/

If RoomServices is small but have too many changes (inserts or updates) then you may consider create an index on RoomNumber.

If my assumptions are false try something like:

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;
/

Or if you have Oracle 11g or greater then you can use a compound trigger:

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;
/

Seems you cannot solve this issue without some workarounds. If there is nothing better you can find, check this out:

I guess you have table Room, otherwise create one:

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

Then update this number with current values (not sure if the statement is valid, it is not the key point here)

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

then in your trigger

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

Looks quite ugly, but, as I've told, I am not sure you can find anything better with trigger.

EDIT The complete working example

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;
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top