Table mutating when "after insert" trigger fired, i don't know what to do - plsql

I want to increment the samenameamount column in my table employees after inserting the similar name
I have a such trigger:
create or replace trigger countNumbEmp after insert on employees for each row
declare
var_count_names number;--var for count same names
begin
select count(emp_name) into var_count_names from employees where emp_name = :new.emp_name;
update employees set samenameamount = var_count_names where emp_name = :new.emp_name;
end;
My table is
- Name Null? Type
-------------- ----- ------------
EMP_ID NUMBER
EMP_NAME VARCHAR2(20)
SAMENAMEAMOUNT NUMBER
After inserting i get this message:
- Error report -
ORA-04091: table DASTAN.EMPLOYEES is mutating, trigger/function may not see it
ORA-06512: at "DASTAN.COUNTNUMBEMP", line 4
ORA-04088: error during execution of trigger 'DASTAN.COUNTNUMBEMP'

Here's a brief list of things that you should not do in a trigger (source link here, my personal favorite page):
On insert triggers have no :OLD values.
On delete triggers have no :NEW values.
Triggers do not commit transactions. If a transaction is rolled back,
the data changed by the trigger is also rolled back.
Commits, rollbacks and save points are not allowed in the trigger
body. A commit/rollback affects the entire transaction, it is all or
none.
Unhandled exceptions in the trigger will cause a rollback of the
entire transaction, not just the trigger.
If more than one trigger is defined on an event, the order in which
they fire is not defined. If the triggers must fire in order, you
must create one trigger that executes all the actions in the required
order.
A trigger can cause other events to execute triggers. A trigger can
not change a table that it has read from. This is the mutating table
error issue.
I am intentionally not pasting the last part of that link, because it mentions things like "how to commit in a trigger" etc., as it's a bad practice, even though it IS in fact a workaround. If you can't avoid having that, however, there's something wrong with the design of the solution or the implementation and this needs to be solved first.
Cheers

Related

How To Create a PL/SQL Trigger That Detects an Inserted or Updated Row and updates a Record in a Different Table?

I am creating a book tracking database for myself that holds information about my books and allows me to keep track of who is borrowing them. I am trying to create a trigger on my Checkouts table that runs if a record is added or updated that will determine if a checkout data has been entered or if a checkin date has been entered and change the "available" field in my Books table to "Y" or "N".
I have created a trigger called "update_book_availablility" on my Checkouts table but I keep getting this error:
"PLS-00103: Encountered the symbol 'end-of-file' when expecting one of the following: ( begin case declare and exception exit for goto if loop mod null pragma raise return select update while with <<continue close current delete fetch lock insert open rollback savepoint set sql execute commit forall merge standard pipe purge json_object
Errors: check compiler log"
Here is my trigger code:
CREATE OR REPLACE NONEDITIONABLE TRIGGER "UPDATE_BOOK_AVAILABILITY"
AFTER INSERT OR UPDATE OF ISBN, PersonID, checkout_date, checkin_date
ON Checkouts
FOR EACH ROW
BEGIN
IF :NEW.checkout_date = NULL
THEN
UPDATE Book
SET available = 'N'
WHERE ISBN IN (SELECT :NEW.ISBN FROM Checkouts);
END IF;
END;
Here is an image of my ERD:
ERD
I have been looking into and double checking my trigger syntax, If condition syntax, subquery syntax, and googling this error but have found nothing that has helped. I am new to PL/SQL and would appreciate any help in understanding what I have done wrong or missed.
PLS-00103: Encountered the symbol end-of-file error is SYNTAX ERROR
Copied your trigger and adjusted it to one of my test tables - it works. I removed NONEDITIONABLE and changed trigger table name as well as column names and table/column beeing updated by trigger.
To Do:
Check your syntax again or write the trigger from scratch once more
"...WHERE ISBN IN (SELECT :NEW.ISBN FROM Checkouts)..." selects one fixed value (FOR EACH ROW) :NEW.ISBN of triggering table, better ->> "... WHERE ISBN = :NEW.ISBN ..."
Prety sure that you don't need NONEDITIONABLE trigger for your books tracking app...
Regards...

Can't delete from a table inside a trigger

I'm building this DB about the University for one of my course classes and I'm trying to create a trigger that doesn't allow for a professor to be under 21yo.
I have a Person class and then a Professor subclass.
What I want to happen is, you create a Person object, then a Professor object using that Person object's id, but, if the Person is under 21yo, delete this Professor object, then delete the Person object.
Everything works fine up until the "delete the Person object" part where this doesn't happen and I'm not sure why. Any help?
This is the sqlite code I have:
AFTER INSERT ON Professor
FOR EACH ROW
WHEN strftime('%J', 'now') - strftime('%J', (SELECT dateOfBirth from Person WHERE personId = NEW.personId)) < 7665 -- 21 years in days
BEGIN
SELECT RAISE(ROLLBACK, 'Professor cant be under 21');
DELETE FROM Person WHERE (personId= new.personId);
END;```
One common issue is that there many not be a current transaction scope to rollback to, which would result in this error:
Error: cannot rollback - no transaction is active
If that occurs, then the trigger execution will be aborted and the delete never executed.
If ROLLBACK does succeed, then this creates a paradox, by rolling back to before the trigger was executed in a strictly ACID environment it would not be valid to continue executing the rest of this trigger, because the INSERT never actually occurred. To avoid this state of ambiguity, any call to RAISE() that is NOT IGNORE will abort the processing of the trigger.
CREATE TRIGGER - The RAISE()
When one of RAISE(ROLLBACK,...), RAISE(ABORT,...) or RAISE(FAIL,...) is called during trigger-program execution, the specified ON CONFLICT processing is performed and the current query terminates. An error code of SQLITE_CONSTRAINT is returned to the application, along with the specified error message.
NOTE: This behaviour is different to some other RDBMS, for instance see this explanation on MS SQL Server where execution will specifically continue in the trigger.
As OP does not provide calling code that demonstrates the scenario it is worth mentioning that in SQLite on RAISE(ROLLBACK,)
If no transaction is active (other than the implied transaction that is created on every command) then the ROLLBACK resolution algorithm works the same as the ABORT algorithm.
Generally, if you wanted to Create a Person and then a Professor as a single operation, you would Create a Stored Procedure that would validate the inputs first, preventing the original insert at the start.
To maintain referential integrity, even if an SP is used, you could still add a check constraint on the Professor record or raise an ABORT from a BEFORE trigger to prevent the INSERT from occurring in the first place:
BEFORE INSERT ON Professor
FOR EACH ROW
WHEN strftime('%J', 'now') - strftime('%J', (SELECT dateOfBirth from Person WHERE personId = NEW.personId)) < 7665 -- 21 years in days
BEGIN
SELECT RAISE(ABORT, 'Professor can''t be under 21');
END
This way it is up to the calling process to manage how to handle the error. The ABORT can be caught in the calling logic and would effectively result in rolling back the outer transaction, but the point is that the calling logic should handle negative side effects. As a general rule triggers that cascade logic should only perform positive side effects, that is to say they should only affect data if the inserted row succeeds. In this case we are rolling back the insert, so it becomes hard to identify why the Person would be deleted.

How to prevent reciprocal primary key - foreign key relationships?

Assume an employees table like the one in Oracle's HR sample schema. It includes the following:
CREATE TABLE employees
(
employee_id NUMBER(6) NOT NULL PRIMARY KEY,
...
manager_id NUMBER(6)
);
ALTER TABLE employees
ADD CONSTRAINT employee_manager_fk
FOREIGN KEY (manager_id)
REFERENCES employees(employee_id);
I want to prevent a reciprocal relationship: if the employee with id 110 reports to the employee with id 110, I don't want to allow employee 110 to report to employee 100. I want to prevent the following:
employee_id manager_id
100 110
110 100
I don't think this is doable with a constraint, because it would require a subquery. So, I think a trigger is necessary. This is only a concern on an UPDATE as it's impossible to create this situation with a DELETE or INSERT. I've created the following trigger, which does the job:
CREATE OR REPLACE TRIGGER employees_bu_trigger
BEFORE
UPDATE OF manager_id
ON employees
FOR EACH ROW
DECLARE
l_managers_manager_id employees.manager_id%TYPE;
reciprocal_managers EXCEPTION;
-- This prevents ORA-04091: table C##HR.EMPLOYEES
-- is mutating, trigger/function may not see it
PRAGMA AUTONOMOUS_TRANSACTION;
BEGIN
SELECT manager_id
INTO l_managers_manager_id
FROM employees
WHERE employee_id = :new.manager_id;
IF l_managers_manager_id = :new.employee_id THEN
RAISE reciprocal_managers;
END IF;
EXCEPTION
WHEN reciprocal_managers THEN
-- re-raise the error to be caught by calling code
RAISE_APPLICATION_ERROR(-20102,
'Employees cannot manage each other.');
END;
But I'm worried about the PRAGMA AUTONOMOUS_TRANSACTION; line. That's needed to prevent the ORA-04091 error, but it makes me think there might be some further underlying problem here.
Is this trigger okay or will it have unforeseen negative side effects?
The danger of the PRAGMA AUTONOMOUS_TRANSACTION is that it runs in a separate transaction. So if your active transaction just added a entry and then before committing you do the update, this trigger will not see the inserted record and will allow records that violate your rule. This problem also occurs with other concurrent users (i.e. if user a makes an update in a transaction and user b makes an update the check will pass for both of them, but once they both commit the error state can exist even though both checks passed.)
Also while the example provided does not have this problem (as Oracle's documentation states that A writer never blocks a reader.), there is a danger of deadlocks if you do any writes in the trigger.
It may be possible to refactor your check to be a AFTER trigger without the FOR EACH ROW.
This will eliminate the danger of deadlocks, and of the test passing in the single user case. BUT it will not eliminate the multiuser issue.
Jon Heller's comment on multilevel loops may also be relevant to our specific problem, but any changes to the checks do not impact the issues you were specifically asking about.

Order of execution of trigger and statements in Oracle stored procedure

Below are my table structures :
Table -Customer
CustomerID Blacklisted Customer Name
101 Y ABC
102 Y DEF
Table -Blacklist
CustomerID BlacklistID Customer Name
101 1011 ABC
102 1012 DEF
Table -Reason
BlacklistID ReasonID Reason Code
1012 02 Rcode2
Main table "Customer" is to store customer information.There is a trigger after update on table "Customer" to insert record in table "Blacklist" if somebody updates the blacklisted as Y in customer table.
We consider the customer as blacklisted if ,
Blacklisted column in Customer table as value 'Y' and.
There are records present for customer in Blacklist and Reason table
Now my requirement is to blacklist the customer from backend.For this i am writing stored procedure with below queries:
Update customer set blacklisted ='Y' where customerid='102';
select BlacklistID into var_id from blacklist where customerid='102';
Insert into reason(BlacklistID,ReasonID,ReasonCode)values(var_ id,111,'RCODE1');
Now to insert entry in Reason table(step-3),i need BlacklistID which is a foreign key and i will get the value of BlacklistID once the trigger on customer table gets exceuted.So my confusion is, can i assume the trigger on update of 'Customer' table will always get excuted before the cntrl reaches my INSERT INTO reason(step-3) statement. Please suggest.
If you need to be certain about the order of trigger execution, you can specify this order when creating the trigger.
This is done with the FOLLOWS ... and PRECEEDS ... options of the create trigger statement:
More details in the manual: http://docs.oracle.com/cd/E11882_01/appdev.112/e25519/create_trigger.htm#CJAEJAFB
FOLLOWS | PRECEDES
Specifies the relative firing of triggers that have the same timing point. It is especially useful when creating crossedition triggers, which must fire in a specific order to achieve their purpose.
Use FOLLOWS to indicate that the trigger being created must fire after the specified triggers. You can specify FOLLOWS for a conventional trigger or for a forward crossedition trigger.
Use PRECEDES to indicate that the trigger being created must fire before the specified triggers. You can specify PRECEDES only for a reverse crossedition trigger.
Yes. Triggers are part of the statement. Although you cannot be fully certain *) of the order in which multiple triggers in the same statement are executed, you can be certain that they al are done when the statement itself is done. So by the time of step 2, all update triggers of step one have fired.
*) Actually, the default order is:
Statement level before triggers
Row level before triggers
Row level after triggers
Statement level after triggers
But if you have, say, two row level before trigger, by default you cannot be certain in which order those two are executed. But I learned from the comments that in Oracle 11, you can actually specify the order to cover even those cases.
I don't see a need for all these tables, and therefore no need for a trigger.
Why do you not just use a blacklist reason code in the customer table? The purpose of the blacklist table is unclear as it seems to add no data and just repeats data from the customer table with an additional id column?
Or if you need multiple reason codes then just use a blacklist_reason table that references the customer id and a reason code -- I don't think you even need the blacklisted column in the customer table.

trigger for updating a value

I am a newbie in PLSQL and I would like to create a trigger that checks first if there is a record in a table before making an update.
The code I got so far is:
CREATE OR REPLACE TRIGGER table_bu
BEFORE UPDATE ON employee
FOR EACH ROW
DECLARE
v_employee_id:=employee.employee_ID%TYPE;
BEGIN
SELECT employee_id INTO v_employee_id FROM employee;
EXCEPTION
WHEN NO_DATA_FOUND THEN
RAISE_APPLICATION_ERROR (-20001,'data not found');
END;
How I can create a trigger that checks up if a record exists in the table and if it does not exists does not allow the update.
My table estructure is:
employee_id NUMBER
employee_name VARCHAR(20)
employee_salary NUMBER
...
Thanks
You are on a wrong way. The trigger as it is will throw runtime 'Mutating table' error even after fixing syntax error - you missed semicolon after raise_application_error(also it should take 2 arguments, not one). Correct syntax :
EXCEPTION
WHEN NO_DATA_FOUND THEN
RAISE_APPLICATION_ERROR (-20001, 'data not found'); -- 1st parameter -error code
Update
As far as I understand the updated version of the question, you want to show error if record doesn't exist. The problem with row level trigger approach is that it won't be executed if nothing is found due to condition in WHERE. The simplest way is to check number of rows affected on client side and raise an error there. Or you can write a procedure that checks sql%rowcount after executing desired update, and then throw an exception if it's 0.
If you prefer to do in a hard way, you can create package variable which of type employee.employee_ID%TYPE, before update statement level trigger that resets variable (say set it to null), after update row level trigger that sets this variable to NEW.employee_ID, and after update statement level trigger that throws an exception if the variable is null. Note: this will properly work for individual updates only.
"How I can create a trigger that checks up if a record exists in the table and if it does not exists does not allow the update."
There is really only one practical way to do this - use a referential constraint (foreign key).

Resources