写保护表中的某些行

时间:2018-02-26 08:37:01

标签: sql-server tsql

我有一种情况,我需要根据某些条件(实际上是外表中的标志)写保护表的某些行。考虑这个架构:

CREATE TABLE Batch (
  Id INT NOT NULL PRIMARY KEY,
  DateCreated DATETIME NOT NULL,
  Locked BIT NOT NULL
)

CREATE UNIQUE INDEX U_Batch_Locked ON Batch (Locked) WHERE Locked=0

CREATE TABLE ProtectedTable (
  Id INT NOT NULL IDENTITY PRIMARY KEY,
  Quantity DECIMAL(10,3) NOT NULL,
  Price Money NOT NULL,
  BatchId INT NULL)

ALTER TABLE ProtectedTable ADD CONSTRAINT FK_ProtectedTable_Batch FOREIGN KEY (BatchId) REFERENCES Batch(id)

如果行链接到具有Locked = 1的批次,我想阻止更改数量和价格。我还需要阻止删除该行。

注意:U_Batch_Locked确保最多可以随时解锁一个批次。

我尝试过使用触发器(yikes)但由于触发器回滚事务而导致更多问题。此更新通常发生在C#客户端中,该客户端在单个事务中执行多个更新(在多个表上)。无论错误如何,客户端都会继续进行更新,并且如果发生任何错误,则会在事务结束时将其回滚。这样,它可以收集所有/大多数约束违规,并让用户在尝试再次保存更改之前修复它们。但是,由于触发器在不满足约束时回滚事务,后续更新将启动它们自己的自动事务并实际上已提交。客户端在批处理结束时发出的最终回滚失败。

我也发现了这篇博文:Using ROWVERSION to enforce business rules,虽然它似乎是我需要的,但它要求外键具有相反的方向(即受保护的表是父子关系中的父项)在我的情况下,受保护的表是孩子)

以前有人做过这样的事吗?这似乎不是一个不常见的业务要求,但我还没有看到适当的解决方案。我知道我可以在客户端实现这一点,但这留下了出错的空间:如果有人使用直接SQL(错误地)更改了这些内容,如果升级/迁移脚本或客户端本身有错误并且无法执行constaint?

2 个答案:

答案 0 :(得分:1)

经过大量搜索和反复试验,我最终使用INSTEAD OF触发器来检查约束,并在必要时引发错误并跳过操作。参考原始问题中的模式,以下是必需的触发器:

更新触发器:

CREATE TRIGGER ProtectedTable_LockU ON ProtectedTable
INSTEAD OF UPDATE
AS
  SET NOCOUNT ON;
  IF UPDATE(Quantity) OR UPDATE(Price) 
     AND EXISTS(
       SELECT * 
       FROM inserted i INNER JOIN deleted d ON d.Id = i.Id
                       LEFT JOIN Batch b ON b.Id = d.BatchId
       WHERE b.Locked <> 0 AND
             (i.Quantity <> d. Quantity OR i.Price <> d.Price))
  BEGIN
    RAISERRROR('[CK_ProtectedTable_DataLocked]: Attempted to update locked data', 16, 1)
    RETURN
  END

  UPDATE pt
  SET Quantity = i.Quantity,
      Price = i.Price,
      BatchId = i.BatchId
  FROM ProtectedTable pt
       INNER JOIN deleted d ON d.Id = pt.ID
       INNER JOIN inserted i ON i.Id = d.Id

删除触发器:

CREATE TRIGGER ProtectedTable_LockD ON ProtectedTable
INSTEAD OF DELETE
AS
  SET NOCOUNT ON;
  IF EXISTS(
       SELECT * 
       FROM deleted d
            LEFT JOIN Batch b ON b.Id = d.BatchId
       WHERE b.Locked <> 0)
  BEGIN
    RAISERRROR('[CK_ProtectedTable_DataLocked]: Attempted to delete locked data', 16, 1)
    RETURN
  END

  DELETE pt
  FROM ProtectedTable pt
       INNER JOIN deleted d ON d.Id = pt.ID

当然可以创建一个视图并将触发器放在视图上。这样,您还可以避免INSTEAD OF触发器与FOREIGN KEY ON DELETE / ON UPDATE规则之间的冲突。

它很丑陋,我根本不喜欢它,但它是实际工作的唯一解决方案,其行为或多或少类似于常规数据库约束(在修改之前检查数据库约束数据)。我还没有对此进行过广泛的测试,我不确定它是否存在任何问题。我也担心竞争条件(例如,如果在执行触发器期间批量修改了什么?我是否可以包含锁定提示以确保完整性?)

PS:无论应用程序设计不好,这仍然是一个合理的要求。在某些情况下,它也是合法的(你必须证明在某些条件得到满足后,某些记录无法改变)。这就是为什么问题偶尔出现在SO中,并且没有明确的解决方案。我发现这个解决方案比使用AFTER触发器更好,因为它的行为更像是普通的数据库约束。

希望这可以帮助处于类似情况的其他人。

答案 1 :(得分:0)

不是直接授予用户对表的访问权限,而是使用VIEW选项创建一个简单的CHECK OPTION,并且只授予用户通过该视图应用修改的权限。类似的东西:

CREATE VIEW BatchData
WITH SCHEMABINDING
WITH CHECK OPTION
AS
    SELECT pt.Id, pt.Quantity, pt.Price, pt.BatchId
    FROM
        dbo.ProtectedTable pt
           INNER JOIN
        dbo.Batch b
           ON
              pt.BatchId = b.Id
     WHERE
        b.Locked = 0

只要所有插入,更新和删除都通过此视图进行引导(如上所述,您通过权限约束),那么它们只能应用于打开的批处理