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

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.

Related

Delete trigger to delete child records

So, I have two tables.
One is sales_order (no_so VARCHAR PRIMARY KEY)
The other is status_so (no_so REFERENCES to sales_order)
I want the no_so from status_so is deleted if I delete the no_so in sales_order. So, I create the trigger to solve the problem.
CREATE TRIGGER trg_sales_order
ON sales_order
FOR DELETE
AS
DELETE FROM status_so
WHERE status_so.no_so IN (SELECT deleted.no_so FROM deleted)
after that I run this code
DELETE FROM sales_order WHERE no_so = 'SO004'
and I still got an error, it says
The DELETE statement conflicted with the REFERENCE constraint "FK_status_so_sales_order". The conflict occurred in database "db", table "dbo.status_so", column 'no_so'.
From the docs:
OR | AFTER FOR or AFTER specifies that the DML trigger fires only when
all operations specified in the triggering SQL statement have launched
successfully. All referential cascade actions and constraint checks
must also succeed before this trigger fires.
You need to create instead of delete trigger and handle the deletions from the both table in the correct order there.

Delete records of table which has 2 foreign keys that reference to same table

I have 2 tables, first one is Compartment and second one is AboveCompartment. Please see the below. Above compartment has 2 columns which are foreign keys and reference to the Compartment table. When I set the delete and update action as cascade for 2 foreign keys, I get the error below.
Introducing FOREIGN KEY constraint 'FK_AboveCompartment_Compartment1' on table 'AboveCompartment' may cause cycles or multiple cascade paths. Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints.
Below CompId and AboveCompId are foreign keys and reference to the Compartment table. Which way should I follow to add delete cascading? I used the trigger below but it also didn't work and get error added below.
AboveCompartment
Compartment
Trigger
ALTER TRIGGER [dbo].[delFromCompartment] on [dbo].[Compartment]
FOR DELETE
AS
DELETE FROM dbo.AboveCompartment
WHERE CompId IN(SELECT deleted.Id FROM deleted)
Error
You cannot implement this using cascades, as SQL Server does not let you.
You also cannot implement it using triggers, because the foreign key is enforced before you get to the trigger.
You need to write a stored procedure that first deletes the parent table rows, then the child table
CREATE OR ALTER PROCEDURE dbo.Delete_Compartment
#CompartmentId int
AS
SET XACT_ABORT, NOCOUNT ON; -- always use XACT_ABORT if you have a transaction
BEGIN TRAN;
DELETE AboveCompartment
WHERE CompId = #CompartmentId;
DELETE AboveCompartment
WHERE AboveCompId = #CompartmentId;
DELETE Compartment
WHERE Id = #CompartmentId;
COMMIT;
I must say, this table design is somewhat suspect. AboveCompId as a column name implies that it represents a single parent for multiple children, rather than multiple parents for multiple children.
If so then you should instead implement this as a self-referencing foreign key. Drop the AboveCompartment table, and add a column
ALTER TABLE Compartment
ADD AboveCompId int NULL REFERENCES Compartment (Id);
This foreign key also cannot be cascading. But now the delete is only on one table, but you can do it in a recursive fashion. As long as you delete all rows in one go, you shouldn't have an issue with foreign key conflicts.
CREATE OR ALTER PROCEDURE dbo.Delete_Compartment
#CompartmentId int
AS
SET NOCOUNT ON;
-- no transaction needed as it's one statement
WITH cte AS (
SELECT #CompartmentId AS Id
UNION ALL
SELECT c.Id
FROM Compartment c
JOIN cte ON cte.Id = c.AboveCompId;
)
DELETE c
FROM Compartment c
JOIN cte ON cte.Id = c.Id;

Oracle before delete trigger

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

multiple cascade paths, on update cascade

I have a problem with sql server, when I want to have 3 table and make relationship between them, and change the "On Update" property to "cascade".
this problem happend when I want to save the diagram:
Introducing FOREIGN KEY constraint 'FK_Company_Slave' on table 'Company' may cause cycles or multiple cascade paths. Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints.
Could not create constraint. See previous errors.
in this picture, I define my data base, FK and ....
thanks.
First:
A FOREIGN KEY in one table points to a PRIMARY KEY in another table. if you don't want to use PRIMARY KEY on other table in order to foreign key, you must be create unique index on the table.
Second:
you can create after trigger on Master table in order to develop on update cascade manually. In other word your foreign key between Company table and Master table created without on update cascade and then create following trigger on master to update company table after changed row in Master table.
create trigger triggername on dbo.[Master]
After Insert
AS Begin
Update Company
Set MasterKey = I.MasterKey
From Inserted I
Inner join Deleted D on D.Code = I.Code
Where Company.MasterKey = D.MasterKey
End

Delete rows with foreign key in PostgreSQL

I would like to delete rows which contain a foreign key, but when I try something like this:
DELETE FROM osoby WHERE id_osoby='1'
I get this statement:
ERROR: update or delete on table "osoby" violates foreign key constraint "kontakty_ibfk_1" on table "kontakty"
DETAIL: Key (id_osoby)=(1) is still referenced from table "kontakty".
How can I delete these rows?
To automate this, you could define the foreign key constraint with ON DELETE CASCADE.
I quote the the manual for foreign key constraints:
CASCADE specifies that when a referenced row is deleted, row(s)
referencing it should be automatically deleted as well.
Look up the current FK definition like this:
SELECT pg_get_constraintdef(oid) AS constraint_def
FROM pg_constraint
WHERE conrelid = 'public.kontakty'::regclass -- assuming public schema
AND conname = 'kontakty_ibfk_1';
Then add or modify the ON DELETE ... part to ON DELETE CASCADE (preserving everything else as is) in a statement like:
ALTER TABLE kontakty
DROP CONSTRAINT kontakty_ibfk_1
, ADD CONSTRAINT kontakty_ibfk_1
FOREIGN KEY (id_osoby) REFERENCES osoby (id_osoby) ON DELETE CASCADE;
There is no ALTER CONSTRAINT command. Drop and recreate the constraint in a single ALTER TABLE statement to avoid possible race conditions with concurrent write access.
You need the privileges to do so, obviously. The operation takes an ACCESS EXCLUSIVE lock on table kontakty and a SHARE ROW EXCLUSIVE lock on table osoby.
If you can't ALTER the table, then deleting by hand (once) or by trigger BEFORE DELETE (every time) are the remaining options.
One should not recommend this as a general solution, but for one-off deletion of rows in a database that is not in production or in active use, you may be able to temporarily disable triggers on the tables in question.
In my case, I'm in development mode and have a couple of tables that reference one another via foreign keys. Thus, deleting their contents isn't quite as simple as removing all of the rows from one table before the other. So, for me, it worked fine to delete their contents as follows:
ALTER TABLE table1 DISABLE TRIGGER ALL;
ALTER TABLE table2 DISABLE TRIGGER ALL;
DELETE FROM table1;
DELETE FROM table2;
ALTER TABLE table1 ENABLE TRIGGER ALL;
ALTER TABLE table2 ENABLE TRIGGER ALL;
You should be able to add WHERE clauses as desired, of course with care to avoid undermining the integrity of the database.
There's some good, related discussion at http://www.openscope.net/2012/08/23/subverting-foreign-key-constraints-in-postgres-or-mysql/
You can't delete a foreign key if it still references another table.
First delete the reference
delete from kontakty
where id_osoby = 1;
DELETE FROM osoby
WHERE id_osoby = 1;
It's been a while since this question was asked, hope can help.
Because you can not change or alter the db structure, you can do this. according the postgresql docs.
TRUNCATE -- empty a table or set of tables.
TRUNCATE [ TABLE ] [ ONLY ] name [ * ] [, ... ]
[ RESTART IDENTITY | CONTINUE IDENTITY ] [ CASCADE | RESTRICT ]
Description
TRUNCATE quickly removes all rows from a set of tables. It has the same effect as an unqualified DELETE on each table, but since it does not actually scan the tables it is faster. Furthermore, it reclaims disk space immediately, rather than requiring a subsequent VACUUM operation. This is most useful on large tables.
Truncate the table othertable, and cascade to any tables that reference othertable via foreign-key constraints:
TRUNCATE othertable CASCADE;
The same, and also reset any associated sequence generators:
TRUNCATE bigtable, fattable RESTART IDENTITY;
Truncate and reset any associated sequence generators:
TRUNCATE revinfo RESTART IDENTITY CASCADE ;
It means that in table kontakty you have a row referencing the row in osoby you want to delete. You have do delete that row first or set a cascade delete on the relation between tables.
Powodzenia!
One can achieve this by issueing an extra SQL script that deletes the records related via the FK.
For this, using subselect in WHERE clause of a DELETE command can simply do what is needed.
Something similar to:
DELETE FROM kontakty
WHERE fk_column_from_kontakty_matching_id_osoby IN (
SELECT id_osoby FROM osoby WHERE id_osoby = '1'
);
DELETE FROM osoby WHERE id_osoby = '1';
the example assumes the FK column from kontakty matching osoby.id_osoby is called fk_column_from_kontakty_matching_id_osoby as it cannot be interpolated from the error message provided
For the case of OP's the query could by simplified a lot, but I have decided to leave it like this for it demonstrates accomplishing more complex scenarios
This approach is quite useful for SQL migrations, where one cannot depend on an assigned id and where ids cannot be easily fetched first and acted upon later.