Oracle before delete trigger - sql

I have two tables
army
name VARCHAR(50) PRIMARY KEY
number_of_soliders INTEGER
soldier
soldier_id INTEGER PRIMARY KEY
army_name VARCHAR(50) REFERENCES army(name)
Now, I need to create trigger, which would work as ON DELETE CASCADE. The problem is, that I have already existing trigger for soldier, which automatically updates number_of_soldiers for his army and if i just put there ON DELETE CASCADE, it will result in a error, because when I delete the army, the trigger can't reference the number_of_soliders back.
So the task is to create trigger, which will delete all soldiers of the army before the army itself is deleted. I have tried:
CREATE OR REPLACE TRIGGER TRG_DELETE_SOLDIER
BEFORE DELETE ON army
FOR EACH ROW
BEGIN
DELETE FROM soldier WHERE army_name = :old.name;
END;
Hovewer, this results in an error table is mutating, trigger may not see it on table operates, M:N table which references Soldier ID (ON DELETE CASCADE) and Weapon ID. However, the trigger does not touch this table, nor the second one for updating number_of_soldiers.
Any ideas what could be wrong?

May I suggest to use on delete cascade with your foreign keys on your tables instead of solving this with a trigger?
alter table soldier
add constraint fk_soldier
foreign key (army_name)
references army(name)
on delete cascade
;
If you already use on delete cascade on this constraint, then maybe you forgot it on the weapon_id, which you also mention in your question.
NOTE 1: please consider using a numerical primary key for your army table and the foreign key. You are up to big trouble if you want to change the army name and need to update all child tables as well.
NOTE 2: you don't have to store the number of soldiers in your army table because you can let the database calculate that. Do you have a specific reason to store it redundantly?

Storing and maintaining number_of_soldiers in the parent table is awkward and can break if multiple sessions insert/delete soldiers for the same army at the same time. If you don't do that then you don't need soldier-level triggers to update the army table, and can go back to ON CASCADE DELETE without a trigger on the army table either. The number of soldiers for each army can be found by joining the tables:
select a.name, count(s.soldier_id) as number_of_soldiers
from army a
left join soldier s on s.army_name = a.name
group by a.name;
db<>fiddle demo. (That query can of course be stored as a view; which could be materialized if necessary.)
But for the sake argument, if you are stuck with the table column maintenance and need the trigger to clean up, an option is to selectively bypass the soldier-level trigger, using a when clause and an environment context value.
For example, you could add calls to dbms_application_info.set_client_info (or module, or action - whatever make most sense), with a value that makes sense for you to indicate why the soldier-level trigger should not fire; something like (with a not-very-helpful value):
CREATE OR REPLACE TRIGGER TRG_DELETE_SOLDIER
BEFORE DELETE ON army
FOR EACH ROW
BEGIN
DBMS_APPLICATION_INFO.SET_CLIENT_INFO('CASCADING');
DELETE FROM soldier WHERE army_name = :old.name;
DBMS_APPLICATION_INFO.SET_CLIENT_INFO(null);
END;
/
Then in the soldier-level trigger test for that value:
CREATE OR REPLACE TRIGGER trg_decrement_army
BEFORE DELETE ON soldier
FOR EACH ROW
WHEN (sys_context('userenv', 'client_info') is null
OR sys_context('userenv', 'client_info') != 'CASCADING')
BEGIN
UPDATE army SET number_of_soliders = number_of_soliders - 1
WHERE name = :old.army_name;
END;
/
If the context value has been set by the army-level trigger, then soldier-level trigger won't run because the when condition is not met. Otherwise it will still run and decrement the soldier count.
db<>fiddle
But this is still not recommended...

Related

SQL Server Relation table parent->child and cycle cascade

I have a sql table (let's call it Person) like, for example :
TABLE Person
Id Firstname Lastname
I want to make relations beetwen somme of this person (parent/child relation) so I built another table (let's call it Person_Relation) like that :
TABLE Person_Relation
Id_person_parent Id_person_child
I made a constraint to avoid the case where parent = child (it would be awkward !) and now i try to make foreign key beetwen table Person and table Person_Relation.I am currently able to make one foreign key, but when i try to set the second I get a : may cause cycles or multiple cascade paths error.
Knowing that I would keep the 'Delete cascade' to automatically delete links in Person_Relation table when an entry in Person is deleted, is there any clean solution to do that ?
Thank you in advance.
SQL Server won't let you create multiple cascade paths that could theoretically conflict. For more on that, see this answer.
One way to still achieve your goal is to use a trigger in place of a foreign key with a cascade action.
CREATE TRIGGER dbo.Person_Delete
ON dbo.Person
FOR DELETE
AS
DELETE dbo.Person_Relation
WHERE Id_person_parent IN (SELECT Id FROM deleted)
OR Id_person_child IN (SELECT Id FROM deleted);

Is there a generic way to replace referential constraints with triggers?

Porting application from MSSQL+Oracle to Sybase, and there's an issue with 'on delete cascade' - Sybase doesn't have the option.
Sybase has a link with a trigger to implement cascading delete: http://infocenter.sybase.com/help/index.jsp?topic=/com.sybase.help.ase_15.0.sqlug/html/sqlug/sqlug815.htm
but there is a problem with that solution when put into context of using it as 'on delete cascade'.
The problem, is triggers get executed after any referential constraints are checked.
The issue is illustrated here:
--drop table A
--drop table B
create table A (c int primary key)
create table B (c int primary key)
alter table A
add constraint Ac FOREIGN KEY (c) REFERENCES B(c)
create trigger delBA on B for delete
as delete A from A, deleted where A.c = deleted.c
insert into B values (1)
insert into A values (1)
delete B where c = 1
The 'delete' statement will fail, because of 'Ac' constraint. Had the trigger fired before the check for referential constraints (instead of after), it would have removed the value '1' from table 'A' and there would not be a problem.
For this reason, I'm thinking to implement the referential constraint by using a trigger. So I have to create an Insert and Update trigger, I believe. Is there some template that I can use?
I want to make sure I'm not overlooking anything, at first look at the issue, I missed that update trigger should be written so that it can validate that after update the constraint is still valid. - That is the reason I'm looking for a template, so I won't miss anything similar.
Triggers are often sources of trouble. A common approach would be to set up your data access layer that will create a transaction, delete the 'children' (cascade part) then delete the parent.

Oracle Unique Constraint - Trigger to check value of property in new relation

Hi I'm having trouble getting my sql syntax correct. I want to create a unique constraint that looks at the newly added foreign key, looks at some properties of the newly related entity to decided if the relationship is allowed.
CREATE or replace TRIGGER "New_Trigger"
AFTER INSERT OR UPDATE ON "Table_1"
FOR EACH ROW
BEGIN
Select "Table_2"."number"
(CASE "Table_2"."number" > 0
THEN RAISE_APPLICATION_ERROR(-20000, 'this is not allowed');
END)
from "Table_1"
WHERE "Table_2"."ID" = :new.FK_Table_2_ID
END;
Edit: APC answer is wonderfully comprehensive, however leads me to think im doing it in the wrong way.
The situation is I have a table of people with different privilege levels, and I want to check these privilege levels, e.g. A user, 'Bob', has low level privileges and he tries to become head of department which requires requires high privileges so the system prevents this happening.
There is a follow-up question which poses a related scenario but with a different data model. Find it here.
So the rule you want to enforce is that TABLE_1 can only reference TABLE_2 if some column in TABLE_2 is zero or less. Hmmm.... Let's sort out the trigger logic and then we'll discuss the rule.
The trigger should look like this:
CREATE or replace TRIGGER "New_Trigger"
AFTER INSERT OR UPDATE ON "Table_1"
FOR EACH ROW
declare
n "Table_2"."number".type%;
BEGIN
Select "Table_2"."number"
into n
from "Table_2"
WHERE "Table_2"."ID" = :new.FK_Table_2_ID;
if n > 0
THEN RAISE_APPLICATION_ERROR(-20000, 'this is not allowed');
end if;
END;
Note that your error message should include some helpful information such as the value of the TABLE_1 primary key, for when you are inserting or updating multiple rows on the table.
What you are trying to do here is to enforce a type of constraint known as an ASSERTION. Assertions are specified in the ANSI standard but Oracle has not implemented them. Nor has any other RDBMS, come to that.
Assertions are problematic because they are symmetrical. That is, the rule also needs to be enforced on TABLE_2. At the moment you check the rule when a record is created in TABLE_1. Suppose at some later time a user updates TABLE_2.NUMBER so it is greater than zero: your rule is now broken, but you won't know that it is broken until somebody issues a completely unrelated UPDATE on TABLE_1, which will then fail. Yuck.
So, what to do?
If the rule is actually
TABLE_1 can only reference TABLE_2 if
TABLE_2.NUMBER is zero
then you can enforce it without triggers.
Add a UNIQUE constraint on TABLE_2 for (ID, NUMBER); you need an additional constraint because ID remains the primary key for TABLE_2.
Add a dummy column on TABLE_1 called TABLE_2_NUMBER. Default it to zero and have a check constraint to ensure it is always zero. (If you are on 11g you should consider using a virtual column for this.)
Change the foreign key on TABLE_1 so (FK_Table_2_ID, TABLE_2_NUMBER) references the unique constraint rather than TABLE_2's primary key.
Drop the "New_Trigger" trigger; you don't need it anymore as the foreign key will prevent anybody updating TABLE_2.NUMBER to a value other than zero.
But if the rule is really as I formulated it at the top i.e.
TABLE_1 can only reference TABLE_2 if
TABLE_2.NUMBER is not greater than zero (i.e. negative values are okay)
then you need another trigger, this time on TABLE_2, to enforce it the other side of the rule.
CREATE or replace TRIGGER "Assertion_Trigger"
BEFORE UPDATE of "number" ON "Table_2"
FOR EACH ROW
declare
x pls_integer;
BEGIN
if :new."number" > 0
then
begin
Select 1
into x
from "Table_1"
WHERE "Table_1"."FK_Table_2_ID" = :new.ID
and rownum = 1;
RAISE_APPLICATION_ERROR(-20001, :new.ID
||' has dependent records in Table_1');
exception
when no_data_found then
null; -- this is what we want
end;
END;
This trigger will not allow you to update TABLE_2.NUMBER to a value greater than zero if it is referenced by records in TABLE_2. It only fires if the UPDATE statement touches TABLE_2.NUMBER to minimise the performance impact of executing the lookup.
Don't use a trigger to create a unique constraint or a foreign key constraint. Oracle has declarative support for unique and foreign keys, e.g.:
Add a unique constraint on a column:
ALTER TABLE "Table_1" ADD (
CONSTRAINT table_1_uk UNIQUE (column_name)
);
Add a foreign key relationship:
ALTER TABLE "ChildTable" ADD (
CONSTRAINT my_fk FOREIGN KEY (parent_id)
REFERENCES "ParentTable" (id)
);
I'm not clear on exactly what you're trying to achieve with your trigger - it's a bit of a mess of SQL and PL/SQL munged together which will not work, and seems to refer to a column on "Table_2" which is not actually queried.
A good rule of thumb is, if your trigger is querying the same table that the trigger is on, it's probably wrong.
I'm not sure, but are you after some kind of conditional foreign key relationship? i.e. "only allow child rows where the parent satisfies condition x"? If so, the problem is in the data model and should be fixed there. If you provide more explanation of what you're trying to achieve we should be able to help you.

SQL Server: Self-reference FK, trigger instead of ON DELETE CASCADE

I need to perform an ON DELETE CASCADE on my table named CATEGORY, which has the following columls
CAT_ID (BIGINT)
NAME (VARCHAR)
PARENT_CAT_ID (BIGINT)
PARENT_CAT_ID is a FK on CAT_ID. Obviously, the lovely SQL Server does not let me use ON DELETE CASCADE claiming circular or multiple paths to deletion.
A solution that I see often proposed is triggers. I made the following trigger:
USE [ma]
GO
/****** Object: Trigger [dbo].[TRG_DELETE_CHILD_CATEGORIES] Script Date: 11/23/2009 16:47:59 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER TRIGGER [dbo].[TRG_DELETE_CHILD_CATEGORIES] ON [dbo].[CATEGORY] FOR DELETE AS
SET NOCOUNT ON
/* * CASCADE DELETES TO '[Tbl B]' */
DELETE CATEGORY FROM deleted, CATEGORY WHERE deleted.CAT_ID = CATEGORY.PARENT_CAT_ID
When I manually delete a category with child categories, I get the following exception:
Any idea what is wrong with my trigger?
UPDATE:
Sorry for the edit, but I have another column CATEGORY.CAT_SCH_ID, which is a FK of another table CAT_SCH.ID. This FK has a CASCADE DELETE as well, meaning that once I delete a CAT_SCH, its CATEGORies must also be deleted. So, I get this error when I define the trigger:
Cannot create INSTEAD OF DELETE or INSTEAD OF UPDATE TRIGGER 'TRG_DEL_CATEGORY_WITH_CHILDREN' on table 'CATEGORY'. This is because the table has a FOREIGN KEY with cascading DELETE or UPDATE.
Any ideas?
The FOR DELETE trigger is raised after the original DELETE has been executed. To delete recursively, you need to write an INSTEAD OF DELETE trigger.
The algorithm is like this:
Insert the PKs from deleted into a temp table
Find detail records of records in temp table
Loop until no more records are found
DELETE records in the table by joining with temp table.
I described recursive deletion in my blog.
Update
I guess you just need to drop that ON DELETE CASCADE flag from your recursive foreign key in Categories. The CASCADE flag on the foreign key from CAT_SCH should not matter.

SQL Delete if the row is not affected by constraints

First note that I have seen this question:TSQL delete with an inner join
I have a large table and several foreign key relations, each of which have data of a given age. We need to remove data older than a given data on a regular basis to stop the DB from growing without bound.
I'm writing a query that will delete from each point on the star if you will by the given parameters (unfortunately these are configurable and different between the tables).
After this first deletion, I have a central table that I'm worried that I'm doing twice the work attempting to delete, as on delete the database checks the conditionals. I have a set of:
AND NOT EXISTS
(SELECT key
FROM table
WHERE table.key = centretable.key)
which TSQL is making into a right anti semi join and doing it nicely on the indexes. The problem is it creates a list of stuff to delete and then does the same checks again as it performs the delete.
I guess my question is whether there is a try delete by row, (I'm not going to do that in a cursor as I know how slow it would be), but you would think that such a keyword would exist, I haven't had any luck finding it though.
In terms of a single command that only checks the relationships once (rather than twice in your example - once for the NOT EXISTS, once for the DELETE), then I expect the answer is a big fat no, sorry.
(off the wall idea):
If this is a major problem, you could try some kind of reference-counting implementation, using triggers to update the counter - but in reality I expect this will be a lot more overhead to maintain than simply checking the keys like you are already.
You could also investigate NOCHECK during the delete (since you are checking it yourself); but you can only do this at the table level (so probably OK for admin scripts, but not for production code) - i.e.:
-- disable
alter table ChildTableName nocheck constraint ForeignKeyName
-- enable
alter table ChildTableName check constraint ForeignKeyName
A quick test shows that with it enabled it does an extra Clustered Index Scan on the foreign key; with it disabled, this is omitted.
Here's a full example; you can look at the query plan of the two DELETE operations... (ideally in isolation from the rest of the code):
create table parent (id int primary key)
create table child (id int primary key, pid int)
alter table child add constraint fk_parent foreign key (pid)
references parent (id)
insert parent values (1)
insert parent values (2)
insert child values (1,1)
insert child values (2,1)
-- ******************* THIS ONE CHECKS THE FOREIGN KEY
delete from parent
where not exists (select 1 from child where pid = parent.id)
-- reset
delete from child
delete from parent
insert parent values (1)
insert parent values (2)
insert child values (1,1)
insert child values (2,1)
-- re-run with check disabled
alter table child nocheck constraint fk_parent
-- ******************* THIS ONE DOESN'T CHECK THE FOREIGN KEY
delete from parent
where not exists (select 1 from child where pid = parent.id)
-- re-enable
alter table child check constraint fk_parent
Again - I stress this should only be run from things like admin scripts.
You could create an Indexed view of your select sentence:
SELECT key FROM table WHERE table.key = centretable.key
The indexed view is a physical copy of the data it would therefore be very fast to check.
You do have the overhead of updating the view, so you would need to test this against your usage pattern.
If you're reusing the same list of stuff to delete then you could consider inserting the keys to delete into a temp table and then using this in the second query.
SELECT Key, ...
INTO #ToDelete
FROM Table T
WHERE ...
Then something like this
...
LEFT OUTER JOIN #ToDelete D
ON T.Key=D.Key
WHERE D.Key IS NULL
DROP #ToDelete
If you specified the foreign key as a constraint when creating the table in the database you can tell the database what to do in case of a delete, by setting the delete rule. This rule specifies what happens if a user tries to delete a row with data that is involved in a foreign key relationship. The "No action" setting tells the user that the deletion is not allowed and the DELETE is rolled back. Implementing it like that would keep you from checking it yourself before deleting it, and thus could be seen as some kind of try.
Well, at least it works like that in MS SQL. http://msdn.microsoft.com/en-us/library/ms177288.aspx
I did find one article that discusses using an outer join in a delete:
http://www.bennadel.com/blog/939-Using-A-SQL-JOIN-In-A-SQL-DELETE-Statement-Thanks-Pinal-Dave-.htm
I hope this works for you!
The short answer to your question is no, there is no standard RDBMS keyword for deleting a master record when all foreign key references to it go away (and certainly none that would account for foreign keys in multiple tables).
Your most efficient option is a second query that is run on an as-needed basis to delete from "centre" based on a series of NOT EXISTS() clauses for each of the tables with foreign keys.
This is based on two statements I believe are both true for your situation:
You will delete more "related" records than "centre" (parent) records. Thus, any operation that attempts to adjust "centre" every time you delete from one of the other tables will result in an instantaneous update to "centre", but will require much wasted querying to delete a "centre" record only occasionally.
Given that there are multiple points on the star from "centre," any "wasted effort" checking for a foreign key in one of them is minimal compared to the whole. For instance, if there are four foreign keys to check before deleting from "centre", you can only save, at best, 25% of the time.