Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Preview SQL DELETE for records which have ON CASCADE constraints

Tags:

sql-server

We have a master 'users' table and many tables that refer to the UserId either

  • Directly: UserId is a FK in the child table OR
  • Indirectly: another FK in the 'grandchild' table that references a record in the child table, that in turn refers to UserID via a FK constraint.

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
like image 528
DeepSpace101 Avatar asked Nov 23 '12 18:11

DeepSpace101


People also ask

Does On Update Cascade also delete?

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.

Is on delete cascade a constraint?

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.

Should I use on delete cascade?

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.


2 Answers

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.

like image 167
Ross Presser Avatar answered Sep 21 '22 05:09

Ross Presser


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.

like image 31
Dalex Avatar answered Sep 23 '22 05:09

Dalex