INSTEAD OF DELETE Trigger conflict with ON DELETE CASCADE FK - sql-server

Batches can have multiple Bills which can have multiple BillLines. I have ON DELETE CASCADE FKs between them so that if you delete a Batch, the associated Bill and BillLine records also get deleted. If you delete a Bill, the associated BillLines get deleted but the Batch record is not affected. Now I need to prevent the delete of a Bill if there is a certain data condition with one or more of the associated BillLine records.
Table Bill clearly needs an INSTEAD OF DELETE trigger. BillLine.BillId has an ON DELETE CASCADE FK referencing Bill.BillId. It makes sense that I need to make that FK ON DELETE NO ACTION instead, because the INSTEAD OF DELETE trigger effectively replaces the CASCADE functionality. When I delete a Bill, the INSTEAD OF DELETE will either delete the associated BillLine records or raise an exception depending on certain data conditions. So far, so good.
However, because Bill.BatchId has an ON DELETE CASCADE FK referencing Batch.BatchId, SQL Server will not let me create the trigger. This I do not understand. Why should I have to build an INSTEAD OF DELETE trigger on Batch just because I have one on Bill?
The code to create the tables and keys below (with all extraneous columns and keys omitted) is how things are now, with no ON DELETE CASCADE clauses. The question is, why can't FK_Bill_Batch_BatchId have that clause instead of my having to create an additional INSTEAD OF DELETE trigger?
CREATE TABLE [Batch](
[BatchId] [bigint] NOT NULL,
CONSTRAINT [PK_Batch_BatchId] PRIMARY KEY CLUSTERED
(
[BatchId] ASC
)
)
CREATE TABLE [Bill](
[BillId] [bigint] NOT NULL,
[BatchId] [bigint] NOT NULL,
[ReversesBillId] [bigint] NULL,
CONSTRAINT [PK_Bill_BillId] PRIMARY KEY CLUSTERED
(
[BillId] ASC
)
)
ALTER TABLE [Bill] WITH CHECK ADD CONSTRAINT [FK_Bill_Batch_BatchId] FOREIGN KEY([BatchId])
REFERENCES [Batch] ([BatchId])
ALTER TABLE [Bill] WITH NOCHECK ADD CONSTRAINT [FK_Bill_ReversesBillId] FOREIGN KEY([ReversesBillId])
REFERENCES [Bill] ([BillId])
CREATE TABLE [BillLine](
[BillLineId] [bigint] NOT NULL,
[BillId] [bigint] NOT NULL,
[ReversedByBillLineId] [bigint] NULL,
CONSTRAINT [PK_BillLine_BillLineId] PRIMARY KEY CLUSTERED
(
[BillLineId] ASC
)
)
ALTER TABLE [BillLine] WITH CHECK ADD CONSTRAINT [FK_BillLine_Bill_BillId] FOREIGN KEY([BillId])
REFERENCES [Bill] ([BillId])
ALTER TABLE [BillLine] WITH CHECK ADD CONSTRAINT [FK_BillLine_ReversedByBillLineId] FOREIGN KEY([ReversedByBillLineId])
REFERENCES [BillLine] ([BillLineId])
GO
CREATE TRIGGER [Bill_Delete]
ON [Bill]
INSTEAD OF DELETE
AS
BEGIN
SET NOCOUNT ON
DECLARE #BillId UNIQUEIDENTIFIER
DECLARE myCursor CURSOR LOCAL FORWARD_ONLY
FOR SELECT b.[BillId]
FROM deleted b
JOIN [Batch] bt on b.[BatchId] = bt.[BatchId]
OPEN myCursor
FETCH NEXT FROM myCursor INTO #BillId
WHILE ##FETCH_STATUS = 0
BEGIN
-- Delete BillLine records reversed by another BillLine in the same Bill
DELETE FROM [BillLine]
WHERE [BillId] = #BillId
AND [ReversedByBillLineId] IN
(SELECT bl.[BillLineId]
FROM [BillLine] bl
WHERE bl.BillId = #BillId
);
-- Delete all remaining BillLine records for the Bill
-- If the BillLine is reversed by a BillLine in a different Bill, the FK will raise an exception.
-- That is the desired behavior.
DELETE FROM [BillLine]
WHERE [BillId] = #BillId;
-- Delete the Bill
DELETE FROM [Bill]
WHERE [BillId] = #BillId;
FETCH NEXT FROM myCursor INTO #BillId
END
END
GO
CREATE TRIGGER [Batch_Delete]
ON [Batch]
INSTEAD OF DELETE
AS
BEGIN
SET NOCOUNT ON
DECLARE #BatchId UNIQUEIDENTIFIER
DECLARE myCursor CURSOR LOCAL FORWARD_ONLY
FOR SELECT [BatchId]
FROM deleted
OPEN myCursor
FETCH NEXT FROM myCursor INTO #BatchId
WHILE ##FETCH_STATUS = 0
BEGIN
-- Delete all Bill records for the Batch.
-- Another INSTEAD OF DELETE trigger on Bill will attempt to delete the associated BillLine records in the correct order.
-- If the BillLine is reversed by a BillLine in a different Bill, FK_BillLine_ReversedByBillLineId will raise an exception.
-- That is the desired behavior.
DELETE FROM [Bill]
WHERE [BatchId] = #BatchId;
FETCH NEXT FROM myCursor INTO #BatchId
END
END
If you try to replace the Batch_Delete trigger with ON DELETE CASCADE:
DROP TRIGGER [Batch_Delete]
ALTER TABLE [Bill] DROP CONSTRAINT [FK_Bill_Batch_BatchId];
ALTER TABLE [Bill] WITH CHECK ADD CONSTRAINT [FK_Bill_Batch_BatchId] FOREIGN KEY([BatchId])
REFERENCES [Batch] ([BatchId]) ON DELETE CASCADE;
You'll get this:
Msg 1787, Level 16, State 0, Line 2
Cannot define foreign key constraint 'FK_Bill_Batch_BatchId' with cascaded DELETE or UPDATE on table 'Bill' because the table has an INSTEAD OF DELETE or UPDATE TRIGGER defined on it.
Msg 1750, Level 16, State 0, Line 2
Could not create constraint. See previous errors.
I don't understand why an ON DELETE CASCADE in this direction should have anything to do with the INSTEAD OF DELETE trigger on the Bill table.

I know this is an old question, but it deserves an answer:
The reason why you cannot specify ON DELETE CASCADE when your child table has an INSTEAD OF DELETE trigger defined is because in your trigger you may decide to not delete the child table rows thus impeding the cascade to take effect.
Since there is no certainty that a cascade is possible the database doesn't know how to handle that situation and thus leaves the issue to the developer to solve.

In my opinion you do not need an INSTEAD OF trigger.
INSTEAD OF trigger work always instead of an operation.
You want to throw an exception in some cases - and delete in others.
So you could use DELETE CASCADE and a normal (AFTER) trigger.
Inside the trigger you can RAISERROR the exception and probably ROLLBACK your transaction.
(There is always an implicit transaction around a trigger)

Related

Error on delete trigger

This trigger should delete a row from the parent table that is not deleted from the child table. The error is in the image below.
My code attempt:
CREATE TRIGGER ProductDeleted ON Product
for DELETE AS
BEGIN
DELETE FROM OrderItem
WHERE ProductID = (SELECT ProductID FROM DELETED)
END
help me please
You could simplify it by adding a CASCADE DELETE hint on a foreign key constraint such as
CREATE TABLE OrderItem
(
ID INT ,
ProductID NOT NULL UNIQUE
CONSTRAINT fk_Products
REFERENCES Products (ID) ON DELETE CASCADE
);
Since you already have a table, all you need to do is drop the constraint and recreate a new one.
ALTER TABLE OrderItem DROP
CONSTRAINT fk_ProductID;
ALTER TABLE OrderItem ADD
CONSTRAINT fk_ProductID
FOREIGN KEY (ID)
REFERENCES Product (ID)
ON DELETE CASCADE;
What this means, is that , any time you delete a record from the parent table (Product), child records from (OrderItem)will be deleted as well, so you dont have to use triggers, unless if you want to do some recording.
If you are really insisting on using triggers then you can tweak it a little bit like this :
ALTER TRIGGER ProductDeleted on Product
INSTEAD OF DELETE AS
BEGIN
SET NOCOUNT ON;
/* First we are deleting referenced columns in OrderItem table */
DELETE FROM OrderItem
where ProductID IN (select deleted.ID /* Columns from product Table */ from deleted)
/* Now we are doing actual delete statement */
DELETE FROM Products where ID IN (select deleted.ID from deleted)
END
But once again you should consider using ON CASCADE DELETE, its much simpler to setup, easier to maintain and you can have only one INSTEAD OF trigger per table, so if you ever need to do something more meaningful you would have to change this one, and add extra overhead.
Add SET NOCOUNT ON as the first thing in the trigger's body (after BEGIN).

TRANSACTION rollback not working as expected

I'm doing some DB schema re-structuring.
I have a script that looks broadly like this:
BEGIN TRAN LabelledTransaction
--Remove FKs
ALTER TABLE myOtherTable1 DROP CONSTRAINT <constraintStuff>
ALTER TABLE myOtherTable2 DROP CONSTRAINT <constraintStuff>
--Remove PK
ALTER TABLE myTable DROP CONSTRAINT PK_for_myTable
--Add replacement id column with new type and IDENTITY
ALTER TABLE myTable ADD id_new int Identity(1, 1) NOT NULL
GO
ALTER TABLE myTable ADD CONSTRAINT PK_for_myTable PRIMARY KEY CLUSTERED (id_new)
GO
SELECT * FROM myTable
--Change referencing table types
ALTER TABLE myOtherTable1 ALTER COLUMN col_id int NULL
ALTER TABLE myOtherTable2 ALTER COLUMN col_id int NOT NULL
--Change referencing table values
UPDATE myOtherTable1 SET consignment_id = Target.id_new FROM myOtherTable1 AS Source JOIN <on key table>
UPDATE myOtherTable2 SET consignment_id = Target.id_new FROM myOtherTable2 AS Source JOIN <on key table>
--Replace old column with new column
ALTER TABLE myTable DROP COLUMN col_id
GO
EXEC sp_rename 'myTable.id_new', 'col_id', 'Column'
GO
--Reinstate any OTHER PKs disabled
ALTER TABLE myTable ADD CONSTRAINT <PK defn>
--Reinstate FKs
ALTER TABLE myOtherTable1 WITH CHECK ADD CONSTRAINT <constraintStuff>
ALTER TABLE myOtherTable2 WITH CHECK ADD CONSTRAINT <constraintStuff>
SELECT * FROM myTable
-- Reload out-of-date views
EXEC sp_refreshview 'someView'
-- Remove obsolete sequence
DROP SEQUENCE mySeq
ROLLBACK TRAN LabelledTransaction
Obviously that's all somewhat redacted, but the fine detail isn't the important thing in here.
Naturally, it's quite hard to locate all the things that need to be turned off/editted before the core change (even with some meta-queries to help me), so I don't always get the script correct first time.
But I put in the ROLLBACK in order to ensure that the failed attempts left the DB unchanged.
But what I actually see is that the ROLLBACK doesn't occur if there were errors in the TRAN. I think I get errors about "no matching TRAN for the rollback"?
My first instinct was that it was about the GO statements, but https://stackoverflow.com/a/11121382/1662268 suggests that labeling the TRAN should have fixed that?
What's happening? Why don't the changes get rolled back properly if there are errors.
How can I write and test these scripts in such a way that I don't have to manually revert any partial changes if the script isn't perfect first time?
EDIT:
Additional comments based on the first answer.
If the linked answer is not applicable to this query, could you expand on why that is, and why it's different from the example that they had given in their answer?
I can't (or rather, I believe that I can't) remove the GOs, because the script above requires the GOs in order to compile. If I remove the GOs then later statements that depend on the newly added/renamed columns don't compile. and the query can't run.
Is there any way to work around this, to remove the GOs?
If you have any error which automatically causes the transaction to be rolled back then the transaction will roll back as part of the current batch.
Then, control will return back to the client tool which will then send the next batch to the server and this next batch (and subsequent ones) will not be wrapped in any transaction.
Finally, when the final batch is executed that tries to run the rollback then you'll get the error message you received.
So, you need to protect each batch from running when its not protected by a transaction.
One way to do it would be to insert our old fried GOTO:
GO
IF ##TRANCOUNT=0 GOTO NBATCH
...Rest of Code
NBATCH:
GO
or SET FMTONLY:
GO
IF ##TRANCOUNT=0 BEGIN
SET FMTONLY ON
END
...Rest of Code
GO
Of course, this won't address all issues - some statements need to be the first or only statement in a batch. To resolve these, we have to combine one of the above techniques with an EXEC of some form:
GO
IF ##TRANCOUNT=0 BEGIN
SET FMTONLY ON
END
EXEC sp_executesql N'/*Code that needs to be in its own batch*/'
GO
(You'll also have to employ this technique if a batch of code relies on work a previous batch has performed which introduces new database objects (tables, columns, etc), since if that previous batch never executed, the new object will not exist)
I've also just discovered the existence of the -b option for the sqlcmd tool. The following script generates two errors when run through SSMS:
begin transaction
go
set xact_abort on
go
create table T(ID int not null,constraint CK_ID check (ID=4))
go
insert into T(ID) values (3)
go
rollback
Errors:
Msg 547, Level 16, State 0, Line 7
The INSERT statement conflicted with the CHECK constraint "CK_ID". The conflict occurred in database "TestDB", table "dbo.T", column 'ID'.
Msg 3903, Level 16, State 1, Line 9
The ROLLBACK TRANSACTION request has no corresponding BEGIN TRANSACTION.
However, the same script saved as Abortable.sql and run with the following commandline:
sqlcmd -b -E -i Abortable.sql -S .\SQL2014 -d TestDB
Generates the single error:
Msg 547, Level 16, State 1, Server .\SQL2014, Line 1
The INSERT statement conflicted with the CHECK constraint "CK_ID". The conflict
occurred in database "TestDB", table "dbo.T", column 'ID'.
So, it looks like running your scripts from the commandline and using the -b option may be another approach to take. I've just scoured the SSMS options/properties to see if I can find something equivalent to -b but I've not found it.
Remove the 'GO', that finishes the transaction
Only ROLLBACK if completes - just use TRY/CATCH:
BEGIN TRANSACTION;
BEGIN TRY
--Remove FKs
ALTER TABLE myOtherTable1 DROP CONSTRAINT <constraintStuff>
ALTER TABLE myOtherTable2 DROP CONSTRAINT <constraintStuff>
--Remove PK
ALTER TABLE myTable DROP CONSTRAINT PK_for_myTable
--Add replacement id column with new type and IDENTITY
ALTER TABLE myTable ADD id_new int Identity(1, 1) NOT NULL
ALTER TABLE myTable ADD CONSTRAINT PK_for_myTable PRIMARY KEY CLUSTERED (id_new)
SELECT * FROM myTable
--Change referencing table types
ALTER TABLE myOtherTable1 ALTER COLUMN col_id int NULL
ALTER TABLE myOtherTable2 ALTER COLUMN col_id int NOT NULL
--Change referencing table values
UPDATE myOtherTable1 SET consignment_id = Target.id_new FROM myOtherTable1 AS Source JOIN <on key table>
UPDATE myOtherTable2 SET consignment_id = Target.id_new FROM myOtherTable2 AS Source JOIN <on key table>
--Replace old column with new column
ALTER TABLE myTable DROP COLUMN col_id
EXEC sp_rename 'myTable.id_new', 'col_id', 'Column'
--Reinstate any OTHER PKs disabled
ALTER TABLE myTable ADD CONSTRAINT <PK defn>
--Reinstate FKs
ALTER TABLE myOtherTable1 WITH CHECK ADD CONSTRAINT <constraintStuff>
ALTER TABLE myOtherTable2 WITH CHECK ADD CONSTRAINT <constraintStuff>
SELECT * FROM myTable
-- Reload out-of-date views
EXEC sp_refreshview 'someView'
-- Remove obsolete sequence
DROP SEQUENCE mySeq
ROLLBACK TRANSACTION
END TRY
BEGIN CATCH
print 'Error caught'
select ERROR_NUMBER() AS ErrorNumber, ERROR_MESSAGE() AS ErrorMessage;
END CATCH;

Deleting rows without checking foreign indexes, triggers, etc in SQL Server

I have a table that is referenced by 11 other tables as foreign key constraint. If I try to delete a row from it, apparently it checks the other tables to see if it should cascade delete other records, which is making the delete take too long. I've tried disabling constraints in the table I'm deleting and the other tables but it's not working (on the results panel I still get 11 lines of "0 rows affected").
I've already deleted the children, what should I so it doesn't go on these tables to check for cascades? Here's how I've been testing this:
BEGIN TRANSACTION
ALTER TABLE Table1 NOCHECK CONSTRAINT ALL
ALTER TABLE Table2 NOCHECK CONSTRAINT ALL
DELETE FROM Table2
WHERE id IN (100, 101)
ALTER TABLE Table2 CHECK CONSTRAINT ALL
ALTER TABLE Table1 CHECK CONSTRAINT ALL
ROLLBACK
BTW: You are in pretty dangerous position of breaking data integrity. Try this code:
CREATE TABLE C1(ID INT IDENTITY(1,1) PRIMARY KEY);
GO
INSERT INTO C1 DEFAULT VALUES;
GO 10
GO
SELECT * INTO C2 FROM C1;
GO
ALTER TABLE C2 WITH CHECK ADD CONSTRAINT FK_C12 FOREIGN KEY(ID) REFERENCES C1 (ID);
GO
BEGIN TRANSACTION
SELECT is_not_trusted from sys.foreign_keys WHERE name = 'FK_C12';
ALTER TABLE C1 NOCHECK CONSTRAINT ALL
ALTER TABLE C2 NOCHECK CONSTRAINT ALL
DELETE FROM C1 WHERE ID = 5;
ALTER TABLE C2 CHECK CONSTRAINT ALL
ALTER TABLE C1 CHECK CONSTRAINT ALL
SELECT is_not_trusted from sys.foreign_keys WHERE name = 'FK_C12';
ROLLBACK
That means after your FK is not trusted anymore you might come up with different/slower execution plans and an additional and maybe unnecessary I/O

Deleting tables with foreign key constraints

I have three tables
GroupTable
GroupId
Name
Each group has a one to many with Users
Users
UserId
Name
GroupId
And each User has one to many with 'Challenges'
Challenges
Name
UserId
I want to be able to delete a group with the users assigned to that specific group
I have tried this where I do manage to delete the Group based on id without getting a foreign key constraint error but ALL the users added to the user table and ALL the challenges get deleted as well
ALTER TABLE GroupTable NOCHECK CONSTRAINT ALL
ALTER TABLE UserTable NOCHECK CONSTRAINT ALL
ALTER TABLE Challanges NOCHECK CONSTRAINT ALL
DELETE FROM GroupTable
WHERE ID = #GroupId
DELETE FROM child
FROM Challanges as child
INNER JOIN UserTable AS parent
ON child.UserId = parent.ID
WHERE parent.GroupId = #GroupId
DELETE FROM parent
FROM UserTable AS parent
WHERE GroupId = GroupId
How can I ammend the above so that I only delete the gropu with the specific Users and there challenges assigned to the Group?
Do not disable the constraints for this, as it will compromise data integrity.
Either use on delete cascade option on the foreign keys, or delete the data from all three tables in the correct order, inside a single transaction.
To add on cascade delete to an existing foreign key you must use the alter table statement to drop the existing constraint and then add it again with the on delete cascade option:
ALTER TABLE table_name
DROP CONSTRAINT constraint_name
ALTER TABLE table_name
ADD CONSTRAINT constraint_name
FOREIGN KEY (column_name)
REFERENCES other_table_name(other_column_name) ON DELETE CASCADE
(of course, this can be done using ssms's design table window)
Deleting the rows from the related tables in the correct order will ensure you will encounter no problems with the existing foreign key constraints, wrapping all the delete statement in a single transaction will ensure you will only delete from every table or none at all:
DECLARE #GroupId int = 5
BEGIN TRY
BEGIN TRANSACTION
DELETE c
FROM Challanges c
INNER JOIN UserTable u ON(c.UserId = u.UserId)
WHERE u.GroupId = #GroupId
DELETE
FROM Users
WHERE GroupId = #GroupId
DELETE
FROM GroupTable
WHERE ID = #GroupId
COMMIT TRANSACTION
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0
ROLLBACK TRANSACTION
END CATCH

Set auto id to primary key column [duplicate]

This question already has answers here:
How to add identity to the column in SQL Server?
(4 answers)
Closed 8 years ago.
I have a table and primary key is already set to that table and now I want that column to be autoincrement. Table has many records. Is it possible? or which one is fastest way to do that?
I think you have to make some effort for this as you cannot create identity column on existing column. However you may have a workaround for this like first try this to add a new column having identity field:
ALTER TABLE dbo.Table_name
ADD ID INT IDENTITY
and then make your ID as primary key like this:
ALTER TABLE dbo.Table_name
ADD CONSTRAINT PK_YourTable
PRIMARY KEY(ID)
And yes you have to remove the old dependencies before performing the above steps like this:
ALTER TABLE Table_name
DROP CONSTRAINT PK_Table1_Col1
EDIT:-
From the source:
We can use ALTER TABLE...SWITCH to work around this by only modifying metadata. See Books Online for restrictions on using the SWITCH method presented below. The process is practically instant even for the largest tables.
USE tempdb;
GO
-- A table with an identity column
CREATE TABLE dbo.Source (row_id INTEGER IDENTITY PRIMARY KEY NOT NULL, data SQL_VARIANT NULL);
GO
-- Some sample data
INSERT dbo.Source (data)
VALUES (CONVERT(SQL_VARIANT, 4)),
(CONVERT(SQL_VARIANT, 'X')),
(CONVERT(SQL_VARIANT, {d '2009-11-07'})),
(CONVERT(SQL_VARIANT, N'áéíóú'));
GO
-- Remove the identity property
BEGIN TRY;
-- All or nothing
BEGIN TRANSACTION;
-- A table with the same structure as the one with the identity column,
-- but without the identity property
CREATE TABLE dbo.Destination (row_id INTEGER PRIMARY KEY NOT NULL, data SQL_VARIANT NULL);
-- Metadata switch
ALTER TABLE dbo.Source SWITCH TO dbo.Destination;
-- Drop the old object, which now contains no data
DROP TABLE dbo.Source;
-- Rename the new object to make it look like the old one
EXECUTE sp_rename N'dbo.Destination', N'Source', 'OBJECT';
-- Success
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
-- Bugger!
IF XACT_STATE() <> 0 ROLLBACK TRANSACTION;
PRINT ERROR_MESSAGE();
END CATCH;
GO
-- Test the the identity property has indeed gone
INSERT dbo.Source (row_id, data)
VALUES (5, CONVERT(SQL_VARIANT, N'This works!'))
SELECT row_id,
data
FROM dbo.Source;
GO
-- Tidy up
DROP TABLE dbo.Source;

Resources