Что делать, если я хочу использовать ограничения базы данных, но помечать их как удаленные вместо удаления? - PullRequest
6 голосов
/ 21 апреля 2009

Я работаю в проекте, где элементы базы данных не удаляются, а только помечаются как удаленные. Примерно так:

id   name     deleted
---  -------  --------
1    Thingy1  0
2    Thingy2  0
3    Thingy3  0

Я бы хотел иметь возможность определить что-то вроде УНИКАЛЬНОГО ограничения для столбца name. Кажется легко, верно?

Давайте представим сценарий, в котором «Thingy3» удаляется, и создается новый (возможно, спустя годы). Мы получаем:

id   name     deleted
---  -------  --------
1    Thingy1  0
2    Thingy2  0
3    Thingy3  1
...
100  Thingy3  0

С точки зрения пользователя он удалил элемент и создал новый. Очень похоже на удаление файла и создание нового файла. Поэтому для него очевидно, что новый элемент не связан и не привязан ни к каким данным, связанным со старым элементом.

Это уже обработано, поскольку БД заботится только о id, и поскольку новый элемент имеет id вместо 100, они совершенно разные.

Моя трудность возникает, когда я хочу запретить пользователю создавать еще один предмет "Thingy3". Если бы у меня было УНИКАЛЬНОЕ ограничение, которое рассматривало только элементы, не помеченные deleted, то я бы решил одну проблему.

(Конечно, тогда мне пришлось бы иметь дело с тем, что происходит, когда кто-то отменяет удаление ...)

Итак, как я могу определить такого рода ограничения?

Ответы [ 10 ]

12 голосов
/ 21 апреля 2009

Вы можете добавить значение id в конец имени, когда запись удаляется, поэтому, когда кто-то удаляет идентификатор 3, имя становится Thingy3_3, а затем, когда они удаляют идентификатор 100, имя становится Thingy3_100. Это позволит вам создать уникальный составной индекс для имени и удаленных полей, но вам придется фильтровать столбец имени при каждом его отображении и удалять идентификатор в конце имени.

Возможно, лучшим решением было бы заменить удаленный столбец на столбец delete_at типа DATETIME. Затем вы можете сохранить уникальный индекс для имени и удаленного в, с не удаленной записью, имеющей нулевое значение в поле delete_at. Это предотвратит создание нескольких имен в активном состоянии, но позволит удалить одно и то же имя несколько раз.

Очевидно, что вам нужно выполнить тест при восстановлении записи, чтобы убедиться, что нет строки с тем же именем и пустым полем delete_at, прежде чем разрешить удаление.

Вы могли бы фактически реализовать всю эту логику в базе данных, используя триггер INSTEAD-OF для удаления. Этот триггер не удаляет записи, а обновляет столбец delete_at при удалении записи.

Следующий пример кода демонстрирует это

CREATE TABLE swtest (  
    id          INT IDENTITY,  
    name        NVARCHAR(20),  
    deleted_at  DATETIME  
)  
GO  
CREATE TRIGGER tr_swtest_delete ON swtest  
INSTEAD OF DELETE  
AS  
BEGIN  
    UPDATE swtest SET deleted_at = getDate()  
    WHERE id IN (SELECT deleted.id FROM deleted)
    AND deleted_at IS NULL      -- Required to prevent duplicates when deleting already deleted records  
END  
GO  

CREATE UNIQUE INDEX ix_swtest1 ON swtest(name, deleted_at)  

INSERT INTO swtest (name) VALUES ('Thingy1')  
INSERT INTO swtest (name) VALUES ('Thingy2')  
DELETE FROM swtest WHERE id = SCOPE_IDENTITY()  
INSERT INTO swtest (name) VALUES ('Thingy2')  
DELETE FROM swtest WHERE id = SCOPE_IDENTITY()  
INSERT INTO swtest (name) VALUES ('Thingy2')  

SELECT * FROM swtest  
DROP TABLE swtest  

Выбор из этого запроса возвращает следующее

id      name       deleted_at
1       Thingy1    NULL
2       Thingy2    2009-04-21 08:55:38.180
3       Thingy2    2009-04-21 08:55:38.307
4       Thingy2    NULL

Таким образом, в вашем коде вы можете удалять записи, используя обычное удаление, и позволить триггеру позаботиться о деталях. Единственная возможная проблема (которую я мог видеть) заключалась в том, что удаление уже удаленных записей может привести к дублированию строк, поэтому в триггере возникает условие не обновлять поле delete_at в уже удаленной строке.

3 голосов
/ 21 апреля 2009

Возможно, стоит подумать об использовании таблицы «Корзина». Вместо того чтобы хранить старые записи в той же таблице с флагом, переместите их в свою собственную таблицу со своими собственными ограничениями. Например, в активной таблице у вас есть ограничение UNIQUE на имя, но в таблице корзины нет.

2 голосов
/ 21 апреля 2009

Проблема с составным уникальным ограничением заключается в том, что невозможно удалить несколько записей с одним и тем же именем. Это означает, что система сломается, как только вы удалите третью запись. Я бы не рекомендовал добавлять материал к именам, потому что теоретически может возникнуть дублирующая ситуация. Кроме того, тем самым вы в основном портите данные в базе данных и добавляете загадочную бизнес-логику к самим данным.

Единственное возможное решение для всей базы данных - это добавить триггер, который проверяет правильность введенных / обновленных данных. Также возможно поместить проверки вне базы данных в код.

1 голос
/ 21 апреля 2009

Требуемым типом ограничения является ограничение на уровне таблицы CHECK, то есть ограничение CHECK, состоящее из подзапроса, который проверяет NOT EXISTS (или эквивалентный) для таблицы, например,

CREATE TABLE Test 
(
   ID INTEGER NOT NULL UNIQUE, 
   name VARCHAR(30) NOT NULL, 
   deleted INTEGER NOT NULL, 
   CHECK (deleted IN (0, 1))
);

ALTER TABLE Test ADD
   CONSTRAINT test1__unique_non_deleted
      CHECK 
      (
         NOT EXISTS 
         (
            SELECT T1.name
              FROM Test AS T1
             WHERE T1.deleted = 0
             GROUP
                BY T1.Name
            HAVING COUNT(*) > 1
         )
      );

INSERT INTO Test (ID, name, deleted) VALUES (1, 'Thingy1', 0)
;
INSERT INTO Test (ID, name, deleted) VALUES (2, 'Thingy2', 0)
;
INSERT INTO Test (ID, name, deleted) VALUES (3, 'Thingy3', 1)
;
INSERT INTO Test (ID, name, deleted) VALUES (4, 'Thingy3', 1)
;
INSERT INTO Test (ID, name, deleted) VALUES (5, 'Thingy3', 0)
;
INSERT INTO Test (ID, name, deleted) VALUES (6, 'Thingy3', 0)
;

Последнее INSERT (ID = 6) вызовет ограничение, и INSERT завершится ошибкой. Что и требовалось доказать

... ооо, почти забыл упомянуть: SQL Server пока не поддерживает ограничения CHECK, которые содержат подзапрос (я проверял выше на ACE / JET, a.k.a.). Хотя вы могли бы использовать FUNCTION Я читал, что это небезопасно из-за ограничений тестирования SQL Server построчно (см. Блог Дэвида Портаса ). До тех пор, пока эта полная функция SQL-92 не будет поддерживаться в SQL Server, я бы предпочел использовать ту же логику в триггере.

1 голос
/ 21 апреля 2009

Вместо удаленного столбца используйте столбец end_date. Когда пользователь удаляет запись, добавьте текущую дату в столбец end_date. Любые записи, в которых столбец end_date равен NULL, являются вашими текущими записями. Определите уникальное ограничение для двух столбцов name и end_date. Из-за этого ограничения у вас никогда не будет сценария, в котором правильное имя записи будет продублировано. Каждый раз, когда пользователь хочет восстановить запись, вам нужно установить для столбца end_date значение null, и если это нарушает ограничение уникальности, то вы показываете пользователю сообщения, что такое имя уже существует.

1 голос
/ 21 апреля 2009

Добавьте случайный хеш после уникального имени. То, что легко обратимо. Возможно разделить подчеркиванием или другим символом.

Редактировать после комментария: Вы можете просто добавить подчеркивание и текущую метку времени.

1 голос
/ 21 апреля 2009

Для простой таблицы с именем [Test] с идентификаторами столбцов (int), Filter (nvarchar), Deleted (bit)

ALTER TABLE [dbo].[Test] ADD 
    CONSTRAINT [DF_Test_Deleted] DEFAULT (0) FOR [Deleted],
    CONSTRAINT [IX_Test] UNIQUE  NONCLUSTERED 
    (
        [filter],
        [Deleted]
    )  ON [PRIMARY] 
1 голос
/ 21 апреля 2009

Например, вы можете добавить недопустимый символ (*) к удаленному имени. Но у вас все еще есть проблемы с удалением удаленного элемента. Поэтому, вероятно, лучшая идея - запретить двойные имена, даже если они удалены.

Вы можете очистить удаленные записи через некоторое время (или переместить их в отдельную таблицу).

0 голосов
/ 21 апреля 2009

Создать уникальное ограничение (имя удалено). Это означает, что вы можете удалить только одно имя.

Очевидное решение для , что работает под ANSI-92, но не на MS SQLServer: https://connect.microsoft.com/SQLServer/feedback/ViewFeedback.aspx?FeedbackID=299229

0 голосов
/ 21 апреля 2009

Не уверен насчет SQLServer2005, но вы можете определить составные ограничения / индексы

CREATE UNIQUE INDEX [MyINDEX] ON [TABLENAME] ([NAME] , [DELETED])

Как указал SteveWeet, это позволит вам только дважды удалить / создать.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...