【问题标题】:What to do when I want to use database constraints but only mark as deleted instead of deleting?当我想使用数据库约束但只标记为已删除而不是删除时该怎么办?
【发布时间】:2009-04-21 05:55:49
【问题描述】:

我正在一个项目中工作,其中数据库项目没有被删除,而只是被标记为已删除。像这样的:

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

我希望能够在 name 列上定义类似 UNIQUE 约束的内容。看起来很简单,对吧?

让我们想象一个场景,其中“Thingy3”被删除,并创建了一个新的(可能几年后)。我们得到:

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

从用户的角度来看,他删除了一个项目并创建了一个新项目。就像删除文件并创建新文件一样。所以对他来说很明显,新项目与旧项目相关的任何数据无关且不附加。

这已经处理好了,因为 DB 只关心 id,而且由于新项目的 id 是 100 而不是 3,所以它们完全不同。

当我想阻止用户创建另一个“Thingy3”项目时,我遇到了困难。如果我有一个 UNIQUE 约束,它只查看未标记为 deleted 的项目,那么我会解决一个问题。

(当然,我必须处理当有人撤消删除时会发生什么......)

那么,我该如何定义这种约束呢?

【问题讨论】:

标签: sql sql-server-2005 tsql constraints


【解决方案1】:

您可以在删除记录时将 id 值添加到名称的末尾,因此当有人删除 id 3 时,名称变为 Thingy3_3,然后当他们删除 id 100 时,名称变为 Thingy3_100。这将允许您在名称和已删除字段上创建唯一的复合索引,但是您必须在显示名称列时过滤它并从名称末尾删除 id。

也许更好的解决方案是将已删除的列替换为 DATETIME 类型的 deleted_at 列。然后,您可以维护 name 和 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  

从此查询中选择返回以下内容

id 名称 deleted_at 1 事物1 NULL 2东西2 2009-04-21 08:55:38.180 3东西2 2009-04-21 08:55:38.307 4 事物2 NULL

因此,在您的代码中,您可以使用普通删除来删除记录,并让触发器处理细节。唯一可能的问题(我可以看到)是删除已删除的记录可能会导致重复的行,因此触发器中的条件是不更新已删除行上的 deleted_at 字段。

【讨论】:

  • 投了赞成票,因为这正是我为自己的应用程序考虑的。试图考虑任何缺点,但到目前为止它似乎足够可靠。
  • 我喜欢“deleted_at”的想法!
【解决方案2】:

可能值得考虑使用“回收站”表。与其将旧记录与标志一起保存在同一个表中,不如将它们移动到具有自己约束的自己的表中。例如,在活动表中您确实对名称有 UNIQUE 约束,但在回收站表中您没有。

【讨论】:

  • 这可能会在使用ORM时导致冲突(不知道这里是否相关)。
【解决方案3】:

复合唯一约束的问题是不可能删除多个同名的记录。这意味着一旦您删除第三条记录,系统就会中断。我不建议在名称上附加内容,因为理论上可能会出现重复的情况。此外,这样做基本上是在破坏数据库中的数据,并向数据本身添加神秘的业务逻辑。

数据库范围内唯一可能的解决方案是添加一个触发器来检查插入/更新的数据是否有效。也可以将检查放在数据库之外的代码中。

【讨论】:

    【解决方案4】:

    例如,您可以在已删除的名称中添加非法字符 (*)。但是您仍然无法取消删除已删除的项目。所以可能更好的主意是禁止双重名称,即使它们被删除。

    您可以在一段时间后清理已删除的记录(或将它们移动到单独的表中)。

    【讨论】:

    • 修改用户输入的数据总是有问题的,因为您不一定能预测数据是否会以非法字符结尾。不可否认,该字符只会添加到“已删除”记录中,因此引入了某种可预测性元素。不管怎样,我更喜欢 Steve Weet 的解决方案,因为它支持取消删除。
    【解决方案5】:

    对于一个名为 [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] 
    

    【讨论】:

    • 当一条记录被“删除”时,此方法会出现问题,添加具有相同“过滤器”值的记录并尝试“删除”该记录 - 因为随后违反了约束。
    【解决方案6】:

    在唯一名称后添加随机哈希。很容易逆转的东西。可能用下划线或其他字符分隔。

    评论后编辑:您可以简单地添加下划线和当前时间戳。

    【讨论】:

    • 太棒了!哎呀,我什至可以将名称更改为“Thingy3(于 2000 年 1 月 1 日 #1 删除)”,以向用户提供更多信息!
    【解决方案7】:

    使用 end_date 列代替已删除的列。当用户删除记录时,在 end_date 列中添加当前日期。 end_date 列为 NULL 的任何记录都是您当前的记录。在两个列 name 和 end_date 上定义唯一约束。由于此限制,您永远不会遇到有效记录名称重复的情况。任何时候用户想要取消删除记录,您都需要将 end_date 列设置为 null,如果这违反了唯一约束,那么您会向用户显示一条消息,告诉用户已经存在相同的名称。

    【讨论】:

    【解决方案8】:

    您需要的约束类型是表级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 将失败。 Q.E.D.

    ...哦,差点忘了提:SQL Server 还不支持包含子查询的CHECK 约束(我在 ACE/JET 上测试了上述内容,a.k.a.)。虽然您可以使用FUNCTION,但我读到这是不安全的,因为SQL Server 逐行测试限制(请参阅David Portas' Blog)。在 SQL Server 支持这一完整的 SQL-92 功能之前,我首选的解决方法是在触发器中使用相同的逻辑。

    【讨论】:

      【解决方案9】:

      在(名称,已删除)上创建唯一约束。但是,这意味着每个名称只能删除一个。

      的明显解决方法在 ANSI-92 下有效,但在 MS SQLServer 上无效:https://connect.microsoft.com/SQLServer/feedback/ViewFeedback.aspx?FeedbackID=299229

      【讨论】:

      • “已删除”列应该是碰撞可能性较小的列(没有布尔值!),因为历史记录中可能有多个同名项目。
      【解决方案10】:

      不确定 SQLServer2005,但您可以定义复合约束/索引

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

      正如 SteveWeet 所指出的,这只允许您删除/创建两次。

      【讨论】:

      • 这将只允许删除一个'Thingy3'你不能创建它,删除它,再次创建它,然后再次删除它。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-07-18
      • 1970-01-01
      • 2016-03-08
      • 2011-04-11
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多