【问题标题】:Trigger to block some updates and allow inserts触发以阻止某些更新并允许插入
【发布时间】:2015-07-04 10:34:49
【问题描述】:

我有一个跟踪客户名称更改的 SQL Server 表:

CREATE TABLE CustomerHistory 
(
 Id INT IDENTITY(1,1) PRIMARY KEY,
 CustomerId INT NOT NULL,
 Name VARCHAR(255) NOT NULL,
 ValidFrom DATETIME NOT NULL,
 ValidTo DATETIME NOT NULL,
 CreatedOn DATETIME NOT NULL,
 ModifiedOn DATETIME NOT NULL
)

INSERT INTO CustomerHistory (CustomerId, Name, ValidFrom, ValidTo, CreatedOn, ModifiedOn ) 
VALUES (1, 'ABC', '1900-01-01','2999-12-31', '2015-07-03 11:29:23.000', '2015-07-03 11:29:23.000')

应用程序允许用户以两种方式进行更改,即通过更改当前记录(NameModifiedOn 已更新)或插入新记录

INSERT INTO CustomerHistory (CustomerId, Name, ValidFrom, ValidTo, CreatedOn, ModifiedOn) 
VALUES (1,'AAB', '2015-07-04','2999-12-31', '2015-07-04 12:29:23.000', '2015-07-04 12:29:23.000')

并更新前一个(Name, ValidTo, ModifiedOn 已更新)。由于以第一种方式完成了一些不需要的更改(仅更新当前记录),我需要阻止它,因此所做的每个更改都以第二种方式完成(插入新记录并更新前一个记录)。我需要使用触发器来执行此操作,以便用户获得特殊的错误信息。

知道如何应对吗?

【问题讨论】:

  • 只是为了了解您的想法。如果您有一个现有客户,并且您为此客户获得了一个新行。您想更新旧版本并将 validTo 设置为当前插入日期并更改 modified_on 。对?你也想阻止一些更新吗?
  • 无论如何,我为此提供了两个触发器。我建议(如果你真的需要触发器)使用两个不同的触发器。它更容易维护。希望您能找到答案。
  • 谢谢你,离子。问题中提到的所有更新都是由界面应用程序进行的 - 我无法更改任何记录中的任何数据。触发器唯一需要做的就是显示错误信息和回滚事务。
  • 啊,好吧,我知道了。抛出错误的条件是什么?如果用户尝试更新一行而不设置ModifiedOn?
  • 条件是更改当前记录中的任何数据而不插入新记录。换句话说 - 客户名称只能通过插入新记录来更改。重要的是,当插入新记录时,应用程序也会更新当前记录中的两个字段:ValidTo 和 ModifiedOn,因此您不能只阻止更新并允许插入。

标签: sql sql-server triggers


【解决方案1】:

您可以使用此触发器,例如:

CREATE TRIGGER dbo.TR_InvalidateOldRows
   ON  CustomerHistory 
   AFTER INSERT
AS 
BEGIN
    SET NOCOUNT ON;

    -- Debug
    --SELECT *
    --FROM inserted

    UPDATE ch
    SET ValidTo = GETDATE(), ModifiedOn = GETDATE()
    FROM (
        SELECT TOP 1 Id, CustomerId, Name, ValidFrom, ValidTo, CreatedOn, ModifiedOn
        FROM (
            SELECT Id, CustomerId, Name, ValidFrom, ValidTo, CreatedOn, ModifiedOn
            FROM CustomerHistory AS ch
            WHERE ch.CustomerId = (SELECT CustomerId FROM inserted)
            EXCEPT 
            SELECT Id, CustomerId, Name, ValidFrom, ValidTo, CreatedOn, ModifiedOn
            FROM inserted
        ) as allRows
        ORDER BY ID DESC
    ) as oldRow
    INNER JOIN CustomerHistory as ch
            ON oldRow.id = ch.Id
END
GO

使用以下方式填充演示数据:

INSERT INTO CustomerHistory (CustomerId, Name, ValidFrom, ValidTo, CreatedOn, ModifiedOn) 
VALUES (1,'AAB', GETDATE(),DATEADD(year,1,GETDATE()), GETDATE(), GETDATE())

表格中填满了:

Id          CustomerId  Name       ValidFrom               ValidTo                 CreatedOn               ModifiedOn
----------- ----------- ---------- ----------------------- ----------------------- ----------------------- -----------------------
1           1           AAB        2015-07-04 13:21:34.500 2016-07-04 13:21:34.500 2015-07-04 13:21:34.500 2015-07-04 13:21:34.500

如果您再次运行插入,它将导致:

Id          CustomerId  Name       ValidFrom               ValidTo                 CreatedOn               ModifiedOn
----------- ----------- ---------- ----------------------- ----------------------- ----------------------- -----------------------
1           1           AAB        2015-07-04 13:21:34.500 2015-07-04 13:22:02.163 2015-07-04 13:21:34.500 2015-07-04 13:22:02.163
2           1           AAB        2015-07-04 13:22:02.153 2016-07-04 13:22:02.153 2015-07-04 13:22:02.153 2015-07-04 13:22:02.153

此触发器只会使特定 CustomerId 的所有旧行无效。

如果您想在发生更新时另外设置 ModifiedOn 日期,您可以创建这个额外的触发器:

CREATE TRIGGER dbo.TR_UpdateModifiedOn
   ON  CustomerHistory 
   AFTER UPDATE
AS 
BEGIN
    SET NOCOUNT ON;

    -- Debug
    --SELECT *
    --FROM inserted

    UPDATE ch
    SET ModifiedOn = GETDATE()
    FROM inserted as i
    INNER JOIN CustomerHistory as ch
            ON i.id = ch.Id
END
GO

顺便说一句,处理这个问题的过程会比触发器更好。但在某些情况下,您无法提供程序。例如,如果用户将使用SSMSAccess 操作数据或通过任何其他应用程序直接更新。

根据反馈进行编辑

我保留上面的旧代码,以防其他人需要上述解决方案。在您的情况下,您只想阻止所有更新,除非它们仅更新 ModifiedOnValidTo

在这种情况下,这个触发器会解决它:

CREATE TRIGGER dbo.TR_InsteadUpdate
   ON CustomerHistory 
   INSTEAD OF UPDATE
AS 
BEGIN
    SET NOCOUNT ON;

    -- Add your conditions here
    IF (
        -- e.g. Updated a row without modifieng the ModifiedOn Column
        SELECT COUNT(*)
        FROM inserted as i
        INNER JOIN deleted as d
                ON i.Id = d.Id
                AND i.CustomerId = d.CustomerId
                AND i.Name = d.Name
                AND i.ValidFrom = d.ValidFrom
                AND i.CreatedOn = d.CreatedOn
                AND i.ValidTo <> d.ValidTo
                AND i.ModifiedOn <> d.ModifiedOn
    ) = 0 BEGIN
        -- Not allowed, rollback
        RAISERROR(N'Not allowed!',16,1) WITH NOWAIT

        ROLLBACK TRANSACTION
    END

    -- Otherwise update it
    UPDATE ch
    SET ModifiedOn = i.ModifiedOn, ValidTo = i.ValidTo
    FROM CustomerHistory AS ch
    INNER JOIN inserted AS i
            ON i.Id = ch.Id
END
GO

这段代码会抛出异常:

-- not allowed
UPDATE CustomerHistory
SET Name = N'EEE'
WHERE id = 2

虽然这个可以工作:

-- allowed
UPDATE CustomerHistory
SET ModifiedOn = GETDATE(),
    ValidTo = DATEADD(day,1,GETDATE())
WHERE id = 2

另一个添加可以是INSTEAD OF-Trigger 内的WHERE-clause IF-clause。如果插入时间超过 60 秒(例如),这将阻止对上述两个列的更新。这可以通过将此行添加到IF 来实现:

WHERE DATEDIFF(SECOND,d.CreatedOn,i.ModifiedOn) < 60

这段代码的结果:

CREATE TRIGGER dbo.TR_InsteadUpdate
   ON CustomerHistory 
   INSTEAD OF UPDATE
AS 
BEGIN
    SET NOCOUNT ON;

    -- Add your conditions here
    IF (
        -- e.g. Updated a row without modifieng the ModifiedOn Column
        SELECT COUNT(*)
        FROM inserted as i
        INNER JOIN deleted as d
                ON i.Id = d.Id
                AND i.CustomerId = d.CustomerId
                AND i.Name = d.Name
                AND i.ValidFrom = d.ValidFrom
                AND i.CreatedOn = d.CreatedOn
                AND i.ValidTo <> d.ValidTo
                AND i.ModifiedOn <> d.ModifiedOn
        WHERE DATEDIFF(SECOND,d.CreatedOn,i.ModifiedOn) < 60
    ) = 0 BEGIN
        -- Not allowed, rollback
        RAISERROR(N'Not allowed!',16,1) WITH NOWAIT

        ROLLBACK TRANSACTION
    END

    -- Otherwise update it
    UPDATE ch
    SET ModifiedOn = i.ModifiedOn, ValidTo = i.ValidTo
    FROM CustomerHistory AS ch
    INNER JOIN inserted AS i
            ON i.Id = ch.Id
END
GO

添加非常广泛的表格

如果您有一个包含许多列的非常宽泛的表,并且您不会维护所有列而只想添加允许的列,您可能可以使用以下触发器,它使用哈希来比较旧行和新行.它的工作原理与上述相同,但使用的是动态哈希算法。

CREATE TRIGGER dbo.TR_InsteadUpdate
   ON CustomerHistory 
   INSTEAD OF UPDATE
AS 
BEGIN
    SET NOCOUNT ON;

    DECLARE @sql nvarchar(max), @hash_before varbinary(max), 
            @hash_after varbinary(max), @columnlist nvarchar(max),
            @paramDefinition nvarchar(500) = N'@hash_value varbinary(max) OUTPUT';

    SELECT @columnlist = COALESCE(
                        @columnlist + N'+ISNULL(CONVERT(nvarchar(max),'+ COLUMN_NAME + N'),N'''')', 
                        N'ISNULL(CONVERT(nvarchar(max),'+ COLUMN_NAME + N'),N'''')')
    FROM INFORMATION_SCHEMA.COLUMNS
    WHERE TABLE_NAME = N'CustomerHistory'
        -- Which columns are allowed to be updated?
        AND COLUMN_NAME NOT IN(N'ModifiedOn',N'ValidTo')

    -- needed due to scope of deleted and inserted
    SELECT * INTO #deleted FROM deleted
    -- Get the hash-value for the before-values
    SET @sql = N'
        SELECT @hash_value = HASHBYTES(''SHA1'','+@columnlist+')
        FROM #deleted'
    EXECUTE sp_executesql @sql, @paramDefinition, @hash_value = @hash_before OUTPUT;
    DROP TABLE #deleted

    SELECT * INTO #inserted FROM inserted
    -- Get the hash-value for the after-values
    SET @sql = N'
        SELECT @hash_value = HASHBYTES(''SHA1'','+@columnlist+')
        FROM #inserted'
    EXECUTE sp_executesql @sql, @paramDefinition, @hash_value = @hash_after OUTPUT;
    DROP TABLE #inserted

    SELECT @hash_before, @hash_after

    IF (@hash_before <> @hash_after) BEGIN
        -- Not allowed, rollback
        RAISERROR(N'Not allowed!',16,1) WITH NOWAIT

        ROLLBACK TRANSACTION
    END

    -- Otherwise update it
    UPDATE ch
    SET ModifiedOn = i.ModifiedOn, ValidTo = i.ValidTo
    FROM CustomerHistory AS ch
    INNER JOIN inserted AS i
            ON i.Id = ch.Id
END
GO

【讨论】:

  • 非常感谢,离子。还有一件事我之前应该提到 - 实际的 CustomerHistory 表有 100 多列宽,并且随着每次接口应用程序升级而增长 - 我无法将所有列都放入脚本中。
  • 没问题。我会提供一个解决方法。但有一件事。我可以假设您的应用程序不会更新递增的 ID 吗?
  • 好吧,我决定不忽略 id。相反,我为它创建了一个动态哈希算法。您可以查看答案中添加的内容。它有点复杂,但是是动态的,并且会随着您的表定义而适应。您只需要指定允许更新哪些列(参见INFORMATION_SCHEMA 的部分)。
  • 顺便说一句。我建议您过滤INFORMATION_SCHEMA,而不仅仅是TABLE_NAME。您应该包括目录以及模式。这会更好,并且会避免错误。只是作为一个提示。我不知道您的架构和数据库名称,这样我就省略了。 :-)
  • 非常感谢 Ionic。剧本有一些空白,但我会继续努力的。谢谢!
【解决方案2】:

理想情况下,我建议只允许用户执行将封装逻辑的存储过程。
如果不能使用存储过程,可以在表上使用而不是更新触发器。这样,更新现有记录的唯一方法就是通过此触发器。

【讨论】:

  • 是的,这将是一个解决方案。通常我也会推荐程序,但在某些情况下,您确实需要直接访问数据的用户。这样,程序就不是一个好的解决方案。 :-)
  • 如上所述——所有更新都由接口应用程序进行——我无法更改任何记录中的任何数据。触发器唯一需要做的就是显示错误信息和回滚事务。
猜你喜欢
  • 1970-01-01
  • 2015-12-18
  • 1970-01-01
  • 2022-07-28
  • 1970-01-01
  • 2021-01-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多