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).
Related
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;
For an assignment I need to write an Oracle SQL trigger that prevents the StdBalance column in the STUDENT table from exceeding 500. Normally I would just use a check constraint to enforce this however I am being forced to write a trigger for it. As expected Oracle is throwing a mutating error due to me using SELECT in an update trigger and will not let me update the StdBalance value with anything. Does anyone have any idea how I could work around this? Here is the code for the trigger.
CREATE OR REPLACE TRIGGER Balance_Trigger
BEFORE UPDATE ON STUDENT
FOR EACH ROW
DECLARE
Current_Balance NUMBER;
BEGIN
SELECT :new.StdBalance
INTO Current_Balance
FROM STUDENT
WHERE :new.stdID = StdID;
IF Current_Balance > 500
THEN Raise_Application_error(-20007, 'You cannot exceed an unpaid balance of $500');
end if;
end;
/
show error;
Just use :
Current_Balance := :new.StdBalance;
Instead of
SELECT :new.StdBalance
INTO Current_Balance
FROM STUDENT
WHERE :new.stdID = StdID;
to suppress the mutating error.
P.S. Even if such assignments are used in these cases, as David Faber warned, there's no need to return a value for Current_Balance, :new.StdBalance may be used directly in comparison for IF :new.StdBalance > 500 ....
I create the trigger A1 so that an article with a certain type, that is 'Bert' cannot be added more than once and it can have only 1 in the stock.
However, although i create the trigger, i can still add an article with the type 'Bert'. Somehow, the count returns '0' but when i run the same sql statement, it returns the correct number. It also starts counting properly if I drop the trigger and re-add it. Any ideas what might be going wrong?
TRIGGER A1 BEFORE INSERT ON mytable
FOR EACH ROW
DECLARE
l_count NUMBER;
PRAGMA AUTONOMOUS_TRANSACTION;
BEGIN
SELECT COUNT(*) INTO l_count FROM mytable WHERE article = :new.article;
dbms_output.put_line('Count: ' || l_count);
IF l_count >0 THEN
IF(:new.TYPEB = 'Bert') THEN
dbms_output.put_line('article already exists!');
ROLLBACK;
END IF;
ELSIF (:new.TYPEB = 'Bert' AND :new.stock_count>1) THEN
dbms_output.put_line('stock cannot have more than 1 of this article with type Bert');
ROLLBACK;
END IF;
END;
This is the insert statement I use:
INSERT INTO mytable VALUES('Chip',1,9,1,'Bert');
A couple of points. First, you are misusing the autonomous transaction pragma. It is meant for separate transactions you need to commit or rollback independently of the main transaction. You are using it to rollback the main transaction -- and you never commit if there is no error.
And those "unforeseen consequences" someone mentioned? One of them is that your count always returns 0. So remove the pragma both because it is being misused and so the count will return a proper value.
Another thing is don't have commits or rollbacks within triggers. Raise an error and let the controlling code do what it needs to do. I know the rollbacks were because of the pragma. Just don't forget to remove them when you remove the pragma.
The following trigger works for me:
CREATE OR REPLACE TRIGGER trg_mytable_biu
BEFORE INSERT OR UPDATE ON mytable
FOR EACH ROW
WHEN (NEW.TYPEB = 'Bert') -- Don't even execute unless this is Bert
DECLARE
L_COUNT NUMBER;
BEGIN
SELECT COUNT(*) INTO L_COUNT
FROM MYTABLE
WHERE ARTICLE = :NEW.ARTICLE
AND TYPEB = :NEW.TYPEB;
IF L_COUNT > 0 THEN
RAISE_APPLICATION_ERROR( -20001, 'Bert already exists!' );
ELSIF :NEW.STOCK_COUNT > 1 THEN
RAISE_APPLICATION_ERROR( -20001, 'Can''t insert more than one Bert!' );
END IF;
END;
However, it's not a good idea for a trigger on a table to separately access that table. Usually the system won't even allow it -- this trigger won't execute at all if changed to "after". If it is allowed to execute, one can never be sure of the results obtained -- as you already found out. Actually, I'm a little surprised the trigger above works. I would feel uneasy using it in a real database.
The best option when a trigger must access the target table is to hide the table behind a view and write an "instead of" trigger on the view. That trigger can access the table all it wants.
You need to do an AFTER trigger, not a BEFORE trigger. Doing a count(*) "BEFORE" the insert occurs results in zero rows because the data hasn't been inserted yet.
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.
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