【问题标题】:How to prevent updates to a table, with an exception for one situation如何防止更新表,但有一种情况除外
【发布时间】:2012-02-29 21:36:21
【问题描述】:

我有一个表,其中包含可以成为账单一部分的记录。我可以分辨出哪些已经是账单的一部分,因为该表有一个 BillId 列,当发生这种情况时,应用程序代码会更新该列。我想阻止更新具有非空 BillId 的任何记录。我认为以下应该解决这个问题:

CREATE TRIGGER [Item_Update_AnyBilled]
ON [dbo].[Item]
FOR UPDATE
AS 
BEGIN
SET NOCOUNT ON;
DECLARE @AnyBilled BIT;

SELECT  TOP(1) @AnyBilled = 1
  FROM  inserted i
        JOIN deleted d ON i.ItemId = d.ItemId
 WHERE  d.BillId IS NOT NULL; 

IF COALESCE(@AnyBilled, 0) = 1 BEGIN
    RAISERROR(2870486, 16, 1);  -- Cannot update a record that is part of a bill.
    ROLLBACK TRANSACTION;
END;
END;

但是,还有一个问题。 Item 表还有一个 DATETIME Modified 列,以及一个更新它的触发器。

CREATE TRIGGER [dbo].Item_Update_Modified 
ON  [dbo].[Item] 
AFTER UPDATE
AS 
BEGIN
SET NOCOUNT ON;

UPDATE a
   SET Modified = getdate()
  FROM Item a JOIN inserted i ON i.ItemId = a.ItemId
END

使用这些触发器,将项目添加到账单总是会导致 RAISERROR 触发。大概是因为当 BillId 被填充时,Item_Update_AnyBilled 允许它通过,因为 deleted.BillId 为 NULL,但 Item_Update_Modified 然后被执行,并且该次要更改导致 Item_Update_AnyBilled 再次被执行,而这一次 deleted.BillId 不再为 NULL。

我如何防止更新 Item 表,除非是在 BillId 被填充或唯一更改是 Modified 列的情况下?

我更喜欢不需要我比较每列的插入和删除值(或使用 COLUMNS_UPDATED())的解决方案,因为这会产生维护问题(有人必须记住更新触发器向表中添加或删除新列的时间)。我使用的是 SQL Server 2005。

【问题讨论】:

    标签: sql-server sql-server-2005 triggers


    【解决方案1】:

    为什么不使用INSTEAD OF 触发器?它需要更多的工作(即重复的UPDATE 声明),但任何时候你都可以阻止工作,而不是让它发生然后回滚,你会变得更好。

    CREATE TRIGGER [dbo].[Item_BeforeUpdate_AnyBilled]
    ON [dbo].[Item]
    INSTEAD OF UPDATE
    AS 
    BEGIN
      SET NOCOUNT ON;
    
      IF EXISTS 
      (
         SELECT 1 FROM inserted i
           JOIN deleted AS d ON i.ItemId = d.ItemId
           WHERE d.BillId IS NULL -- it was NULL before, may not be NULL now
      )
      BEGIN
         UPDATE src 
           SET col1 = i.col1 --, ... other columns
              ModifiedDate = CURRENT_TIMESTAMP -- this eliminates need for other trigger
           FROM dbo.Item AS src
           INNER JOIN inserted AS i
           ON i.ItemId = src.ItemId
           AND (criteria to determine if at least one column has changed);
      END
      ELSE
      BEGIN
         RAISERROR(...);
      END
    END
    GO
    

    这并不完美。我遗漏的标准是有原因的:确定列值是否已更改可能很复杂,因为它取决于数据类型、列是否可以为 NULL 等。AFAIK 内置触发器函数只能判断是否指定了某个列,而不能判断该值是否实际上比以前发生了变化。

    EDIT 考虑到您只关心由于 after 触发器而更新的其他列,我认为以下 INSTEAD OF 触发器可以替换您现有的两个触发器并处理一次更新多行(有些不符合您的条件):

    CREATE TRIGGER [dbo].[Item_BeforeUpdate_AnyBilled]
    ON [dbo].[Item]
    INSTEAD OF UPDATE
    AS 
    BEGIN
      SET NOCOUNT ON;
    
      UPDATE src SET col1 = i.col1 --, ... other columns,
         ModifiedDate = CURRENT_TIMESTAMP
         FROM dbo.Item AS src
         INNER JOIN inserted AS i
         ON src.ItemID = i.ItemID
         INNER JOIN deleted AS d
         ON i.ItemID = d.ItemID 
         WHERE d.BillID IS NULL; 
    
      IF @@ROWCOUNT = 0
      BEGIN
        RAISERROR(...);
      END
    END
    GO
    

    【讨论】:

    • 这样的事情(对于表中的每一列)是否足以满足“确定至少一列是否已更改的标准”? OR (src.[ColumnName] IS NULL AND i.[ColumnName] IS NOT NULL) OR (src.[ColumnName] IS NOT NULL AND i.[ColumnName] IS NULL) OR src.[ColumnName] != i.[ColumnName ]
    • 对于 NULLable 列,是的,您需要进行“null 变为非 null 或非 null 变为 null”检查,但如果值更改,也需要进行检查。在您确定至少应该更新一行之后,CHECKSUM 可能会更简单。这将是 99% 的可靠,但可以为不同的行生成相同的值。让我考虑一下并更新我的答案。
    • "其中 d.ItemID 为空;"应该是“哪里 d.BillId 为空;”。无论如何,当我执行 CREATE TRIGGER 时,我得到这个:无法在表 'dbo.Item' 上创建 INSTEAD OF DELETE 或 INSTEAD OF UPDATE TRIGGER 'Item_BeforeUpdate_AnyBilled'。这是因为该表有一个 FOREIGN KEY 和级联 DELETE 或 UPDATE。
    • 抱歉,已转录。您是否有机会使用触发器而不是显式级联选项来管理级联删除?不幸的是,我认为这在后触发器中不会起作用。
    • 我可以用另一个触发器替换级联。我将来可能会重新审视这个,因为我更喜欢你的解决方案,因为它消除了比较插入和删除值的需要。但是,我的 AFTER UPDATE 触发器正在工作,我需要继续扑灭其他火灾。我在代码中放置了一个 TODO 项,并引用了此页面。谢谢!
    猜你喜欢
    • 2023-04-02
    • 2021-12-30
    • 2020-03-18
    • 2018-10-16
    • 2023-04-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-08-07
    相关资源
    最近更新 更多