Oracle DBMS - Read a table before processing Updating in an AFTER trigger - mutating table - sql

I've been experimenting with Oracle over the past few weeks and I've stumbled upon an issue which I can't seem to wrap my head around.
I am building a small property management system and I am trying to process as many actions as possible on the database side (purely experimental, I just wanted to clear this up before anyone asks, "why dont you just update these rows through the client")
In my system, I have a properties and rooms table (simplified schema below):
`-------------------------------
` PROPERTIES
`-------------------------------
`- PropertyID: PK
`- PropertyStatus: VARCHAR
`-------------------------------
`-------------------------------
` ROOMS
`-------------------------------
`- RoomID: PK
`- PropertyID: FK
`- RoomStatus: VARCHAR
`-------------------------------
Whenever a user is assigned to a room the rooms status is updated to OCCUPIED, once this happens I wish to check how many rooms associated with property n are taken, if all rooms are taken the property_status should be Updated to FULL, then if users are unassigned from properties the value updates to VACANCIES AVAILABLE etc.
I have the basic logic for this mapped out:
-- Return how many vacant rooms belong to this property
CREATE OR REPLACE FUNCTION prop_vacancy_query(
p_property_id properties.property_id%TYPE
)
RETURN NUMBER
IS
v_prop_rooms NUMBER;
BEGIN
SELECT COUNT(room_status)
INTO v_prop_rooms
FROM rooms
JOIN properties ON
rooms.property_id = properties.property_id
WHERE room_status = 'VACANT'
AND rooms.property_id = p_property_id;
RETURN v_prop_rooms;
END prop_vacancy_query;
In my AFTER trigger on my rooms table I try to call the query but I get a mutating table error, I believe this is because prop_vacancy_query is reading the properties table.
CREATE OR REPLACE TRIGGER trg_rooms_after
AFTER INSERT OR UPDATE ON rooms FOR EACH ROW
BEGIN
-- Update the table based on the result
IF prop_vacancy_query(:NEW.property_id) = 0 THEN
UPDATE properties
SET prop_status = 'VACANT'
WHERE properties.property_id = :NEW.property_id;
ELSE
UPDATE properties
SET prop_status = 'FULL'
WHERE properties.property_id = :NEW.property_id;
END IF;
END;
Previously this code worked for my system, but since reading more into pragma autonomous transactions I have realised it was extremely bad practice to run the prop_vacancy_query() on its own independent transaction.
Is there any way that I can read from the properties table and then update the rooms table without getting a mutating error?

Just to clarify, the mutating table exception is thrown because you are trying to read from the rooms table in your function, not because you are trying to read from the properties table. Since you have a row-level trigger on rooms, that means that the rooms table is in the middle of a change when the row-level trigger is firing and that it may be in an inconsistent state. Oracle prevents you from querying the rooms table in that situation because the results are not necessarily deterministic or reproducible.
If you created a statement-level trigger (removing the FOR EACH ROW) and put your logic there, you would no longer encounter a mutating table exception because the rooms table would no longer be in an inconsistent state. A statement-level trigger, though, is not able to see which row(s) were modified. That would mean that you'd need to look across all properties to see which status values should be adjusted. That's not going to be particularly efficient.
At the cost of additional complexity, you can improve the performance by capturing which properties changed in a row-level trigger and then referring to that in a statement-level trigger. That generally requires three triggers and a package, which obviously increases the number of moving pieces substantially (if you're on 11.2, you can use a compound trigger with three component triggers which simplifies things a bit by eliminating the need to use the package). That would look something like
CREATE OR REPLACE PACKAGE trigger_collections
AS
TYPE modified_property_tbl IS TABLE OF properties.property_id%type;
g_modified_properties modified_property_tbl;
END;
-- Initialize the collection in a before statement trigger just in case
-- there were values there from a prior run
CREATE OR REPLACE TRIGGER trg_initialize_mod_prop_coll
BEFORE INSERT OR UPDATE ON rooms
BEGIN
trigger_collections.g_modified_properties := trigger_collections.modified_property_tbl();
END;
-- Put the property_id of the modified row in the collection
CREATE OR REPLACE TRIGGER trg_populate_mod_prop_coll
AFTER INSERT OR UPDATE ON rooms
FOR EACH ROW
BEGIN
trigger_collections.g_modified_properties.extend();
trigger_collections.g_modified_properties( trigger_collections.g_modified_properties.count + 1 ) := :new.property_id;
END;
CREATE OR REPLACE TRIGGER trg_process_mod_prop_coll
AFTER INSERT OR UPDATE ON rooms
BEGIN
FOR p IN 1 .. trigger_collections.g_modified_properties.count
LOOP
IF prop_vacancy_query( trigger_collections.g_modified_properties(i) ) = 0
THEN
...
END;

Related

Is an insert and a corresponding trigger call an atomic process?

I have a table called LOCK and I want to ensure that not more than a single row with a given name and type WRITE exists. Though multiple rows with type READ and an equal name are allowed but only if there is no row with the same name and type WRITE.
create table "LOCK"
(
"LOCK_ID" NUMBER(19,0) NOT NULL,
"NAME" VARCHAR2(255 CHAR),
"TYPE" VARCHAR2(32 CHAR),
CONSTRAINT "SYS_LOCK_PK" PRIMARY KEY ("LOCK_ID")
);
Inserting a row has to be atomic, for instance no query with a following insert depending on the result of the query (because it could have changed meanwhile).
To ensure atomicy I created a trigger to check the initially mentioned condition (raising error on fail), which is occasionally ending up in various invalid states like two WRITE rows.
If inserts are executed sequentially the trigger works perfectly which leads to the assumption insert + trigger is no atomic process and if so, is there a safe way to solve my issue?
Here's the trigger:
create or replace trigger "LOCK_TRIGGER"
before insert on "LOCK"
referencing NEW AS NEW
for each row
declare
c integer := 0;
begin
select count(*) into c from "LOCK" where (:NEW.typ = 'WRITE' and name = :NEW.name) or (:NEW.typ = 'READ' and name = :NEW.name and typ = 'WRITE');
if (c > 0) then
raise_application_error(-20634, 'Nope!');
end if;
end;
Trigger doesn't help here for the multiuser environment. You need to serialize the access to the particular lock name. For this case I would go for the custom locks. The database package dbms_lock is used for this. You can create a function which does the following:
acquires custom lock for the incoming name - this lock should be created with an option that it is not released on commit/rollback
makes the validation in the table for the name
inserts the record if possible (if validation passed) and commits it
releases custom lock
returns the result (either OK or NOK)
Hope that helps.

PL/SQL table level trigger running after second update

I have this problem whith this trigger which is calling a procedure updating a table after updating a row in other table.The problem is that you have to update table STAVKARACUNA two times to table RACUN update but it uses old values. Here is a code of both:
Here is a code of aa procedure:
create or replace PROCEDURE ukupnaCenaRacun (SIF IN VARCHAR2) AS
SUMA float := 0;
suma2 float := 0;
Mesec NUMBER;
popust float :=0.1;
BEGIN
SELECT SUM(iznos) INTO SUMA
FROM STAVKARACUNA
WHERE SIF = SIFRARAC;
SELECT SUM(vredrobe*pdv) INTO SUMA2
FROM STAVKARACUNA
WHERE SIF = SIFRARAC;
SELECT EXTRACT (MONTH FROM DATUM) INTO Mesec FROM RACUN WHERE SIF=SIFRARAC;
IF(Mesec = 1) THEN
UPDATE RACUN
SET PDVIZNOS = SUMA2, ukupnozanaplatu = suma*(1-popust)
WHERE SIFRARAC=SIF;
END IF;
IF (MESEC != 1) THEN
UPDATE RACUN
SET PDVIZNOS = SUMA2, ukupnozanaplatu = suma
WHERE SIFRARAC=SIF;
END IF;
END;
Here is a trigger:
create or replace TRIGGER "UKUPNACENA_RACUN_UKUPNO"
AFTER INSERT OR UPDATE OR DELETE OF CENA,KOL,PDV ON STAVKARACUNA
DECLARE
SIF VARCHAR2(20) := PACKAGE_STAVKARACUNA.SIFRARAC;
BEGIN
PACKAGE_STAVKARACUNA.ISKLJUCI_TRIGER('FORBID_UPDATING');
ukupnaCenaRacun(SIF);
PACKAGE_STAVKARACUNA.UKLJUCI_TRIGER('FORBID_UPDATING');
END;
The problem is when a table STAVKARACUNA is updated, nothing happens with table RACUN, but next time table STAVKARACUNA is updated the data in table RACUN is updated but with old values.
Thank you very much.
Are you aware that a trigger for an event on a table should not directly access that table? The code is inside a DML event. The table is right in the middle of being altered in some say. So any query back to the same table could well attempt to read data that is in the process of being changed. It could try to read data that does not quite exist before a commit is performed or is one value now but will be a different value once a commit is performed. The table is mutating.
This goes for any code outside the trigger that the triggers calls. So the ukupnaCenaRacun procedure is executed in the context of the trigger. Yet it goes out and queries table STAVKARACUNA in two places (which can be placed in a single query but that is neither here nor there).
Since you're not getting a mutating table error, I can only assume that the update is not taking place until after the triggering event is committed but then you won't see the results until after that is committed sometime later -- like when the second update is committed.
That explanation actually sounds hollow to me as I have always thought that all activity performed by a trigger is committed or rolled back as part of one transaction. But that is the action you are describing.
It appears that SIF is a package variable defined in the package spec. Since everything in the procedure keys off that value and the trigger doesn't change the value, can't SUMA and SUMA2 also be defined as variables, values to be updated whenever SIF changes?

Compound trigger: collecting mutating rows into nested table

I have two tables in my project: accounts and transactions (one-to-many relationship). In every transaction I store the balance of the associated account (after the transaction is executed). Additionally in every transaction I store a value of the transaction.
So I needed a trigger fired when someone adds new transaction. It should check whether new account balance will be correct (old account balance + transaction value = new account balance stored in transaction).
So I was suggested, I should use a compound trigger which would:
in before each row section: save a row's PK (made of two columns) somewhere,
in after statement section: check whether all inserted transactions where correct.
Now I can't find anywhere how could I implement the first point.
What I already have:
CREATE OR REPLACE TRIGGER check_account_balance_is_valid
FOR INSERT
ON Transactions
COMPOUND TRIGGER
TYPE Modified_transactions_T IS TABLE OF Transactions%ROWTYPE;
Modified_transactions Modified_transactions_T;
BEFORE STATEMENT IS BEGIN
Modified_transactions := Modified_transactions_T();
END BEFORE STATEMENT;
BEFORE EACH ROW IS BEGIN
Modified_transactions.extend;
Modified_transactions(Modified_transactions.last) := :NEW;
END BEFORE EACH ROW;
AFTER STATEMENT IS BEGIN
NULL; -- I will write something here later
END AFTER STATEMENT;
END check_account_balance_is_valid;
/
However, I got that:
Warning: execution completed with warning
11/58 PLS-00049: bad bind variable 'NEW'
Could someone tell me, how to fix it? Or maybe my whole "compound trigger" idea is wrong and you have better suggestions.
Update 1
Here is my ddl script: http://pastebin.com/MW0Eqf9J
Maybe try this one:
TYPE Modified_transactions_T IS TABLE OF ROWID;
Modified_transactions Modified_transactions_T;
BEFORE STATEMENT IS BEGIN
Modified_transactions := Modified_transactions_T();
END BEFORE STATEMENT;
BEFORE EACH ROW IS BEGIN
Modified_transactions.extend;
Modified_transactions(Modified_transactions.last) := :NEW.ROWID;
END BEFORE EACH ROW;
or this
TYPE PrimaryKeyRecType IS RECORD (
Col1 Transactions.PK_COL_1%TYPE, Col2 Transactions.PK_COL_2%TYPE);
TYPE Modified_transactions_T IS TABLE OF PrimaryKeyRecType;
...
Modified_transactions(Modified_transactions.last) := PrimaryKeyRecType(:NEW.PK_COL_1, :NEW.PK_COL_2);
Your immediate problem is that :new is not a real record so it is not of type Transactions%ROWTYPE. If you're really going to go down this path, you would generally want to declare a collection of the primary key of the table
TYPE Modified_transactions_T IS TABLE OF Transactions.Primary_Key%TYPE;
and then put just the primary key in the collection
BEFORE EACH ROW IS BEGIN
Modified_transactions.extend;
Modified_transactions(Modified_transactions.last) := :NEW.Primary_Key;
END BEFORE EACH ROW;
The fact that you are trying to work around a mutating table exception in the first place, however, almost always indicates that you have an underlying data modeling problem that you should really be solving. If you need to query other rows in the table in order to figure out what you want to do with the new rows, that's a pretty good indication that you have improperly normalized your data model and that one row has some dependency on another row in the same table rather than being an autonomous fact. Fixing the data model is almost always preferrable to working around the mutating table exception.

PL/SQL Triggers Library Infotainment System

I am trying to make a Library Infotainment System using PL/SQL. Before any of you speculate, yes it is a homework assignment but I've tried hard and asking a question here only after trying hard enough.
Basically, I have few tables, two of which are:
Issue(Bookid, borrowerid, issuedate, returndate) and
Borrower(borrowerid, name, status).
The status in Borrower table can be either 'student' or 'faculty'. I have to implement a restriction using trigger, that per student, I can issue only 2 books at any point of time and per faculty, 3 books at any point of time.
I am totally new to PL/SQL. It might be easy, and I have an idea of how to do it. This is the best I could do. Please help me in finding design/compiler errors.
CREATE OR REPLACE TRIGGER trg_maxbooks
AFTER INSERT ON ISSUE
FOR EACH ROW
DECLARE
BORROWERCOUNT INTEGER;
SORF VARCHAR2(20);
BEGIN
SELECT COUNT(*) INTO BORROWERCOUNT
FROM ISSUE
WHERE BORROWER_ID = :NEW.BORROWER_ID;
SELECT STATUS INTO SORF
FROM BORROWER
WHERE BORROWER_ID = :NEW.BORROWER_ID;
IF ((BORROWERCOUNT=2 AND SORF='STUDENT')
OR (BORROWERCOUNT=3 AND SORF='FACULTY')) THEN
ROLLBACK TRANSACTION;
END IF;
END;
Try something like this:
CREATE OR REPLACE TRIGGER TRG_MAXBOOKS
BEFORE INSERT
ON ISSUE
FOR EACH ROW
BEGIN
IF ( :NEW.BORROWERCOUNT > 2
AND :NEW.SORF = 'STUDENT' )
OR ( :NEW.BORROWERCOUNT > 3
AND :NEW.SORF = 'FACULTY' )
THEN
RAISE_APPLICATION_ERROR (
-20001,
'Cannot issue beyond the limit, retry as per the limit' );
END IF;
END;
/
There should not be a commit or rollback inside a trigger. The logical exception is equivalent to ROLLBACK
This is so ugly I can't believe you're being asked to do something like this. Triggers are one of the worst ways to implement business logic. They will often fail utterly when confronted with more than one user. They are also hard to debug because they have hard-to-anticipate side effects.
In your example for instance what happens if two people insert at the same time? (hint: they won't see the each other's modification until they both commit, nice way to generate corrupt data :)
Furthermore, as you are probably aware, you can't reference other rows of a table inside a row level trigger (this will raise a mutating error).
That being said, in your case you could use an extra column in Borrower to record the number of books being borrowed. You'll have to make sure that the trigger correctly updates this value. This will also take care of the multi-user problem since as you know only one session can update a single row at the same time. So only one person could update a borrower's count at the same time.
This should help you with the insert trigger (you'll also need a delete trigger and to be on the safe side an update trigger in case someone updates Issue.borrowerid):
CREATE OR REPLACE TRIGGER issue_borrower_trg
AFTER INSERT ON issue
FOR EACH ROW
DECLARE
l_total_borrowed NUMBER;
l_status borrower.status%type;
BEGIN
SELECT nvl(total_borrowed, 0) + 1, status
INTO l_total_borrowed, l_status
FROM borrower
WHERE borrower_id = :new.borrower_id
FOR UPDATE;
-- business rule
IF l_status = 'student' and l_total_borrowed >= 3
/* OR faculty */ THEN
raise_application_error(-20001, 'limit reached!');
END IF;
UPDATE borrower
SET total_borrowed = l_total_borrowed
WHERE borrower_id = :new.borrower_id;
END;
Update: the above approach won't even work in your case because you record the issue date/return date in the issue table so the number of books borrowed is not a constant over time. In that case I would go with a table-level POST-DML trigger. After each DML verify that every row in the table validates your business rules (it won't scale nicely though, for a solution that scales, see this post by Tom Kyte).

trigger after on insert, error table mutation upon inserting record

I have created a trigger that will update the Payment table (for bill) when a new record is inserted to the table Enrollment. My trigger is as follows:
CREATE OR REPLACE TRIGGER EnrollFee_trig
AFTER INSERT ON Enrollment
FOR EACH ROW
DECLARE
amount Payment.TotalPrice%TYPE;
id Payment.LearnerID%TYPE;
BEGIN
SELECT SUM(Price) into amount
FROM LearnerEnrollCourse_View
WHERE LearnerID = :NEW.LearnerID
AND Paid = 'N';
SELECT LearnerID into id
FROM Payment
WHERE LearnerID = :NEW.LearnerID
AND PaymentDate IS NULL;
IF SQL%FOUND THEN
UPDATE Payment
SET TotalPrice = amount
WHERE LearnerID = :new.LearnerID
AND PaymentDate IS NULL;
ELSE
INSERT INTO Payment VALUES
(PaymentID_Seq.nextval, :new.LearnerID, '', amount);
END IF;
END;
/
The trigger can be created successfully. But when inserting new record into Enrollment table, there is error saying ' table ENROLLMENT is mutating, trigger/function may not see it'. I want to know more specific about what problem causing this and how can I solve it.
Mutating table exceptions occur when we try to reference the triggering table in a query from within row-level trigger code. See more here
In this instance I suspect ( though I don't know as there is no definition for LearnerEnrollCourse_View ) the problem is caused by this statement :-
SELECT SUM(Price) into amount
FROM LearnerEnrollCourse_View
WHERE LearnerID = :NEW.LearnerID
AND Paid = 'N';
If the LearnerEnrollCourse_view view refers to the Enrollment table, you will get the mutating table error. There are a number of ways round it, moving your trigger code into a statement level trigger and holding the data in package variables is one workaround, in general though, I think using triggers is not the best way to do this, the more triggers you have, the more likely you are to run into this and other problems. Instead, I would have an api package for the enrollment table, and move the trigger code into there.
Good discussion of triggers here