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

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

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

Я хотел бы иметь возможность определить что-то вроде уникального ограничения на

10 ответов


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

возможно, лучшим решением было бы заменить удаленный столбец на столбец deleted_at типа DATETIME. Затем вы можете поддерживать уникальный индекс на name и deleted at, при этом не удаленная запись имеет нулевое значение в поле deleted_at. Это предотвратит создание нескольких имен в активном состоянии, но позволит вам удалить одно и то же имя несколько раз.

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

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

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

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

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


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


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

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


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

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


для простой таблицы под названием [Test] со столбцами ID (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] 

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

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


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


тип ограничения, который вам требуется, - это уровень таблицы 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.к. a. ). В то время как вы мог бы использовать FUNCTION Я прочитал, что это небезопасно из-за ограничений тестирования SQL Server по строкам (см. Блог Дэвида Портаса). Пока эта полная функция SQL-92 не будет поддерживаться в SQL Server, мой предпочтительный обходной путь-использовать ту же логику в триггере.


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

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


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

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

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