We have a master 'users' table and many tables that refer to the UserId either
Now if everything is perfect, deleting a user should be as straightforward as deleting them in the master table and the ON CASCADE constraints rippling it thru the rest of the tables. The issue is that we're not 100% sure if every FK relationship in every table referenced (directly or indirectly) has the ON CASCADE constraint. We need some way to issue that delete and watch which tables SQL server actually touches to delete. I read this and tried it but it doesn't display any tables cascaded into - just the entries in the master table only
Here is what I tried:
DELETE umt
OUTPUT DELETED.*
FROM [OurAppDb].[dbo].[UserMasterTable] umt
WHERE umt.UserId LIKE 'ABCDABCD-ABCD-ABCD-ABCD-ABCDABCDABCD'
How can I see all the tables the above query would touch?
NOTE: The ON CASCADE constraint is a constraint in the database that we think we added for every table when each table was built. Example of it being added on one table
ALTER TABLE [dbo].[UserEmailPrefs]
WITH CHECK ADD CONSTRAINT [FK_UserEmailPrefs_UserMasterTable_UserId] FOREIGN KEY([UserId])
REFERENCES [dbo].[UserMasterTable] ([UserId])
ON DELETE CASCADE
GO
ALTER TABLE [dbo].[UserEmailPrefs] CHECK CONSTRAINT [FK_UserEmailPrefs_UserMasterTable_UserId]
GO
CASCADE. It is used in conjunction with ON DELETE or ON UPDATE. It means that the child data is either deleted or updated when the parent data is deleted or updated. SET NULL.
ON DELETE CASCADE constraint is used in MySQL to delete the rows from the child table automatically, when the rows from the parent table are deleted. For example when a student registers in an online learning platform, then all the details of the student are recorded with their unique number/id.
ON DELETE CASCADE is fine, but only when the dependent rows are really a logical extension of the row being deleted. For example, it's OK for DELETE ORDERS to delete the associated ORDER_LINES because clearly you want to delete this order, which consists of a header and some lines.
To inspect the referential constraints in your entire database that reference the UserMasterTable, use INFORMATION_SCHEMA views.
SELECT RC.CONSTRAINT_NAME, TU.TABLE_NAME, RC.DELETE_RULE, RC.UPDATE_RULE
FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS RC
INNER JOIN INFORMATION_SCHEMA.CONSTRAINT_TABLE_USAGE TU
ON RC.CONSTRAINT_CATALOG = TU.CONSTRAINT_CATALOG
AND RC.CONSTRAINT_NAME = TU.CONSTRAINT_NAME
INNER JOIN INFORMATION_SCHEMA.table_constraints TC
ON RC.unique_constraint_name = TC.CONSTRAINT_NAME
WHERE TC.TABLE_NAME='Users'
This returns a list of the referential constraints targeting UserMasterTable, and for each one, which table is referencing UserMasterTable, and what the ON DELETE and ON UPDATE rules are. From this you can quickly see which referential constraints are lacking the desired CASCADE rule. No need to get trigger-happy.
To extend this to "grandchild" references, add two more join clauses. To extend it to any number of levels, go for a recursive CTE.
I think you can do this in trigger(analyse [inserted] table and search for subordinated tables). As example: you can create table which will store queries for detecting Foreign Key links :
IF NOT EXISTS (
SELECT *
FROM [sys].tables T
WHERE T.NAME = 'FKCheck'
)
CREATE TABLE FKCheck (
TableName SYSNAME
,ChildTable SYSNAME
,QueryText NVARCHAR(MAX)
)
ELSE
EXEC('DROP TABLE FKCheck')
Then fill it with dynamic query extracted from metadata
;WITH CTE_FKs
AS (
SELECT FK.NAME
,OBJECT_name(fk.parent_object_id) AS ChildTable
,OBJECT_name(fk.referenced_object_id) AS ParentTable
,delete_referential_action_desc AS DeleteAction
,MainTable.NAME AS MainTableColumn
,ChildObject.NAME AS ChildColumnName
FROM sys.foreign_keys FK
INNER JOIN sys.foreign_key_columns FKC ON FKC.constraint_object_id = FK.object_id
INNER JOIN sys.columns AS ChildObject ON ChildObject.object_id = FKc.parent_object_id
AND FKC.parent_column_id = ChildObject.column_id
INNER JOIN sys.columns AS MainTable ON MainTable.object_id = FK.referenced_object_id
AND MainTable.column_id = FKC.referenced_column_id
)
,CTE_Tables
AS (
SELECT DISTINCT C.NAME
,C.ParentTable
,C.DeleteAction
,C.ChildTable
FROM [CTE_FKs] C
)
INSERT INTO [dbo].[FKCheck] (
TableName
,ChildTable
,QueryText
)
SELECT C.ParentTable,C.ChildTable
,'IF EXISTS (select 1 from inserted INNER JOIN ' + QUOTENAME(C.ChildTable) + ' ON ' + STUFF((
SELECT ' AND inserted.' + QUOTENAME(C2.MainTableColumn) + ' = ' + + QUOTENAME(C2.ChildTable) + '.' + QUOTENAME(C2.ChildColumnName)
FROM CTE_FKs C2
WHERE C2.ParentTable = C.ParentTable
AND C2.NAME = C.NAME
FOR XML PATH('')
,TYPE
).value('.', 'nvarchar(MAX)'), 1, 4, '') + ')
RAISERROR(''Relation with ' + QUOTENAME(C.ChildTable) +':'+ CASE C.DeleteAction
WHEN 'CASCADE'
THEN ' data will be deleted'
WHEN 'SET_NULL'
THEN ' set as NULL'
WHEN 'NO_ACTION'
THEN ' no default action'
ELSE 'Unknown'
END + ''')'
FROM [CTE_Tables] C
Your query in table will be looking like:
IF EXISTS (select 1 from inserted INNER JOIN [UserEmailPrefs] ON inserted.[UserId] = [UserEmailPrefs].[UserId])
RAISERROR('Relation with [UserEmailPrefs]: no default action')
IF EXISTS (select 1 from inserted INNER JOIN [UserEmail] ON inserted.[UserId] = [UserEmail].[UserId])
RAISERROR('Relation with [UserEmail]: set as NULL')
Then in trigger you can execute query to print message:
DECLARE @TableName SYSNAME = 'UserMasterTable';
DECLARE @sSQL NVARCHAR(MAX) = '';
SELECT @sSQL += F.QueryText + CHAR(10)
FROM FKCheck F
WHERE F.TableName = @TableName;
EXEC(@sSQL)
ROLLBACK
If you need to analyze more "distant" tables, you need to walk through hierarchy in FKCheck table.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With