How to prevent reciprocal primary key - foreign key relationships? - plsql

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.

Related

How to begin an Oracle transaction in Navicat

I'm trying to have a transaction for simple queries written in Navicat Premium, when using a Oracle database:
ALTER TABLE "APPLICATIONS_EXTENSION"
ADD CONSTRAINT "APPLICATIONS_EXTENSION_F01" FOREIGN KEY ("APPLICATION_ID")
REFERENCES "MYDB"."APPLICATIONS" ("APPLICATION_ID")
DEFERRABLE INITIALLY DEFERRED;
UPDATE "APPLICATIONS_EXTENSION" SET "APPLICATION_ID" = 100000; -- Fake ID
UPDATE "APPLICATIONS_EXTENSION" SET "APPLICATION_ID" = 1; -- Good ID
COMMIT;
This should execute fine, since, at COMMIT time, the deferred foreign-key constraint is satisfied.
However, Navicat seems to have auto-commit active, so, right-after the second statement an error is thrown:
> ORA-02091: transaction rolled back
ORA-02291: integrity constraint (MYDB.APPLICATIONS_EXTENSION_F01) violated - parent key not found
I found no way to have auto-commit off, nor an explicit way to start a transaction.
The "Automatically start a transaction" option in the "Records" tab of Navicat Options seems to just apply to table views (as its tab name seemed to indicate).

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

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

Alternative to using subquery inside CHECK constraint?

I am trying to build a simple hotel room check-in database as a learning exercise.
CREATE TABLE HotelReservations
(
roomNum INTEGER NOT NULL,
arrival DATE NOT NULL,
departure DATE NOT NULL,
guestName CHAR(30) NOT NULL,
CONSTRAINT timeTraveler CHECK (arrival < departure) /* stops time travelers*/
/* CONSTRAINT multipleReservations CHECK (my question is about this) */
PRIMARY KEY (roomNum, arrival)
);
I am having trouble specifying a constraint that doesn't allow inserting a new reservation for a room that has not yet been vacated. For example (below), guest 'B' checks into room 123 before 'A' checks out.
INSERT INTO HotelStays(roomNum, arrival, departure, guestName)
VALUES
(123, date("2017-02-02"), date("2017-02-06"), 'A'),
(123, date("2017-02-04"), date("2017-02-08"), 'B');
This shouldn't be allowed but I am unsure how to write this constraint. My first attempt was to write a subquery in check, but I had trouble figuring out the proper subquery because I don't know how to access the 'roomNum' value of a new insert to perform the subquery with. I then also figured out that most SQL systems don't even allow subquerying inside of check.
So how am I supposed to write this constraint? I read some about triggers which seem like it might solve this problem, but is that really the only way to do it? Or am I just dense and missing an obvious way to write the constraint?
The documentation indeed says:
The expression of a CHECK constraint may not contain a subquery.
While it would be possible to create a user-defined function that goes back to the database and queries the table, the only reasonable way to implement this constraint is with a trigger.
There is a special mechanism to access the new row inside the trigger:
Both the WHEN clause and the trigger actions may access elements of the row being inserted, deleted or updated using references of the form "NEW.column-name" and "OLD.column-name", where column-name is the name of a column from the table that the trigger is associated with.
CREATE TRIGGER multiple_reservations_check
BEFORE INSERT ON HotelReservations
BEGIN
SELECT RAISE(FAIL, "reservations overlap")
FROM HotelReservations
WHERE roomNum = NEW.roomNum
AND departure > NEW.arrival
AND arrival < NEW.departure;
END;

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

Handling Constraint SqlException in Asp.net

Suppose I have a user table that creates strong relationships (Enforce Foreign Key Constraint) with many additional tables. Such orders table ..
If we try to delete a user with some orders then SqlException will arise.. How can I catch this exception and treat it properly?
Is this strategy at all?
1) first try the delete action if an exception Occur handel it?
2) Or maybe before the delete action using code adapted to ensure that offspring records throughout the database and alert according to .. This piece of work ...
So how to do it?
--Edit:
The goal is not to delete the records from the db! the goal is to inform the user that this record has referencing records. do i need to let sql to execute the delete command and try to catch SqlException? And if so, how to detect that is REFERENCE constraint SqlException?
Or - should I need to write some code that will detect if there are referencing records before the delete command. The last approach give me more but its a lot of pain to implement this kind of verification to each entity..
Thanks
Do you even really want to actually delete User records? Instead I'd suggest having a "deleted" flag in your database, so when you "delete" a user through the UI, all it does is update that record to set the flag to 1. After all, you wouldn't want to delete users that had orders etc.
Then, you just need to support this flag in the appropriate areas (i.e. don't show "deleted" users in the UI).
Edit:
"...but just for the concept, assume that i do want delete the user how do i do that?"
You'd need to delete the records from the other tables that reference that user first, before deleting the user record (i.e. delete the referencing records first then delete the referenced records). But to me that doesn't make sense as you would be deleting e.g. order data.
Edit 2:
"And if so, how to detect that is REFERENCE constraint SqlException?"
To detect this specific error, you can just check the SqlException.Number - I think for this error, you need to check for 547 (this is the error number on SQL 2005). Alternatively, if using SQL 2005 and above, you could handle this error entirely within SQL using the TRY...CATCH support:
BEGIN TRY
DELETE FROM User WHERE UserId = #MyUserId
END TRY
BEGIN CATCH
IF (ERROR_NUMBER() = 547)
BEGIN
-- Foreign key constraint violation. Handle as you wish
END
END CATCH
However, I'd personally perform a pre-check like you suggested though, to save the exception. It's easily done using an EXISTS check like this:
IF NOT EXISTS(SELECT * FROM [Orders] WHERE UserId=#YourUserId)
BEGIN
-- User is not referenced
END
If there are more tables that reference a User, then you'd need to also include those in the check.

Resources