触发以阻止某些更新并允许插入

时间:2015-07-04 10:34:50

标签: sql sql-server triggers

我有一个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已更新)。由于在第一种方式中完成了几个不需要的更改(仅更新当前记录),我需要阻止它,因此每次更改都以第二种方式完成(插入新记录并更新前一个记录)。我需要使用触发器进行操作,以便用户获得特殊的错误信息。

知道如何应对吗?

2 个答案:

答案 0 :(得分:2)

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

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

如果再次运行Insert,将导致:

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

另一个附加可以是WHERE - INSTEAD OF - IF - 子句内的触发器内的IF - 子句。如果Insert超过60秒(例如),这将阻止对两个提到的列进行更新。这可以通过将此行添加到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

添加非常广泛的表格

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

Route::resource('user', 'UserController');

答案 1 :(得分:0)

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