2009-04-21 41 views
5

我正在項目中的數據庫項目不被刪除,但只標記爲已刪除。事情是這樣的:當我想使用數據庫約束,但只標記爲刪除而不是刪除時該怎麼辦?

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 

但從用戶的角度,他刪除了一個項目,並創建一個新的。就像刪除一個文件並創建一個新文件一樣。所以對他來說很明顯,新項目與任何與舊項目相關的數據都是不相關的,並且沒有連接到任何數據。

這已經處理,因爲DB只關心id,並且由於新項目的id爲100而不是3,所以它們完全不同。

當我想阻止用戶創建另一個「Thingy3」項目時,出現了我的困難。如果我有一個UNIQUE約束只查看未標記爲deleted的項目,那麼我將解決一個問題。

(當然,我不得不處理時,有人做刪除的撤銷會發生什麼......)

所以,我怎麼可以定義排序約束的?

+2

順便說一句,這被稱爲軟刪除http://databaserefactoring.com/IntroduceSoftDelete.html – cherouvim 2009-04-21 06:08:56

+1

很高興知道這有一個名字。謝謝! – scraimer 2009-04-21 06:28:05

回答

11

當記錄被刪除時,您可以將id值添加到名稱末尾,因此當某人刪除id 3時,名稱變爲Thingy3_3,然後當他們刪除id 100時,名稱變爲Thingy3_100。這將允許您在名稱和已刪除的字段上創建一個唯一的組合索引,但是您必須在顯示名稱列時過濾名稱列,並從名稱末尾刪除該ID。

也許更好的解決方案是用DATETIME類型的deleted_at列替換已刪除的列。然後,您可以在名稱上保留一個唯一索引,並在刪除時刪除在deleted_at字段中具有空值的未刪除記錄。這會阻止在活動狀態下創建多個名稱,但可以讓您多次刪除相同的名稱。

您明顯需要在取消刪除記錄時進行測試,以確保在允許刪除之前沒有同名的行和空的deleted_at字段。

實際上,通過使用INSTEAD-OF觸發器來刪除數據庫,您實際上可以在數據庫中實現所有這些邏輯。該觸發器不會刪除記錄,而是在刪除記錄時更新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字段。

+0

投票贊成,因爲這正是我正在考慮爲我自己的應用程序。試圖考慮任何缺點,但它迄今似乎足夠穩固。 – belugabob 2009-04-21 07:29:34

0

不知道有關SQLServer2005的,但你可以定義複合constrainst /索引

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

正如SteveWeet指出的,這樣只會讓你刪除/創建兩次。

+0

這將只允許刪除一個'Thingy3',但您不能創建它,刪除它,再次創建它,然後再次刪除它。 – 2009-04-21 06:51:24

+0

好點,這不會允許N次更新,修復它 – 2009-04-21 07:22:09

1

例如,您可以將非法字符(*)添加到已刪除的名稱。但是,您仍然無法刪除已刪除的項目。所以最好的辦法是禁止雙重名稱,即使它們被刪除。

您可以在一段時間後清理已刪除的記錄(或將它們移動到單獨的表格中)。

+0

修改用戶輸入的數據總是會有問題,因爲您無法預測數據是否已經以非法字符結尾。無可否認,角色只會被添加到「刪除」的記錄中,因此引入了一定的可預測性。無論如何,我更喜歡Steve Weet的解決方案,因爲它支持反刪除。 – belugabob 2009-04-21 07:33:13

1

對於被叫[測試]的列ID(INT),過濾器(nvarchar的),已刪除簡單的表(位)

ALTER TABLE [dbo].[Test] ADD 
    CONSTRAINT [DF_Test_Deleted] DEFAULT (0) FOR [Deleted], 
    CONSTRAINT [IX_Test] UNIQUE NONCLUSTERED 
    (
     [filter], 
     [Deleted] 
    ) ON [PRIMARY] 
+0

當一個記錄被'刪除'一個具有相同「過濾器」值的記錄被添加並且嘗試'刪除'這條記錄時,這種方法會變得有問題 - 因爲違反了約束條件。 – belugabob 2009-04-21 07:36:09

1

在唯一名稱後添加一個隨機散列。一些容易扭轉的東西。可能與下劃線或其他字符分開。

在評論之後編輯:您可以簡單地添加下劃線和當前時間戳。

+0

太棒了!哎呀,我甚至可以將名稱更改爲「Thingy3(在1/1/2000#1上刪除)」,以便爲用戶提供更多信息! – scraimer 2009-04-21 06:32:58

2

複合唯一約束的問題是不能刪除多個具有相同名稱的記錄。這意味着一旦刪除了第三條記錄,系統就會中斷。我不建議在名稱上添加內容,因爲理論上可能會出現重複的情況。而且,通過這樣做,您基本上正在破壞數據庫中的數據,併爲數據本身添加了神祕的業務邏輯。

數據庫範圍內唯一可能的解決方案是添加一個觸發器,檢查插入/更新的數據是否有效。也可以將檢查數據庫外的檢查代碼放入代碼中。

3

這可能值得考慮使用「回收站」表。與其將舊記錄保存在具有標誌的同一個表中,不如使用自己的約束將它們移動到其自己的表中。例如,在活動表中,您確實對名稱有一個UNIQUE約束,但在回收站表中則沒有。

1

而不是刪除列使用end_date列。當用戶刪除一條記錄時,在end_date列中添加當前日期。 end_date列爲NULL的任何記錄都是您當前的記錄。在兩列name和end_date上定義一個唯一約束。由於這個限制,你永遠不會有有效的記錄名稱被重複的場景。任何時候用戶都想要取消刪除記錄,您需要將end_date列設置爲null,並且如果這違反了唯一性約束,那麼您向用戶顯示消息用戶已存在相同的名稱。

1

您需要的約束類型是一個表級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測試的限制逐行的基礎(見David Portas' Blog)。在SQL Server中支持完整的SQL-92功能之前,我首選的解決方法是在觸發器中使用相同的邏輯。

相關問題