Self referencing foreign-key constraints and delete - sql

what is the recommended way to handle self-referencing foreignkey constraints in SQL-Server?
Table-Model:
fiData references a previous record in tabData. If i delete a record that is referenced by fiData, the database throws an exception:
"The DELETE statement conflicted with the SAME TABLE REFERENCE
constraint "FK_tabDataPrev_tabDataNext". The conflict occurred in
database "MyDataBase", table "dbo.tabData", column 'fiData'"
if Enforce Foreignkey Constraint is set to "Yes".
I don't need to cascade delete records that are referenced but i would need to set fiData=NULL where it's referenced. My idea is to set Enforce Foreignkey Constraint to "No" and create a delete-trigger. Is this recommendable or are there better ways?
Thank you.

Unlike Andomar, I'd be happy using a trigger - but I wouldn't remove the constraint checking. If you implement it as an instead of trigger, you can reset the other rows to null before performing the actual delete:
CREATE TRIGGER T_tabData_D
on tabData
instead of delete
as
set nocount on
update tabData set fiData = null where fiData in (select idData from deleted)
delete from tabData where idData in (select idData from deleted)
It's short, it's succinct, it wouldn't be necessary if SQL Server could handle foreign key cascades to the same table (in other RDBMS', you may be able to just specify ON DELETE SET NULL for the foreign key constraint, YMMV).

Triggers add implicit complexity. In a database with triggers, you won't know what a SQL statement does by looking at it. In my experience triggers are a bad idea with no exceptions.
In your example, setting the enforced constrained to "No" means you could add a nonexistent ID. And the query optimizer will be less effective because it can't assume the key is valid.
Consider creating a stored procedure instead:
create procedure dbo.NukeTabData(
#idData int)
as
begin transaction
update tabData set fiData = null where fiData = #idData
delete from tabData where idData = #idData
commit transaction
go

This very late to answer.
But for some one who is searching like me.
and want to cascade
here is very good explanation
http://devio.wordpress.com/2008/05/23/recursive-delete-in-sql-server/
The Problem
Although you can define a foreign key with CASCADE DELETE in SQL Server, recursive cascading deletes are not supported (i.e. cascading delete on the same table).
If you create an INSTEAD OF DELETE trigger, this trigger only fires for the first DELETE statement, and does not fire for records recursively deleted from this trigger.
This behavior is documented on MSDN for SQL Server 2000 and SQL Server 2005.
The Solution
Suppose you have a table defined like this:
CREATE TABLE MyTable (
OID INT, -- primary key
OID_Parent INT, -- recursion
... other columns
)
then the delete trigger looks like this:
CREATE TRIGGER del_MyTable ON MyTable INSTEAD OF DELETE
AS
CREATE TABLE #Table(
OID INT
)
INSERT INTO #Table (OID)
SELECT OID
FROM deleted
DECLARE #c INT
SET #c = 0
WHILE #c <> (SELECT COUNT(OID) FROM #Table) BEGIN
SELECT #c = COUNT(OID) FROM #Table
INSERT INTO #Table (OID)
SELECT MyTable.OID
FROM MyTable
LEFT OUTER JOIN #Table ON MyTable.OID = #Table.OID
WHERE MyTable.OID_Parent IN (SELECT OID FROM #Table)
AND #Table.OID IS NULL
END
DELETE MyTable
FROM MyTable
INNER JOIN #Table ON MyTable.OID = #Table.OID
GO

Related

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;

Can the table referenced by a foreign key depend on the contents of another field?

Setting aside for a moment the sanity of whether this is a good idea or not, I was wondering whether it would be possible to set up a field which links to more than two tables and still be able to enforce referential integrity?
e.g. I'd like to be able to create foreign key definitions something like this :
create table TestTable
(
Id int identity not null primary key,
SourceId int not null,
SourceTable varchar(100) not null
/* some other common data goes here */
constraint FK_TestTable_TableA foreign key (SourceId) references TableA(Id) when TestTable(SourceTable) = 'TableA'
constraint FK_TestTable_TableB foreign key (SourceId) references TableB(Id) when TestTable(SourceTable) = 'TableB'
)
Is there a pattern for achieving this kind of behaviour, or if I go down this route am I simply doomed to the creeping horror that is a lack of referential integrity?
No, this isn't possible without workarounds such as #Damien's
An alternative workaround is to use triggers to check up on the integrity.
Here's an INSTEAD OF trigger implementation - SqlFiddle here
CREATE TRIGGER t_TestTable ON TestTable INSTEAD OF INSERT
AS
BEGIN
SET NOCOUNT ON;
INSERT INTO TestTable(SourceID, SourceTable)
SELECT i.SourceID, i.SourceTable --, + i.Other field values
FROM INSERTED i
WHERE (i.SourceTable = 'TableA' AND EXISTS (SELECT * FROM TableA where ID = i.SourceID))
OR (i.SourceTable = 'TableB' AND EXISTS (SELECT * FROM TableB where ID = i.SourceID));
-- IF ##ROWCOUNT = 0 THROW / RAISERROR ?
END;
GO
You'll also need to cover UPDATES on TestTable, and cover UPDATES and DELETES on TableA / TableB, and also determine what to do in the event of FK violations (ignoring the data probably as I've done isn't a good strategy)
Instead of reinventing the wheel, a better design IMO is to use table inheritance - e.g. make TableA and TableB inherit from a common base ancestor Table which has a unique Primary Key common to both tables (and add the SourceTable` table type qualifier to the base table). This will allow for direct RI.

How do I switch two ID [PK] on postgres database?

I want to change the ID on two rows on Postgres, to switch them. They are already defined as foreign key so I cannot use a third number to do the switch.
How can I do this in one SQL query or transaction?
Example:
UPDATE mytable SET id=2 WHERE ID=1;
UPDATE mytable SET id=1 WHERE ID=2
You mention foreign keys, but it remains unclear whether id is the referenced or the referencing column of a foreign key constraint.
If id is the referenced column you just define the fk constraint ON UPDATE CASCADE. Then you can change your id as much as you want. Changes are cascaded to the depending columns.
If id is the referencing column (and no other foreign key constraints point to it), then there is another, faster way since PostgreSQL 9.0. You can use a deferrable primary or unique key. Consider the following demo:
Note that you can't use this if you want to reference id with a foreign key constraint from another table. I quote the manual here:
The referenced columns must be the columns of a non-deferrable unique
or primary key constraint in the referenced table.
Testbed:
CREATE TEMP TABLE t
( id integer
,txt text
,CONSTRAINT t_pkey PRIMARY KEY (id) DEFERRABLE INITIALLY DEFERRED
);
INSERT INTO t VALUES
(1, 'one')
,(2, 'two');
Update:
UPDATE t
SET id = t_old.id
FROM t t_old
WHERE (t.id, t_old.id) IN ((1,2), (2,1));
Result:
SELECT * FROM t;
id | txt
---+-----
2 | one
1 | two
You can also declare the constraint DEFERRABLE INITIALLY IMMEDIATE and use SET CONSTRAINTS ... DEFERRED in the same transaction.
Be sure to read about the details in the manual:
CREATE TABLE
SET CONSTRAINTS
Even seems to work with DEFERRABLE INITIALLY IMMEDIATE and no SET CONSTRAINTS. I posted a question about that.
begin;
alter table mytable drop constraint_name;
UPDATE mytable SET id=-1 WHERE ID=1;
UPDATE mytable SET id=1 WHERE ID=2;
UPDATE mytable SET id=2 WHERE ID=-1;
alter table mytable add table_constraint;
commit;
Have you tried something like:
BEGIN;
CREATE TEMP TABLE updates ON COMMIT DROP AS
SELECT column1::int oldid, column2::int newid
FROM ( VALUES (1, 2), (2, 1) ) foo;
UPDATE mytable
FROM updates
SET id = newid
WHERE id = oldid;
--COMMIT;
ROLLBACK;
Of course rollback gets commented out and commit in when you are ready to go.

Replace Create Trigger with Foreign Key (RI)

Create Trigger:
SELECT #oldVersionId = (SELECT DISTINCT VERSION_ID FROM Deleted)
SELECT #newVersionId = (SELECT DISTINCT VERSION_ID FROM Inserted)
SELECT #appId = (SELECT DISTINCT APP_ID FROM Deleted)
UPDATE [TableName]
SET [VERSION_ID] = #newVersionId
WHERE (([VERSION_ID] = #oldVersionId) AND ([APP_ID] = #appId) )
Can this Trigger be replace with a Foreign Key to update the VERSION_ID ?
What I think could be a problem is the AND condition, how to express that in a FK with On del/update Cascade?
FOREIGN KEY CONSTRAINTS don't update anything. They check the values being written to a record and cause the write to fail if they cause a constraint to fail.
Also, as #marc_s points out in his comment, triggers in MS SQL Server are set based. The INSERTED and DELETED tables can hold multiple records at once. Your code only works for one record.
You could try something along these lines...
UPDATE
table
SET
VERSION_ID = inserted.VERSION_ID
FROM
table
INNER JOIN
deleted
ON table.VERSION_ID = deleted.VERSION_ID
AND table.APP_ID = deleted.APP_ID
INNER JOIN
inserted
ON deleted.PRIMARY_KEY = inserted.PRIMARY_KEY
EDIT
I just read your comment, and I think I understand. You want a foreign key constraint with ON UPDATE CASCADE.
You use this format to create that with DDL.
ALTER TABLE DBO.<child table>
ADD CONSTRAINT <foreign key name> FOREIGN KEY <child column>
REFERENCES DBO.<parent table>(<parent column>)
{ON [DELETE|UPDATE] CASCADE}
Or you could just SQL Server Management Studio to set it up. Just make sure the ON UPDATE CASCADE is present.
I cannot really tell you what you're looking for - you're too unclear in your question.
But basically, if two tables are linked via a foreign key constraint, of course you can add a clause to that to make sure the child table gets updated when the parents table's PK changes:
ALTER TABLE dbo.ChildTable
ADD CONSTRAINT FK_ChildTable_ParentTable
FOREIGN KEY(ChildTableColumn) REFERENCES dbo.ParentTable(PKColumn)
ON UPDATE CASCADE
The ON UPDATE CASCADE does exactly that - if the referenced column (the PKColumn in ParentTable) changes, then the FK constraint will "cascade" that update down into the child table and update it's ChildTableColumn to match the new PKColumn
Read all about cascading referential integrity constraints and what options you have on MSDN Books Online

Self-referencing constraint in MS SQL

Is it true that MS SQL restrict self-referencing constraints with ON DELETE CASCADE option?
I have a table with parent-child relation, PARENT_ID column is foreign key for ID. Creating it with ON DELETE CASCADE option causes error
"Introducing FOREIGN KEY constraint
may cause cycles or multiple cascade
paths. Specify ON DELETE NO ACTION or
ON UPDATE NO ACTION, or modify other
FOREIGN KEY constraints."
I can't believe that I have to delete this hierarchy in recursive mode. Is there any issue except triggers?
It is the case that you cannot set up ON DELETE CASCADE on a table with self-referencing constraints. There is a potential of cyclical logic problems, hence it won't allow it.
There's a good article here - though it's for version 8 rather than 9 of SQL - though the same rules apply.
I just answered another question where this question was bound as duplicate. I think it's worth to place my answer here too:
This is not possible. You can solve this with an INSTEAD OF TRIGGER
create table locations
(
id int identity(1, 1),
name varchar(255) not null,
parent_id int,
constraint pk__locations
primary key clustered (id)
)
GO
INSERT INTO locations(name,parent_id) VALUES
('world',null)
,('Europe',1)
,('Asia',1)
,('France',2)
,('Paris',4)
,('Lyon',4);
GO
--This trigger will use a recursive CTE to get all IDs following all ids you are deleting. These IDs are deleted.
CREATE TRIGGER dbo.DeleteCascadeLocations ON locations
INSTEAD OF DELETE
AS
BEGIN
WITH recCTE AS
(
SELECT id,parent_id
FROM deleted
UNION ALL
SELECT nxt.id,nxt.parent_id
FROM recCTE AS prv
INNER JOIN locations AS nxt ON nxt.parent_id=prv.id
)
DELETE FROM locations WHERE id IN(SELECT id FROM recCTE);
END
GO
--Test it here, try with different IDs. You can try WHERE id IN(4,3) also...
SELECT * FROM locations;
DELETE FROM locations WHERE id=4;
SELECT * FROM locations
GO
--Clean-Up (Carefull with real data!)
if exists(select 1 from INFORMATION_SCHEMA.TABLES where TABLE_NAME='locations')
---DROP TABLE locations;
CREATE TRIGGER MyTable_OnDelete ON MyTable
INSTEAD OF DELETE
AS
BEGIN
SET NOCOUNT ON;
DELETE FROM mt
FROM deleted AS D
JOIN MyTable AS mt
ON d.Id = mt.ParentId
DELETE FROM mt
FROM deleted AS D
JOIN MyTable AS mt
ON d.Id = mt.Id
END