Foreign key constraint cycles or multiple cascade paths - sql

I have a database design like below. I have 3 tables Compartment, CompartmentRelation and CompartmentRelationType . CompartmentRelation table keeps the other compartments around the selected compartment (below,above,behind,infront,etc). CompartmentRelationType keeps the position. Think that i have compartments in the Compartment table named comp-1, comp-2, comp-3, comp-4 and insert the the compartments above comp-1 as comp-2,comp-3 in CompartmentRelation as below. Problem is that setting delete action as cascade for the column RelatedCompId in CompartmentRelation table throw the excaption as
Unable to create relationship 'FK_CompartmentRelation_Compartment1'.
Introducing FOREIGN KEY constraint 'FK_CompartmentRelation_Compartment1' on table 'CompartmentRelation' 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 or index. See previous errors.
Which way should i follow ?
Compartment
comp-1
comp-2
comp-3
comp-4
Compartment Relation
comp-1 -> comp-2
comp-1 -> comp-3
CREATE TABLE [dbo].[Compartment] (
[Id] INT IDENTITY (1, 1) NOT NULL,
[Name] NVARCHAR (500) NOT NULL,
CONSTRAINT [PK_Compartment] PRIMARY KEY CLUSTERED ([Id] ASC),
CREATE TABLE [dbo].[CompartmentRelation] (
[Id] INT IDENTITY (1, 1) NOT NULL,
[CompId] INT NOT NULL,
[RelationTypeId] INT NOT NULL,
[RelatedCompId] INT NOT NULL,
CONSTRAINT [PK_CompartmentRelation] PRIMARY KEY CLUSTERED ([Id] ASC),
CONSTRAINT [FK_CompartmentRelation_CompartmentRelationType] FOREIGN KEY ([RelationTypeId]) REFERENCES [dbo].[CompartmentRelationType] ([Id]) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT [FK_CompartmentRelation_Compartment1] FOREIGN KEY ([RelatedCompId]) REFERENCES [dbo].[Compartment] ([Id]),
CONSTRAINT [FK_CompartmentRelation_Compartment] FOREIGN KEY ([CompId]) REFERENCES [dbo].[Compartment] ([Id]) ON DELETE CASCADE ON UPDATE CASCADE);
CREATE TABLE [dbo].[CompartmentRelationType] (
[Id] INT IDENTITY (1, 1) NOT NULL,
[Name] NVARCHAR (200) NOT NULL,
[NameLan1] NVARCHAR (200) NOT NULL,
[NameLan2] NVARCHAR (200) NULL,
CONSTRAINT [PK_CompartmentRelationType] PRIMARY KEY CLUSTERED ([Id] ASC)
);

Problem is that setting delete action as cascade for the column
RelatedCompId in CompartmentRelation table throw the excaption as
Unable to create relationship
'FK_CompartmentRelation_Compartment1'. Introducing FOREIGN KEY
constraint 'FK_CompartmentRelation_Compartment1' on table
'CompartmentRelation' 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 or index.
See previous errors.
The basic issue appears to be that deletes from table Compartment (and updates to its PKs) can cascade to CompartmentRelation through two different foreign keys. If you intend to support Compartments being related to themselves, then that's end-of-story for cascading from Compartment to CompartmentRelation -- you can't do it.* If you intend to forbid self relationships then you could try adding a check constraint to CompartmentRelation to enforce that prohibition, though I'm not at all sure that SQL Server will take that into account.
If SQL Server won't accept the cascading deletes then you have at least three options:
Make it an application responsibility to clean up compartment relationships before deleting compartments. (And don't cascade.)
Create triggers to handle relationship deletion when compartments are deleted. (And don't cascade.)
Create a stored procedure for deleting compartments, and make it handle the needed relationship deletions. (And don't cascade.)
Which way should i follow ?
Whichever of those makes the most sense for your application. All have advantages and disadvantages.
Additionally,
Do not cascade updates of surrogate key columns, especially when the key values are machine generated, as all yours are. Those keys should never be updated in the first place, and if an attempt were ever made to update one then it would be better for the DB to reject it, for whatever reason, than to accept it.
You probably don't want to cascade deletions of CompartmentRelationType to ComponentRelation. Including such cascading allows for deleting all the relations of a given type by deleting the type itself, but such a cascade is more likely to be performed mistakenly than intentionally, and if it were performed mistakenly then the resulting data loss would be significant. It's probably better to make the application delete all those relations explicitly if that's what it really means to do, and otherwise to reject deletion of types that are in use by existing relations.
*Technically, you could do it by cascading from only one of the two FKs with Compartment, but it seems unlikely that such a half-measure would serve your purposes.

Related

Efficiently enforcing a 1:1 relationship between two rows with foreign key constraints without creating redundant unique indexes

I have two PostgreSQL tables designed in the following way:
create type content_owner as enum (
'document',
'task'
);
create table content (
id serial not null primary key,
owner content_owner not null,
owner_document_id int references document(id) deferrable initially deferred,
owner_task_id int references task(id) deferrable initially deferred,
-- ...
constraint collab_content_owner_document
check (owner_document_id is null or (owner = 'document' and owner_document_id is not null)),
constraint collab_content_owner_task
check (owner_task_id is null or (owner = 'task' and owner_task_id is not null))
);
create table document (
id serial not null primary key,
content_id int not null references content(id),
-- ...
);
create table task (
id serial not null primary key,
content_id int not null references content(id),
-- ...
);
I want to enforce a 1:1 relationship at the database level for the document<->content relationship and the task<->content relationship.
Adding the following constraints accomplishes that:
alter table collab_content add foreign key (owner_document_id, id) references document (id, content_id) deferrable initially deferred;
alter table collab_content add foreign key (owner_task_id, id) references task (id, content_id) deferrable initially deferred;
alter table document add foreign key (content_id, id) references collab_content (id, owner_document_id);
alter table task add foreign key (content_id, id) references collab_content (id, owner_task_id);
Since I’m saying the ID pair should reference the same ID pair in the other table for both directions. However, this also requires me to create the following indexes:
alter table document add unique (id, content_id);
alter table task add unique (id, content_id);
alter table collab_content add unique (id, owner_document_id);
alter table collab_content add unique (id, owner_task_id);
These indexes feel pretty redundant given that there’s already a primary key on the id columns for these tables. It feels like PostgreSQL should be smart enough to be able to use the existing primary key constraint to make sure the foreign key constraints are met. Ideally I wouldn’t create a second, redundant, index on these tables for the purpose of these foreign key constraints.
Is there a way for me to avoid creating new unique indexes and instead tell PostgreSQL to only lookup the unique ID when resolving the foreign key?
Will PostgreSQL detect that these unique indexes are redundant (because the first column is the primary key) and not materialize a new index on disk for their purpose?
Is there a better way to enforce this constraint?
Two-way linking like this is a recipe for headaches. I recommend avoiding reference cycles if you can. In your case, the simplest way to store this information is to relax the constraint that there cannot be a content without a document or a task. Ask yourself, how might such a situation occur, how else could it be avoided, and what damage might it cause if it happens?
If we can remove that constraint, then we can have a very simple structure where document and task each have a content_id foreign key, and a unique index on it to ensure that no two documents have the same content.
If we can't remove that constraint, then the answers to your questions are:
There is no way to avoid creating those new unique indexes for the foreign keys. Foreign keys must have matching unique indexes.
Postgres will not detect that these indexes are redundant, and they will indeed be materialized and take up space.

Will not specifying a foreign key constraint get me in trouble?

I have the following T-SQL in VS2013:
CREATE TABLE [dbo].[Jugo]
(
[JugoID] INT IDENTITY (1, 1) NOT NULL,
[Jugo] NVARCHAR (50) NOT NULL,
[ColorID] INT NOT NULL,
[IngreID] INT NOT NULL,
PRIMARY KEY CLUSTERED ([JugoID] ASC),
FOREIGN KEY (ColorID) REFERENCES Color (ColorID)
ON UPDATE CASCADE ON DELETE CASCADE,
FOREIGN KEY (IngreID) REFERENCES Ingrediente (IngreID)
ON UPDATE CASCADE ON DELETE CASCADE
);
And I get the following message before updating the database:
Highlights
None
User actions
Create
Foreign Key: unnamed constraint on [dbo].[Jugo] (Foreign Key)
Foreign Key: unnamed constraint on [dbo].[Jugo] (Foreign Key)
Supporting actions
None
Can I proceed with these unnamed constraints? What would I need the constraints for?
For reference this is what I'm trying to do:
3 tables:
Jugo, Color, Ingrediente: so to create a jugo (juice) you specify name, color and ingredients, color and ingredients are on their own tables, hence the foreign keys I want to define.
Thanks.
As long as the ColorID and IngreID are both primary key in their own tables, you won't have any trouble.
But here's the catch, you didn't constrain your primary keys. It is a good practice to use constraint for every PRIMARY KEY you are using, in that way, when you foreign key it to another table. It can read that it is from another table. I don't know about SQL in VS2013 But here's my sample for SQL Server:
CREATE TABLE JUGO
(
JUGOID numeric identity (1,1) //For auto increment
CONSTRAINT PK_JUGOID PRIMARY KEY (JUGOID),
JUGO nvarchar(50) not null,
.
.
.
)
The PK_JUGOID in CONSTRAINT PK_JUGOID PRIMARY KEY (JUGOID), is user-defined, but it is a good practice to just copy the format of the primary key so you won't be confused when using foreign keys. :)
Foreign keys are for data consistency. You create the foreign keys on columns, so as not to allow invalid entries.
FOREIGN KEY (ColorID) REFERENCES Color (ColorID)
ON UPDATE CASCADE ON DELETE CASCADE,
This constraint makes sure that your jugo can only have a color that exists in the color table. Moreover you have two cascade commands:
ON UPDATE CASCADE means that if color red is ID 5, but you want to change it to 500, the change will be performed automatically for all jugo records. However, an ID should never change, so this is a clause seldom used.
ON DELETE CASCADE means that if you delete red, then you delete all red jugo records with it. I don't know if this is an appropriate case for you. It is used for instance on a users table. Remove a user, so you remove its complete data. But remove a color?
At last you should give your constraints names, so it's easier for you to deal with them.
CONSTRAINT pk_jugo PRIMARY KEY CLUSTERED ([JugoID] ASC),
CONSTRAINT fk_jugo_color FOREIGN KEY (ColorID) REFERENCES Color (ColorID),
CONSTRAINT fk_jugo_ingre FOREIGN KEY (IngreID) REFERENCES Ingrediente (IngreID)

Is there any "reverse" ON DELETE CASCADE option?

Let's say I have the following database in SQL Server:
CREATE TABLE [Order]
(
ID BIGINT IDENTITY(1,1)
CONSTRAINT PK_Order PRIMARY KEY CLUSTERED (ID)
);
CREATE TABLE OrderItem
(
ID BIGINT IDENTITY(1,1),
ORDER_ID BIGINT NOT NULL,
PRICE_ID BIGINT NOT NULL,
DISCOUNTED_PRICE_ID BIGINT NULL,
CONSTRAINT PK_OrderItem PRIMARY KEY CLUSTERED (ID)
);
CREATE TABLE Price
(
ID BIGINT IDENTITY(1,1),
AMOUNT FLOAT NOT NULL,
CURRENCY VARCHAR(3) NOT NULL,
CONSTRAINT PK_Price PRIMARY KEY CLUSTERED (ID)
);
ALTER TABLE OrderItem ADD CONSTRAINT FK_OrderItem_Order
FOREIGN KEY (ORDER_ID) REFERENCES [Order](ID) ON DELETE CASCADE;
ALTER TABLE OrderItem ADD CONSTRAINT FK_OrderItem_Price
FOREIGN KEY (PRICE_ID) REFERENCES Price(ID);
ALTER TABLE OrderItem ADD CONSTRAINT FK_OrderItem_DiscountedPrice
FOREIGN KEY (DISCOUNTED_PRICE_ID) REFERENCES Price(ID);
If I delete an order, all order items will be deleted (because of ON DELETE CASCADE on FK_OrderItem_Order constraint), but corresponding prices (normal and discounted) will remain in the database forever.
Is there any option in SQL Server (or generic SQL) to delete corresponding prices from Price table?
I can think of a trigger which is a perfect match, but it is too much hassle for such simple (and common) task. I would prefer to specify something on my constraints (FK_OrderItem_Price and FK_OrderItem_DiscountedPrice) that basically say "this is one-to-one relationship", delete parent (Price is a parent table in this case) if a child was deleted.
In a nutshell: no. Cascading works only from parent to child1, not the other way around.
It could be argued that some parents should be removed when they lose the last of their children, but that's simply not how current DBMSes are implemented.
You'll have to use a trigger for such "special" referential action, or a batch job it it doesn't have to happen immediately. Or hide the operations behind some sort of API (stored procedure, middle-tier method) that does that explicitly.
See also: order stability.
1 In you case, Order and Price both act as parents to OrderItem.
Add the OrderItemID to Price and set up a cascade delete relationship. This column is of course redundant but it allows you to have a cascade delete in the right direction.
Consider inlining the Price table two times into OrderItems. As this is a 1:1 relationship you can do that. It is a matter of taste whether you like this solution or not.
you can create a trigger which acts as a reverse cascade:
DELIMITER $$
CREATE TRIGGER reverse_cascade_OrderItem_Price
AFTER DELETE ON `OrderItem`
FOR EACH ROW
BEGIN
DELETE FROM `Price` WHERE ID = old.PRICE_ID;
END$$

SQL: Foreign Key on Column Pointing to Same Column

I've recently come across a bizzare scenario in one of our legacy databases and our DBA (not the one that created it) is uncertain why this would have been done and what benefit it would have. The only thing we can think of is that it was done in error. The following foreign key constraint has been defined on a table:
CREATE TABLE [dbo].[SomeTable]
(
[Id] SMALLINT IDENTITY (1,1) NOT NULL,
-- other columns
CONSTRAINT [PK_SomeTable] PRIMARY KEY CLUSTERED ([Id] ASC)
) ON [PRIMARY]
GO
ALTER TABLE [dbo].[SomeTable] WITH CHECK ADD CONSTRAINT [FK_SomeTable_SomeTable]
FOREIGN KEY ([Id]) REFERENCES [dbo].[SomeTable] ([Id])
GO
ALTER TABLE [dbo].[SomeTable] CHECK CONSTRAINT [FK_SomeTable_SomeTable]
GO
Anyone know or have any thoughts on what this may actually do?
It's possible to use a foreign key that references to the same table for implementing hierarchy, but of course we should use different columns in the same table.
I think the creator of this table just made mistake.
The rows cannot be deleted because of the foreign key constraint. At least SQL Server isn't smart enough to realize that the target of the foreign key is actually the same row. The same goes for trying to edit the value of the primary key.
I guess this could be by design in order to prevent the rows from ever be changed or deleted, but a better solution would be to use triggers instead.

How can I stop the delete on a parent if a child entity that references that parent exists?

I have the following DDL that I am using with SQL Server 2012:
CREATE TABLE Subject (
[SubjectId] INT IDENTITY (1, 1) NOT NULL,
[Name] NVARCHAR (50) Not NULL,
CONSTRAINT [PK_Subject] PRIMARY KEY CLUSTERED ([SubjectId] ASC)
)
CREATE TABLE Topic (
[TopicId] INT IDENTITY (1, 1) NOT NULL,
[Name] NVARCHAR (50) NOT NULL,
[SubjectId] INT NOT NULL,
CONSTRAINT [PK_Topic] PRIMARY KEY CLUSTERED ([TopicId] ASC)
)
ALTER TABLE [Topic] WITH CHECK ADD CONSTRAINT [FK_TopicSubject]
FOREIGN KEY([SubjectId]) REFERENCES [Subject] ([SubjectId])
ON DELETE CASCADE
CREATE TABLE SubTopic (
[SubTopicId] INT IDENTITY (1, 1) NOT NULL,
[TopicId] INT NOT NULL,
[Name] NVARCHAR (4000) Not NULL,
CONSTRAINT [PK_SubTopic] PRIMARY KEY CLUSTERED ([SubTopicId] ASC)
)
ALTER TABLE [SubTopic] WITH CHECK ADD CONSTRAINT [FK_SubTopicTopic]
FOREIGN KEY([TopicId]) REFERENCES [Topic] ([TopicId])
ON DELETE CASCADE
When I try to run the scripts I get the following message:
{"Introducing FOREIGN KEY constraint 'FK_TopicSubject'
on table 'Topic' may cause cycles or multiple cascade paths.
Specify ON DELETE NO ACTION or ON UPDATE NO ACTION,
or modify other FOREIGN KEY constraints.\r\nCould not create constraint.
See previous errors."}
What I really need is for when a person tries to DELETE a subject when there are topics for the delete to fail. If I include neither DELETE ON CASCADE or DELETE NO ACTION then will this happen. If not then how can I stop the delete on subject happening if there are Topics for that subject?
Short answer is: If you don’t want cascade updates and deletions then use ON DELETE NO ACTION. Same applies for Update.
Here is a copy from MSDN article (it’s SQL Server 2000 but same rules still apply)
ON DELETE NO ACTION
Specifies that if an attempt is made to delete a row with a key referenced by foreign keys in existing rows in other tables, an error is raised and the DELETE is rolled back.
ON UPDATE NO ACTION
Specifies that if an attempt is made to update a key value in a row whose key is referenced by foreign keys in existing rows in other tables, an error is raised and the UPDATE is rolled back.
Please refer to this link. It has given a detail explanation of this error, and also has suggested to create a trigger as an alternative.
Foreign key constraint may cause cycles or multiple cascade paths?