2016-02-12 6 views
3

Если у меня есть простая таблица пользователей в моей базе данных и простую таблицу Item с user.id в качестве внешнего ключа, таким образом:каскадных ромбовидные удалений в SQL

(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. Использовать триггеры.

Так я удалить 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, который показывает тупик между двумя вызовами на удаление.

Каков правильный способ решения этих каскадов удаления алмазов?

+0

Какая СУБД для этого? Добавьте тег, чтобы указать, используете ли вы 'mysql',' postgresql', 'sql-server',' oracle' или 'db2' - или что-то еще. –

+0

Поскольку вы говорите, что тесты не терпят неудачу «массово», я думаю, что тупик не встречается среди триггеров DELETE, которые мне кажутся прекрасными. Поэтому, возможно, вам нужно смотреть дальше, чем просто триггеры. – Oliver

+0

Они включены в триггеры удаления (хотя из профилировщика это сложно получить). Я отредактирую вопрос и поставлю скриншот и еще одну деталь тупика, хотя это довольно сложно, поскольку образец, который я имею заданный в вопросе, упрощается из реальных таблиц. – dumbledad

ответ

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 будет иметь логику, очень похожую на описанную выше процедуру.

+0

Спасибо. Я собираюсь удалить триггеры и переписать их в качестве хранимых процедур, подобных этому. Надеюсь, это исправит тупик - я отчитаю. – dumbledad

+0

Он сделал - больше не тупик. еще раз спасибо – dumbledad

2

Несколько способов рабочего приходит на ум:

  1. Не удалять пользователя, просто отключить его. Добавьте поле BIT active и установите его для 0 для деактивированных пользователей. Простая, легкая, быстрая и поддерживает журнал, который есть у пользователей в вашей системе и что такое их состояние. Обычно вы не должны удалять такую ​​информацию о пользователе, вы хотите сохранить ее для использования в будущем.

  2. Не полагайтесь на каскады и триггеры, обрабатывайте их самостоятельно в коде. Каскады и триггеры могут быть трудными для поддержания и их поведение трудно предсказать (см. Тупик, который вы испытываете).

  3. Если вы не можете/не хотите делать что-либо из вышеперечисленного, подумайте об удалении всего из триггера удаления пользователя. Сначала disable the delete triggers при отправке таблиц, сделайте все свои удаления, а затем enable the delete triggers при ссылках на таблицы.

+1

...вы все равно можете получить блокировки от кода, тем более, что вы все еще используете (в значительной степени) тот же самый оператор, только из более медленного местоположения. Вероятность _ возможно лучше (что такое происходит), но фактическое предсказание порядка не меняется. Мне не нравится отключать триггеры как часть «стандартного действия, инициированного клиентом». Во-первых, это предотвратило бы одновременные вызовы. –

+0

Drat - понимание и исправление этих тупиков - вот что мне нужно. – dumbledad

+0

@ Clockwork-Muse Как я уже сказал в 2, триггеры - это такой беспорядок ... Я использую только тех, если нет другого пути. Оказывается, я никогда их не использую. Я бы выбрал вариант 1, почему вы хотите удалить пользователя, если вы можете просто отключить их? –

1

Еще одна вещь, которую нужно попробовать - установить уровень изоляции SERIALIZABLE в вашем триггере при удалении пользователя/элемента/коллекции. Поскольку вы удаляете много элементов/коллекций/собранных элементов при удалении пользователя, если другая транзакция INSERT во время этого прогона может вызвать проблемы. SERIALIZABLE решает это в некоторой степени.

SQL-сервер использует этот уровень изоляции на каскадное удаление именно по этой причине: http://blogs.msdn.com/b/conor_cunningham_msft/archive/2009/03/13/conor-vs-isolation-level-upgrade-on-update-delete-cascading-ri.aspx

+0

Ну, вы можете _still_ тупик с 'SERIALIZABLE' (на самом деле, это проще, так как он должен эскалировать определенные вещи). Вы должны быть очень осторожны с упорядочением операций, чтобы предотвратить появление таких ссылок. –

Смежные вопросы