【问题标题】:How to add constraint for deleting in sql如何在sql中添加删除约束
【发布时间】:2017-08-11 12:36:37
【问题描述】:

我有一个用于书籍、书籍类别和类别的表格

看起来像这样

Book
    Id uniqueidentifier,
    Title varchar(255),
    Author varchar(255),
    Deleted datetime

BookCategory
    BookId
    CategoryId

Category
    Id uniqueidentifier
    Name varchar(255)
    Deleted datetime

我想编写一个约束,如果该书的类别存在,则可以防止删除该书

意思是,如果一本书(比如说哈利波特)有一个虚构的类别,例如,表 BookCategory 将包含两个 ID。如果用户想要删除具有特定类别的图书,他将无法执行此操作。

有人可以帮我吗?

PS当我删除项目时,我实际上并没有删除它们,而是将属性设置为日期时间。

【问题讨论】:

  • "当我删除项目时,我并没有真正删除它们,而是将属性设置为 datetime。"然后你必须使用UPDATE 触发器;约束对你没有帮助。
  • 更新触发器是什么意思? @JeroenMostert
  • 我会加我的 5 美分。 @JeroenMostert 的意思是您没有实际删除记录,因此外键(人们建议您使用)不会以这种方式提供帮助,而实现这一目标的唯一方法是使用不带约束的触发器。在那里你需要实现检查是否有任何记录使用它的逻辑。
  • 我确实知道它不起作用。我的意思是他是否可以为我的案例展示任何示例@DmitrijKultasev
  • 如果您确实选择使用触发器路线,如果您希望强制执行(大多数人通常这样做),您还必须为 BookCategory 表编写额外的触发器也不允许在该表中创建引用已删除行的新行。

标签: sql sql-server constraints


【解决方案1】:

这回答了编辑前的原始问题。

您正在寻找外键约束:

alter table BookCategory add constraint fk_BookCategory_Book
    foreign key (BookId) references Book(Id);

默认情况下,此类约束不允许您删除具有类别的图书。您可以了解cascading 选项以提供更精确的行为。不提供级联操作时默认为ON DELETE NO ACTION,即不会删除Book 中的行。

【讨论】:

  • “默认是删除无操作”是什么意思。上述约束的默认值?
  • 关于你可能想看的问题有一个小的更新。
  • 他写道:“当我删除项目时,我实际上并没有删除它们,而是将已删除的属性设置为日期时间”,因此在那里不起作用。 Jeroen 提出了正确的建议
【解决方案2】:

既然你想执行软删除,你可以通过添加一些额外的帮助列和外键来完成你想要的:

create table Books (
    Id uniqueidentifier primary key,
    Title varchar(255) not null,
    Author varchar(255) not null,
    Deleted datetime null,
    _DelXRef as CASE WHEN Deleted is null then 0 else 1 END persisted,
    constraint UQ_Books_DelXRef UNIQUE (Id,_DelXRef)
)
create table Categories (
    Id uniqueidentifier primary key,
    Name varchar(255) not null,
    Deleted datetime null,
    _DelXRef as CASE WHEN Deleted is null then 0 else 1 END persisted,
    constraint UQ_Categories_DelXRef UNIQUE (Id,_DelXRef)
)
create table BookCategories (
    BookId uniqueidentifier not null,
    CategoryId uniqueidentifier not null,
    _DelXRef as 0 persisted,
    constraint FK_BookCategories_Books foreign key (BookID) references Books(Id),
    constraint FK_BookCategories_Books_DelXRef foreign key (BookID,_DelXRef) references Books(Id,_DelXRef),
    constraint FK_BookCategories_Categories foreign key (CategoryId) references Categories(Id),
    constraint FK_BookCategories_Categories_DelXRef foreign key (CategoryId,_DelXRef) references Categories(Id,_DelXRef)
)

希望您可以看到外键如何确保引用表中的 _DelXRef 列必须始终保持 0,因此无法将 Deleted 设置为任何非 NULL 值,同时该行是从BookCategories 表中引用的。

(此时,“原始”外键 FK_BookCategories_BooksFK_BookCategories_Categories 似乎是多余的。我更喜欢将它们保留在模型中以记录真正的 FK 关系。我也使用自己的约定以_ 为对象添加前缀,其中不打算将它们用于数据库的用户 - 它们的存在只是为了允许强制执行 DRI)

【讨论】:

  • 感谢无论如何使用约束使其工作。对于触发器的一般坏处是否真的超过幻列、额外索引和该解决方案的一般不明显性,我有些犹豫,但它是一个解决方案。
  • @JeroenMostert - 如果触发器碰巧被暂时禁用,您最终可能会在表中得到错误的数据。我更喜欢使用约束/索引/计算列的解决方案,因为如果它们当前被检查,你知道他们试图执行的实际上在数据中执行的。
  • 不要忘记约束同样容易禁用,不幸的是忘记使用 WITH CHECK 重新启用约束很容易做到(因为它是默认设置),所以你仍然可以最终得到不一致的数据。简单的约束显然优于触发器;这种情况更取决于偏好。
  • @JeroenMostert - 但是在重新启用触发器时没有 WITH CHECK 类型的选项。您永远无法知道您是否可以相信他们试图强制执行的约束当前实际上是真实的,无论它们当前的状态是什么。这就是我想要指出的。
  • 啊,我明白了。好吧,您可以知道,但是您必须编写显式查询才能找到(或者,至少,做一些不直观的事情,例如SET A = A 来强制触发器查看现有行),这显然不如让引擎为你做检查那么可靠。
【解决方案3】:

如果我理解,您要做的是限制对表的更新以将记录标记为“已删除”。对表的约束不能做到这一点,但您可以通过更新触发器来完成这一点。此外,您应该尝试在您的应用程序中处理此逻辑。阅读与您类似的问题:Can an SQL constraint be used to prevent a particular value being changed when a condition holds?

【讨论】:

    【解决方案4】:
    CREATE TABLE Book
    (
        Id      UNIQUEIDENTIFIER
      , Title   VARCHAR(255)
      , Author  VARCHAR(255)
      , Deleted DATETIME
    );
    
    CREATE TABLE BookCategory
    (
        BookId     UNIQUEIDENTIFIER
      , CategoryId UNIQUEIDENTIFIER
    );
    
    CREATE TABLE Category
    (
        Id      UNIQUEIDENTIFIER
      , Name    VARCHAR(255)
      , Deleted DATETIME
    );
    
    INSERT INTO dbo.Book (   Id
                           , Title
                           , Author
                           , Deleted
                         )
    VALUES (   'BCF8DE45-D26F-43EE-82CD-9975E35E51B1' -- Id - uniqueidentifier
             , 'Harry Potter'                         -- Title - varchar(255)
             , 'RK'                                   -- Author - varchar(255)
             , NULL                                   -- Deleted - datetime
           );
    
    INSERT INTO dbo.Category (   Id
                               , Name
                               , Deleted
                             )
    VALUES (   'EB6E823D-DFE8-448D-B648-EAE1EFE06358' -- Id - uniqueidentifier
             , 'Fantasy'                              -- Name - varchar(255)
             , NULL                                   -- Deleted - datetime
           );
    
    INSERT INTO dbo.Category (   Id
                               , Name
                               , Deleted
                             )
    VALUES (   '9B53C866-0DAA-4637-8169-5B8885A2E644' -- Id - uniqueidentifier
             , 'Category without books'               -- Name - varchar(255)
             , NULL                                   -- Deleted - datetime
           );
    
    INSERT INTO dbo.BookCategory (   BookId
                                   , CategoryId
                                 )
    VALUES (   'BCF8DE45-D26F-43EE-82CD-9975E35E51B1' -- BookId - int
             , 'EB6E823D-DFE8-448D-B648-EAE1EFE06358' -- CategoryId - int
           );
    GO
    
    CREATE TRIGGER trg_Category_Check_Category_Usage
    ON Category
    INSTEAD OF UPDATE
    AS
        BEGIN
            IF UPDATE(Deleted)
                BEGIN
                    IF EXISTS (   SELECT *
                                    FROM INSERTED         i
                                    JOIN dbo.BookCategory AS b ON b.CategoryId = i.Id
                                   WHERE i.Deleted IS NOT NULL
                              )
                        THROW 60000, 'record exists', 1;
                END;
    
            UPDATE b
               SET b.deleted = i.deleted
                 , b.Name = i.Name
              FROM Category b
              JOIN INSERTED i ON i.Id = b.Id;
        END;
    GO
    UPDATE dbo.Category SET Deleted = GETDATE()WHERE Name = 'Fantasy'; --error
    
    UPDATE dbo.Category
       SET Deleted = GETDATE()
     WHERE Name = 'Category without books'; --no error
    

    【讨论】:

    • INSTEAD OF 触发器比AFTER 触发器更难维护,因为每次更改表结构时都必须重写它们。 AFTER 在这里也可以做到——触发器中引发的错误将回滚原始更新。
    【解决方案5】:

    为表类别中的id创建一个主键,并通过外键将其与表bookcategory(categoryid)相关联。这将确保您不会在不引用父表类别的情况下添加id,同时您不能删除记录也来自类别表。

    【讨论】:

      猜你喜欢
      • 2012-05-08
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-09-25
      • 1970-01-01
      相关资源
      最近更新 更多