Compound trigger: collecting mutating rows into nested table - sql

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.

Related

How to successfully reference another table before insert with a trigger

I'm trying to create a trigger to validate if a new entry in the table registraties (registrations) contains a valid MNR (employee number) but I'm getting stuck on the part where I'm referencing the table medewerkers (employees).
Could someone help me out?
CREATE OR REPLACE TRIGGER t_MNRcontrole
BEFORE INSERT OR UPDATE
ON registraties
DECLARE
MNR_medewerkers number (SELECT MNR FROM MEDEWERKERS);
FOR EACH ROW
BEGIN
IF :new.MNR <> MNR_medewerkers
THEN raise_application_error(-20111, 'Medewerker niet herkend!');
END IF;
END;
Error message received is
ORA-24344: success with compilation error
The PL/SQL assignment operator is :=, or select x into y from z to populate from a SQL query.
FOR EACH ROW is part of the trigger spec, not the PL/SQL code.
If :new.mnr is not present in the parent table, you will get a no_data_found exception, not a mismatched variable.
It's good practice for error messages to include details of what failed.
In programming, we use indentation to indicate code structure.
A fixed version would be something like:
create or replace trigger trg_mnrcontrole
before insert or update on registraties
for each row
declare
mnr_medewerkers medewerkers.mnr%type;
begin
select mw.mnr into mnr_medewerkers
from medewerkers mw
where mw.mnr = :new.mnr;
exception
when no_data_found then
raise_application_error(-20111, 'Medewerker '||:new.mnr||' niet herkend!');
end;
However, we can implement this kind of check better using a foreign key constraint, for example:
alter table registraties add constraint registraties_mw_fk
foreign key (mnr) references medewerkers.mnr;
MNR_medewerkers number (SELECT MNR FROM MEDEWERKERS);
will always fail because its not a NUMBER, unless your table happens to only have one single entry and even then I am not sure PLSQL will allow it to pass.
The more standard case for this would be to first declare the number, then in the codeblock you do a SELECT INTO along with a WHERE clause where you make sure to only pick one specific row from the table. Then you can compare that number with the new one.
If however you are not trying to compare to one specific row, but are instead checking if the entry exists in that table.
BEGIN
SELECT 1
INTO m_variable
FROM table
WHERE MNR = :new.MNR;
EXCEPTION
WHEN TOO_MANY_ROWS THEN
m_variable = 1;
WHEN OTHERS THEN
m_variable = 0;
END;
Declare the m_variable beforehand, and then check if its 0 then report the error.
The too_many_rows is in case there is more than one row in the table with this MNR, and the OTHERS is there for the NO_DATA_FOUND, but I use OTHERS to handle everything else that could happen but probably wont.
Btw this is a code block to be included within the main code block, so between your BEGIN and IF, then just change the IF to check if the variable is 0.

Oracle trigger error ORA-04091

I get an error (ORA-04091: table DBPROJEKT_AKTIENDEPOT.AKTIE is mutating, trigger/function may not see it) when executing my trigger:
CREATE OR REPLACE TRIGGER Aktien_Bilanz_Berechnung
AFTER
INSERT OR UPDATE OF TAGESKURS
OR INSERT OR UPDATE OF WERT_BEIM_EINKAUF
ON AKTIE
FOR EACH ROW
DECLARE
bfr number;
Begin
bfr := :new.TAGESKURS - :new.WERT_BEIM_EINKAUF;
UPDATE AKTIE
SET BILANZ = TAGESKURS - WERT_BEIM_EINKAUF;
IF bfr < -50
THEN
DBMS_OUTPUT.PUT_LINE('ACHTUNG: The value (Nr: '||:new.AKTIEN_NR||') is very low!');
END IF;
END;
I want to check the value "BILANZ" after calculating it, wether it is under -50.
Do you have any idea why this error is thrown?
Thanks for any help!
There are several issues here:
Oracle does not allow you to perform a SELECT/INSERT/UPDATE/DELETE against a table within a row trigger defined on that table or any code called from such a trigger, which is why an error occurred at run time. There are ways to work around this - for example, you can read my answers to this question and this question - but in general you will have to avoid accessing the table on which a row trigger is defined from within the trigger.
The calculation which is being performed in this trigger is what is referred to as business logic and should not be performed in a trigger. Putting logic such as this in a trigger, no matter how convenient it may seem to be, will end up being very confusing to anyone who has to maintain this code because the value of BILANZ is changed where someone who is reading the application code's INSERT or UPDATE statement can't see it. This calculation should be performed in the INSERT or UPDATE statement, not in a trigger. It considered good practice to define a procedure to perform INSERT/UPDATE/DELETE operations on a table so that all such calculations can be captured in one place, instead of being spread out throughout your code base.
Within a BEFORE ROW trigger you can modify the values of the fields in the :NEW row variable to change values before they're written to the database. There are times that this is acceptable, such as when setting columns which track when and by whom a row was last changed, but in general it's considered a bad idea.
Best of luck.
You are modifying the table with the trigger. Use a before update trigger:
CREATE OR REPLACE TRIGGER Aktien_Bilanz_Berechnung
BEFORE INSERT OR UPDATE OF TAGESKURS OR INSERT OR UPDATE OF WERT_BEIM_EINKAUF
ON AKTIE
FOR EACH ROW
DECLARE
v_bfr number;
BEGIN
v_bfr := :new.TAGESKURS - :new.WERT_BEIM_EINKAUF;
:new.BILANZ := v_bfr;
IF v_bfr < -50 THEN
Raise_Application_Error(-20456,'ACHTUNG: The value (Nr: '|| :new.AKTIEN_NR || ') is very low!');
END IF;
END;

ROLLBACK event triggers in postgresql

I know it may sound odd but is there any way I can call my trigger on ROLLBACK event in a table? I was going through postgresql triggers documentation, there are events only for CREATE, UPDATE, DELETE and INSERT on table.
My requirement is on transaction ROLLBACK my trigger will select last_id from a table and reset table sequence with value = last_id + 1; in short I want to preserve sequence values on rollback.
Any kind of ideas and feed back will be appreciated guys!
You can't use a sequence for this. You need a single serialization point through which all inserts have to go - otherwise the "gapless" attribute can not be guaranteed. You also need to make sure that no rows will ever be deleted from that table.
The serialization also means that only a single transaction can insert rows into that table - all other inserts have to wait until the "previous" insert has been committed or rolled back.
One pattern how this can be implemented is to have a table where the the "sequence" numbers are stored. Let's assume we need this for invoice numbers which have to be gapless for legal reasons.
So we first create the table to hold the "current value":
create table slow_sequence
(
seq_name varchar(100) not null primary key,
current_value integer not null default 0
);
-- create a "sequence" for invoices
insert into slow_sequence values ('invoice');
Now we need a function that will generate the next number but that guarantees that no two transactions can obtain the next number at the same time.
create or replace function next_number(p_seq_name text)
returns integer
as
$$
update slow_sequence
set current_value = current_value + 1
where seq_name = p_seq_name
returning current_value;
$$
language sql;
The function will increment the counter and return the incremented value as a result. Due to the update the row for the sequence is now locked and no other transaction can update that value. If the calling transaction is rolled back, so is the update to the sequence counter. If it is committed, the new value is persisted.
To ensure that every transaction uses the function, a trigger should be created.
Create the table in question:
create table invoice
(
invoice_number integer not null primary key,
customer_id integer not null,
due_date date not null
);
Now create the trigger function and the trigger:
create or replace function f_invoice_trigger()
returns trigger
as
$$
begin
-- the number is assigned unconditionally so that this can't
-- be prevented by supplying a specific number
new.invoice_number := next_number('invoice');
return new;
end;
$$
language plpgsql;
create trigger invoice_trigger
before insert on invoice
for each row
execute procedure f_invoice_trigger();
Now if one transaction does this:
insert into invoice (customer_id, due_date)
values (42, date '2015-12-01');
The new number is generated. A second transaction then needs to wait until the first insert is committed or rolled back.
As I said: this solution is not scalable. Not at all. It will slow down your application massively if there are a lot of inserts into that table. But you can't have both: a scalable and correct implementation of a gapless sequence.
I'm also pretty sure that there are edge case that are not covered by the above code. So it's pretty likely that you can still wind up with gaps.

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

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;

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