【发布时间】:2016-05-23 08:17:02
【问题描述】:
如果我的数据库中有一个简单的 User 表和一个以 User.id 作为外键的简单 Item 表:
(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)
错误表明这“可能导致循环或多个级联路径”。我看到推荐的解决方法是
- 重新设计表格,但我不知道如何;或者,通常表示为"a last resort"
- 使用触发器。
所以我像这样删除ON DELETE CASCADE 和instead use triggers (documentation):
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,它显示了两个删除调用之间的死锁。
解决这些菱形删除级联的正确方法是什么?
【问题讨论】:
-
这适用于哪个 RDBMS?请添加标签以指定您使用的是
mysql、postgresql、sql-server、oracle还是db2- 或其他完全不同的东西。 -
既然你说测试只是“整体”失败,我猜你的 DELETE 触发器之间没有发生死锁,这对我来说似乎很好。因此,也许您需要进一步了解触发器。
-
他们在删除触发器上(尽管很难从分析器中得到它。)我将编辑问题并放入屏幕截图和死锁的更多细节,虽然它是一个但棘手,因为我在问题中给出的示例是从真实表格中简化而来的。
-
...“其他”用户可以收集其他用户的物品吗?如果是这样,我敢打赌那就是导致问题的原因。你有冲突的删除都是为了同一件事。
-
否;但是还有其他表允许 UserA 收藏一些 UserB 的集合。我的单元测试(还)没有行使这一点,所以这不是死锁的根源。保持这些建议通过,谢谢
标签: sql sql-server dml