Deleting tables with referential integrity - get REFERENCE constraint - sql-server

I want to delete a User (parent table) which has blog comment(s) and blog comment reply(s)
I have it coded to 1st delete the BlogCommentReply (child to the BlogComment), then the BlogComment (parent to BlogCommentReply), then the user (parent to both).
I get the error:
The DELETE statement conflicted with the REFERENCE constraint "FK_BlogCommentReply_UserId". The conflict occurred in database "DBGbngDev", table "dbo.BlogCommentReply", column 'UserId'.
I have FK keys on the BlogCommentReply and BlogComment tables.
1.) Did I create the table structure correctly?
2.) Do I need cascades - why?
3.) Is the delete code order correct?
_ I am of the belief that the parent table cannot be deleted until the
children are deleted first. Is that correct?
Table creates:
CREATE TABLE [dbo].[User]
(
[UserId] [int] IDENTITY(1,1) NOT NULL, -- PK
other columns....
CONSTRAINT [PK_User] PRIMARY KEY CLUSTERED
(
[UserId] ASC
) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS =
ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
CREATE TABLE [dbo].[BlogComment]
(
[BlogCommentId] [int] IDENTITY(1,1) NOT NULL, -- PK
[UserId] [int] NOT NULL, -- FK
other columns....
CONSTRAINT [PK_BlogComment] PRIMARY KEY CLUSTERED
(
[BlogCommentId] ASC
) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON,
ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
ALTER TABLE [dbo].[BlogComment] WITH CHECK ADD CONSTRAINT [FK_BlogComment_UserId] FOREIGN
KEY([UserId]) REFERENCES [dbo].[User] ([UserId])
GO
CREATE TABLE [dbo].[BlogCommentReply]
(
[BlogCommentReplyId] [int] IDENTITY(1,1) NOT NULL, -- PK
[UserId] [int] NOT NULL, -- FK
[BlogCommentId] [int] NOT NULL, -- FK
other columns....
CONSTRAINT [PK_BlogCommentReply] PRIMARY KEY CLUSTERED
(
[BlogCommentReplyId] ASC
) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS =
ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
ALTER TABLE [dbo].[BlogCommentReply] WITH CHECK ADD CONSTRAINT [FK_BlogCommentReply_UserId] FOREIGN
KEY([UserId]) REFERENCES [dbo].[User] ([UserId])
GO
ALTER TABLE [dbo].[BlogCommentReply] WITH CHECK ADD CONSTRAINT [FK_BlogCommentReply_BlogCommentId]
FOREIGN KEY([BlogCommentId]) REFERENCES [dbo].[BlogComment] ([BlogCommentId])
GO
Stored procedure (simplified for discussion) that does the deletes:
DELETE dbo.BlogCommentReply
FROM dbo.BlogComment a
WHERE ( BlogCommentReply.BlogCommentId = a.BlogCommentId AND BlogCommentReply.UserId = #a_UserId )
DELETE dbo.BlogComment
WHERE UserId = #a_UserId
DELETE dbo.[User]
WHERE ( UserId = #a_UserId )

It appears that your FK's are doing their jobs and preventing you from creating orphaned rows when deleting data from BlogCommentReply.
Your first DELETE from BlogCommentReply can be rewritten as two statements to make work correctly:
-- Remove replies added by the deleted user
DELETE
FROM dbo.BlogCommentReply
WHERE (UserId = #a_UserId)
-- Remove blog comment replies added by other users creating replies to a comment made by the deleted user
DELETE
FROM dbo.BlogCommentReply
WHERE (BlogCommentId IN (
SELECT BlogCommentId
FROM BlogComment
WHERE (UserId = #a_UserId)
))
In the first statement you are simply trying to delete rows from BlogCommentReply that use a FK to the User table.
In the second statement you delete any reply to a blog comment that will be deleted.
In response to your specific questions:
1. Did I create the table structure correctly?
It looks fine although there are several ways to achieve the same.
2. Do I need cascades - why?
No. Cascade deletes reek of poor design and lazy development. Try to
avoid them (note this is an opinion not a statement of fact).
There are some interesting points in this question.
3. Is the delete code order correct?
Yes, delete from the tables in order of key precidence.
4. I am of the belief that the parent table cannot be deleted until the children are deleted first. Is that correct?
Yes indeed, without cascaded deletes that is correct and, in your
case, the error thrown shows that the referential integrity of the
database is sound.

Related

How do I insert a new record to a table containing just an IDENTITY column?

I have a single-column table where the column is a primary key and clustered index. It is used on other tables to relate records together. It doesn't seem an Insert statement is the way to go, there's no other columns to populate. It's a bit cumbersome to SET IDENTITY_INSERT off and on, etc.
I just need to "increment" the primary key of the table to the next integer value.
I believe it's an easy problem to solve, but I'm at that stage of mental exhaustion where the wheel is still spinning but the hamster is dead.
Here is a script to recreate the table I'm working with.
CREATE TABLE [dbo].[PKOnly]
(
[Id] [BIGINT] IDENTITY(1,1) NOT NULL,
CONSTRAINT [PK_PKOnly]
PRIMARY KEY CLUSTERED ([Id] ASC)
WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF,
IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON,
ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY];
You can use DEFAULT VALUES:
INSERT dbo.PKOnly DEFAULT VALUES;
Example db<>fiddle
Note this will also work if you have other columns with defaults.

Is there a technical reason why you can't have multiple cascade paths in SQL?

I've been trying to implement a table in TSQL that references another table twice in 2 different columns, and applying cascade UPDATE/DELETE to those references:
CREATE TABLE [dbo].[Discussion](
[id] [bigint] IDENTITY(100000,1) NOT NULL,
[name] [nvarchar](255) NOT NULL,
[originated_user_id] [bigint] NOT NULL,
CONSTRAINT [PK_Discussion] PRIMARY KEY CLUSTERED
(
[id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO
CREATE TABLE [dbo].[User_Badge_Count](
[id] [bigint] IDENTITY(1,1) NOT NULL,
[discussion_id] [bigint] NULL,
[chat_id] [bigint] NULL,
[count] [int] NOT NULL,
CONSTRAINT [PK_User_Badge_Count] PRIMARY KEY CLUSTERED
(
[id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO
ALTER TABLE [dbo].[User_Badge_Count] ADD CONSTRAINT [DF_User_Badge_Count_count] DEFAULT ((0)) FOR [count]
GO
ALTER TABLE [dbo].[User_Badge_Count] WITH CHECK ADD CONSTRAINT [FK_User_Badge_Count_Chat] FOREIGN KEY([chat_id])
REFERENCES [dbo].[Discussion] ([id]) ON DELETE CASCADE ON UPDATE CASCADE
GO
ALTER TABLE [dbo].[User_Badge_Count] CHECK CONSTRAINT [FK_User_Badge_Count_Chat]
GO
ALTER TABLE [dbo].[User_Badge_Count] WITH CHECK ADD CONSTRAINT [FK_User_Badge_Count_Discussion] FOREIGN KEY([discussion_id])
REFERENCES [dbo].[Discussion] ([id]) ON DELETE CASCADE ON UPDATE CASCADE
GO
ALTER TABLE [dbo].[User_Badge_Count] CHECK CONSTRAINT [FK_User_Badge_Count_Discussion]
GO
It's done this way because we can consider a discussion to be a "discussion" or a "chat", and we want to reference as such in the badge table. However, trying to create these 2 cascades to the same table results in the TSQL error:
Introducing FOREIGN KEY constraint 'FK_User_Badge_Count_Discussion' on table 'User_Badge_Count' may cause cycles or multiple cascade paths. Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints.
Is there a technical reason for this restriction? It seems a bit arbitrary; the DBMS could just update/delete the row if either foreign key referenced is updated/deleted?

Difference between 'Foreign Key Relations' window and 'Keys' in Object explorer SSMS

In the image, the Foreign Key Relationships window is showing the FK for the itemModifier_Rel table while on the object explorer it doesn't shows the same results.
What are the differences between these?
I'm having a problem of duplicated keys, some DBA have a wrong process that causes this and I'm trying to fix it.
I deleted the duplicates but now I'm seeing this. Although the resulting CREATE TABLE script for the table shows just 3 FK while on the Foreign Key Relationships I'm seeing 6
Here's the table's script after dropping the duplicated FK
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[itemModifier_Rel](
[ItemModifierTypeID] [bigint] NOT NULL,
[ItemID] [bigint] NOT NULL,
[ModifierItemID] [bigint] NULL,
[ModifierSequenceID] [bigint] NULL,
PRIMARY KEY NONCLUSTERED
(
[ItemModifierTypeID] ASC,
[ItemID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
ALTER TABLE [dbo].[itemModifier_Rel] WITH CHECK ADD FOREIGN KEY([ItemID])
REFERENCES [dbo].[item] ([ItemID])
GO
ALTER TABLE [dbo].[itemModifier_Rel] WITH CHECK ADD FOREIGN KEY([ItemModifierTypeID])
REFERENCES [dbo].[itemModifierType_Cat] ([ItemModifierTypeID])
GO
ALTER TABLE [dbo].[itemModifier_Rel] WITH CHECK ADD FOREIGN KEY([ModifierItemID])
REFERENCES [dbo].[item] ([ItemID])
GO
On left you see the keys constraint names under Keys. On right you have the dialog for setting up key properties visually. That dialog is listing all the foreign keys of your table plus all the foreign keys that reference to table. IOW you have 3 FK + 2 references to your table PK in that picture.

Multiple cascade paths warning and not sure why. The alternative if I can't?

I originally had the following two tables.
tbl_Vehicle
VehicleID int identity
tbl_VehicleAssignment
RecordID int identity
VehicleID int FK
EmployeeID int
Original bit
When a vehicle comes in, it has an originally assignee, noted by the [Original] column in the second table above. Also set ON DELETE CASCADE, so if a vehicle is deleted, so are any associated assignment records. No problem there.
I decided this wasn't the best way to organize the data, and adding rules on the VehicleAssignment table to enforce only one vehicle can have a single 1 for Original seemed expensive in the long run. When I say that, I have several tables like this, some of which will grow quite large, so thinking of performance.
I then decided to change my tables like so:
tbl_Vehicle
VehicleID int identity
Orig_Assignment int NULL
tbl_VehicleAssignment
RecordID int identity
VehicleID int FK
EmployeeID int
The problem is, I then wanted to add a constraint so that if RecordID in tbl_VehicleAssignment was deleted, it set Orig_Assignment back to NULL.
Getting the following error:
Introducing FOREIGN KEY constraint 'FK_Orig_Vehicle_VehicleAssignment'
on table 'tbl_Vehicle' may cause cycles or multiple cascade paths.
Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other
FOREIGN KEY constraints.
But I'm not sure why exactly. If a vehicle is deleted, it cascades and deletes assignment records. But I'd also like to set [Orig_Assignment] to NULL when its associated record is deleted. I don't see how the two cross paths to cause multiple cascade paths. What's interesting though, is if I use the table designer wizard to create this, it actually saves the constraint in the tbl_VehicleAssignment table, but fails on the tbl_Vehicle table, and although closing the dialog and looking at the relationships in the tables look correct, there's obviously something wrong.
If I can't get around this, what would be the best method?
1. Trigger
I'd rather stay away from this
2. Another table for originals that allows nulls
So it would be like:
tbl_VehicleOriginal
VehicleID int
Orig_Assignment int NULL
Orig_SomethingElse int NULL
Orig_Etc. int NULL
This would create a row for every row in the tbl_Vehicle table.
3. Another table for originals but only when present
Such as:
tbl_VehicleOriginal
VehicleID int PK
RecordType PK
RecordID FK
I suppose my question is, why can't I just add the constraint? If it's not possible, what's the best way to organize the data?
UPDATE - Scripts to build the example
CREATE TABLE [dbo].[A_Vehicle](
[VehicleID] [int] IDENTITY(1,1) NOT NULL,
[Orig_Assignment] [int] NULL,
CONSTRAINT [PK_A_Vehicle] PRIMARY KEY CLUSTERED
(
[VehicleID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
CREATE TABLE [dbo].[A_VehicleAssignment](
[RecordID] [int] IDENTITY(1,1) NOT NULL,
[VehicleID] [int] NOT NULL,
CONSTRAINT [PK_A_VehicleAssignment] PRIMARY KEY CLUSTERED
(
[RecordID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
ALTER TABLE [dbo].[A_VehicleAssignment] WITH CHECK ADD CONSTRAINT [FK_A_VehicleAssignment_A_Vehicle] FOREIGN KEY([VehicleID])
REFERENCES [dbo].[A_Vehicle] ([VehicleID])
ON DELETE CASCADE
GO
ALTER TABLE [dbo].[A_VehicleAssignment] CHECK CONSTRAINT [FK_A_VehicleAssignment_A_Vehicle]
GO
-- The below will fail
ALTER TABLE [dbo].[A_Vehicle] WITH CHECK ADD CONSTRAINT [FK_Orig_A_VehicleAssignment_A_Vehicle] FOREIGN KEY([Orig_Assignment])
REFERENCES [dbo].[A_VehicleAssignment] ([RecordID])
ON DELETE SET NULL
GO

Inserting records in database tables using "inheritance"

I have a table whose primary key is also a foreign key to the primary key of another table (i.e. "inheritance" as simulated in a database).
/****** Object: Table [dbo].[BaseClass] Script Date: 07/15/2011 18:17:27 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[BaseClass](
[ID] [int] IDENTITY(1,1) NOT NULL,
[Title] [nvarchar](50) NOT NULL,
[Description] [nvarchar](max) NOT NULL,
CONSTRAINT [PK_BaseClass] PRIMARY KEY CLUSTERED
(
[ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
USE [TestConcepts]
GO
/****** Object: Table [dbo].[DerivedTable] Script Date: 07/15/2011 18:17:49 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[DerivedTable](
[ID] [int] IDENTITY(1,1) NOT NULL,
[SpecialProperty] [uniqueidentifier] NOT NULL,
CONSTRAINT [PK_DerivedTable] PRIMARY KEY CLUSTERED
(
[ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
ALTER TABLE [dbo].[DerivedTable] WITH CHECK ADD CONSTRAINT [FK_DerivedTable_BaseClass] FOREIGN KEY([ID])
REFERENCES [dbo].[BaseClass] ([ID])
GO
ALTER TABLE [dbo].[DerivedTable] CHECK CONSTRAINT [FK_DerivedTable_BaseClass]
GO
What is the proper way to insert records in this situation? Obviously an insert doesn't return the PK of the inserted row (plus the child's table's PK is identity as well).
Here are a few examples of this pattern.
The sub-type table should not have auto-increment ID, the ID matches the one in the super-type table.
The basic technique (using your example) looks something like
DECLARE #MY_ID integer;
INSERT INTO BaseTable(Title, Description)
VALUES ('title_here', 'blah, blah');
SELECT #MY_ID = SCOPE_IDENTITY();
INSERT INTO DerivedTable(ID, SpecialProperty)
VALUES (#MY_ID, newid()); -- the SpecialProperty is uniqueidentifier
One approach is to create a view, one for each sub-type table, or just one over all of them. Then create an INSTEAD OF INSERT TRIGGER on the view and use the technique inside the trigger.
You may also find this technique for capturing multiple inserted IDs useful too.
Nobody calls this "inheritance". That's not what it is. It's a relation, the R in RDBMS.
INSERTs do tell you the PK of the just-inserted row. Look at ##SCOPE_IDENTITY on SQL Server.
The DERIVEDCLASS table could have an auto-incremented (identity) PK but if so there must be another column that is a foreign key reference back to BASETABLE:
BASETABLE
id int pk autoincrement
baseattribute1
baseattribute2
etc etc
DERIVEDTABLE
id int pk autoincrement
**baseid** foreign key references BASETABLE(id)
extendedattribute1
extendedattribute2
This would permit multiple derivations of each base entity. Placing a unique index on DERIVEDTABLE.baseid or making baseid the PK would prevent this, if that is desired.
The following would instantiate the members of the base class and their derived instances and/or extended properties if any [which it is will depend on whether baseid has unique constraint in DerivedTable; if the latter it could be the PK in a one-to-one relationship with BaseTable, rather than a many-to-one]:
select * from BASETABLE
LEFT JOIN DERIVEDTABLE
on BASETABLE.id = DERIVEDTABLE.baseid
Instances of the base class that have not been extended will have NULL in the extendedattribute columns.
To find only those entities that have been extended use an inner join:
select * from DERIVEDTABLE
inner join BASETABLE
on DERIVEDTABLE.baseid = BASETABLE.id

Resources