在SQL中级联菱形删除

时间:2016-02-12 08:47:17

标签: sql sql-server dml

如果我的数据库中有一个简单的User表,并且有一个简单的Item表,其中User.id作为外键,那么:

(id UNIQUEIDENTIFIER DEFAULT (NEWID()) NOT NULL,
name NVARCHAR (MAX) NULL,
email NVARCHAR (128) NULL,
authenticationId NVARCHAR (128) NULL,
createdAt DATETIME DEFAULT GETDATE() NOT NULL,
PRIMARY KEY (id))

CREATE TABLE Items
(id UNIQUEIDENTIFIER DEFAULT (NEWID()) NOT NULL,
userId UNIQUEIDENTIFIER NOT NULL,
name NVARCHAR (MAX) NULL,
description NVARCHAR (MAX) NULL,
isPublic BIT DEFAULT 0 NOT NULL,
createdAt DATETIME DEFAULT GETDATE() NOT NULL,
PRIMARY KEY (id),
FOREIGN KEY (userId) REFERENCES Users (id))

如果从表中删除用户,我需要首先删除所有相关项,以避免破坏参照完整性约束。使用CASCADE DELETE

可以轻松完成此操作
CREATE TABLE Items
(id UNIQUEIDENTIFIER DEFAULT (NEWID()) NOT NULL,
userId UNIQUEIDENTIFIER NOT NULL,
name NVARCHAR (MAX) NULL,
description NVARCHAR (MAX) NULL,
isPublic BIT DEFAULT 0 NOT NULL,
createdAt DATETIME DEFAULT GETDATE() NOT NULL,
PRIMARY KEY (id),
FOREIGN KEY (userId) REFERENCES Users (id) ON DELETE CASCADE)

但是,如果我也有引用用户的集合,以及将项目收集到集合中的表格,我遇到了麻烦,即以下附加代码不起作用。

CREATE TABLE Collections
(id UNIQUEIDENTIFIER DEFAULT (NEWID()) NOT NULL,
userId UNIQUEIDENTIFIER NOT NULL,
name NVARCHAR (MAX) NULL,
description NVARCHAR (MAX) NULL,
isPublic BIT DEFAULT 0 NOT NULL,
layoutSettings NVARCHAR (MAX) NULL,
createdAt DATETIME DEFAULT GETDATE() NOT NULL,
PRIMARY KEY (id),
FOREIGN KEY (userId) REFERENCES Users (id) ON DELETE CASCADE)

CREATE TABLE CollectedItems
(itemId UNIQUEIDENTIFIER NOT NULL,
collectionId  UNIQUEIDENTIFIER NOT NULL,
createdAt DATETIME DEFAULT GETDATE() NOT NULL,
PRIMARY KEY CLUSTERED (itemId, collectionId),
FOREIGN KEY (itemId) REFERENCES Items (id) ON DELETE CASCADE,
FOREIGN KEY (collectionId) REFERENCES Collections (id) ON DELETE CASCADE)

错误表示此"可能导致循环或多个级联路径"。我推荐的方法是

  1. 重新设计表格,但我看不出怎样;或者,通常表示为"a last resort"
  2. 使用触发器。
  3. 所以我删除ON DELETE CASCADEinstead use triggersdocumentation),如下所示:

    CREATE TRIGGER DELETE_User
       ON Users
       INSTEAD OF DELETE
    AS 
    BEGIN
     SET NOCOUNT ON
     DELETE FROM Items WHERE userId IN (SELECT id FROM DELETED)
     DELETE FROM Collections WHERE userId IN (SELECT id FROM DELETED)
     DELETE FROM Users WHERE id IN (SELECT id FROM DELETED)
    END
    
    CREATE TRIGGER DELETE_Item
       ON Items
       INSTEAD OF DELETE
    AS 
    BEGIN
     SET NOCOUNT ON
     DELETE FROM CollectedItems WHERE itemId IN (SELECT id FROM DELETED)
     DELETE FROM Items WHERE id IN (SELECT id FROM DELETED)
    END
    
    CREATE TRIGGER DELETE_Collection
       ON Collections
       INSTEAD OF DELETE
    AS 
    BEGIN
     SET NOCOUNT ON
     DELETE FROM CollectedItems WHERE collectionId IN (SELECT id FROM DELETED)
     DELETE FROM Collections WHERE id IN (SELECT id FROM DELETED)
    END
    

    然而,虽然巧妙但这失败了。我有一堆单元测试(用xUnit编写)。测试总是单独通过。但是大量运行一些随机失败的SQL死锁。在another answer中,我指向了SQL Profiler,它显示了两次删除调用之间的死锁。

    解决这些菱形删除级联的正确方法是什么?

3 个答案:

答案 0 :(得分:1)

想到几种工作方式:

  1. 请勿删除用户,只需将其停用即可。添加BIT字段active并为已停用的用户将其设置为0。简单,简单,快速,并维护日志系统中的用户及其相关状态。通常您不应该删除有关用户的此类信息,您希望将其保留以供将来参考。

  2. 不要依赖级联和触发器,在代码中自行处理。级联和触发器很难维护,并且很难预测其行为(参见您遇到的死锁)。

  3. 如果您不想/不想执行上述任何操作,请考虑从用户删除触发器中删除所有内容。引用表的第一个disable the delete triggers,删除所有删除,然后enable the delete triggers引用表。

答案 1 :(得分:1)

尝试的另一件事是在删除用户/项目/集合时在触发器中将隔离级别设置为SERIALIZABLE。由于您可能在删除用户时删除了许多项目/集合/收集的项目,因此在此次运行期间使用另一个事务INSERT会导致问题。 SERIALIZABLE在某种程度上解决了这个问题。

SQL-Server在级联删除时使用该隔离级别,原因如下: http://blogs.msdn.com/b/conor_cunningham_msft/archive/2009/03/13/conor-vs-isolation-level-upgrade-on-update-delete-cascading-ri.aspx

答案 2 :(得分:1)

我更喜欢具有自动级联操作,无论是DELETE还是UPDATE。只是为了安心。想象一下,您已经配置了级联删除,然后您的程序由于某些错误尝试删除错误的用户,即使数据库有一些与之相关的数据。相关表格中的所有相关数据都将在没有任何警告的情况下消失。

通常我确保首先使用显式单独的过程删除所有相关数据,每个相关表一个,然后删除主表中的行。删除将成功,因为引用的表中没有子行。

对于您的示例,我有一个专用存储过程DeleteUser,其中包含一个参数UserID,它知道哪些表与用户相关,以及应删除详细信息的顺序。此过程已经过测试,是删除用户的唯一方法。如果错误地将程序的其余部分尝试直接从Users表中删除一行,则如果相关表中存在某些数据,则此尝试将失败。如果错误删除的用户没有任何详细信息,则尝试将通过,但至少您不会丢失大量数据。

对于您的架构,该过程可能如下所示:

CREATE PROCEDURE dbo.DeleteUser
    @ParamUserID int
AS
BEGIN
    SET NOCOUNT ON; SET XACT_ABORT ON;

    BEGIN TRANSACTION;
    BEGIN TRY
        -- Delete from CollectedItems going through Items
        DELETE FROM CollectedItems
        WHERE CollectedItems.itemId IN
        (
            SELECT Items.id
            FROM Items
            WHERE Items.userId = @ParamUserID
        );

        -- Delete from CollectedItems going through Collections
        DELETE FROM CollectedItems
        WHERE CollectedItems.collectionId IN
        (
            SELECT Collections.id
            FROM Collections
            WHERE Collections.userId = @ParamUserID
        );

        -- Delete Items
        DELETE FROM Items WHERE Items.userId = @ParamUserID;

        -- Delete Collections
        DELETE FROM Collections WHERE Collections.userId = @ParamUserID;

        -- Finally delete the main user
        DELETE FROM Users WHERE ID = @ParamUserID;

        COMMIT TRANSACTION;
    END TRY
    BEGIN CATCH
        ROLLBACK TRANSACTION;
        ...
        -- process the error
    END CATCH;
END

如果你真的想设置级联删除,那么我只为Users表定义一个触发器。同样,没有带有级联删除的外键,但Users表上的触发器将具有与上述过程非常相似的逻辑。