将 INSTEAD OF 触发器与 SQL 服务器时态表相结合以实现优势的解决方法

Workaround to achieve the benefits combining INSTEAD OF triggers with SQL Server Temporal tables

我想获得将 INSTEAD OF INSERT 和 INSTEAD OF UPDATE 触发器与 Temporal tables 相结合的好处。

INSTEAD OF 触发器在多个列中强制执行唯一性,其中一列在不同但相关的 table 中。如果用户尝试插入或更新打破唯一性不变量的记录,它们会抛出异常。

时间跟踪记录了什么、什么时候和(可能)谁。

因此,INSTEAD OF 触发器和时间跟踪都有好处。我很好奇是否有解决方法来实现两者的好处。

Temporal Table Considerations and Limitations 文档页面表明

INSTEAD OF triggers are not permitted on either the current or the history table to avoid invalidating the DML logic.

但也许有一个聪明的解决方法。

在下面的代码示例中,我想将时间跟踪添加到 [HubAssembly].[AftermarketParts] table。但它的 INSTEAD OF 触发器阻止将其变成临时 table.

CREATE SCHEMA [Part] AUTHORIZATION [dbo];
GO

CREATE SCHEMA [HubAssembly] AUTHORIZATION [dbo];
GO

CREATE TABLE [Part].[Types] (
    [Id]   INT IDENTITY (1, 1) NOT NULL,
    [Name] NVARCHAR (50)       NOT NULL,
    CONSTRAINT [PK_Part.Types] PRIMARY KEY CLUSTERED ([Id] ASC),
    CONSTRAINT [Part.Types.Name_Unique] UNIQUE NONCLUSTERED ([Name] ASC)
);
GO

CREATE TABLE [Part].[Parts] (
    [PartNumber]             NVARCHAR (18)  NOT NULL,
    [PartTypeId]             INT            NOT NULL,
    CONSTRAINT [PK_Part.Parts] PRIMARY KEY CLUSTERED ([PartNumber] ASC),
    CONSTRAINT [FK_Part.Parts_Part.Types_PartTypeId] FOREIGN KEY ([PartTypeId]) REFERENCES [Part].[Types] ([Id])
);
GO

CREATE TABLE [HubAssembly].[AftermarketPartUsages] (
    [Id]   INT IDENTITY (0, 1) NOT NULL,
    [Name] NVARCHAR (50)       NOT NULL,
    CONSTRAINT [PK_HubAssembly.AftermarketPartUsages] PRIMARY KEY CLUSTERED ([Id] ASC),
    CONSTRAINT [IX_HubAssembly_AftermarketPartUsages_Name_Unique] UNIQUE NONCLUSTERED ([Name] ASC),
);
GO

CREATE FUNCTION [Part].[f_PartTypeId] (@PartNumber AS NVARCHAR(18))
RETURNS INT
AS  
BEGIN
  DECLARE @Return AS INT
  SET @Return = (SELECT PartTypeId FROM Part.Parts WHERE PartNumber = @PartNumber)
  RETURN @Return
END
GO

CREATE TABLE [HubAssembly].[AftermarketParts] (
    [Id]                    INT          IDENTITY (0, 1) NOT NULL,
    [HubAssemblyNumber]     NVARCHAR(18) NOT NULL,
    [AftermarketPartNumber] NVARCHAR(18) NOT NULL,
    [Ranking]               INT          CONSTRAINT [DF_HubAssembly_AftermarketParts_Ranking] DEFAULT ((1)) NOT NULL,
    [UsageId]               INT          DEFAULT ((0)) NOT NULL,
    CONSTRAINT [PK_HubAssembly.AftermarketParts] PRIMARY KEY NONCLUSTERED ([Id] ASC),
    CONSTRAINT [FK_HubAssembly.AftermarketParts_HubAssembly.AftermarketPartUsages_UsageId] FOREIGN KEY ([UsageId]) REFERENCES [HubAssembly].[AftermarketPartUsages] ([Id]),

    -- FK_HubAssembly.AftermarketParts_Part.Parts_HubAssemblyNumber is simplified here. It actually is a foreign key to HubAssembly.HubAssemblies.HubAssemblyNumber which is a FK to Part.Parts.PartNumber
    CONSTRAINT [FK_HubAssembly.AftermarketParts_Part.Parts_HubAssemblyNumber] FOREIGN KEY ([HubAssemblyNumber]) REFERENCES [Part].[Parts] ([PartNumber]),

    CONSTRAINT [FK_HubAssembly.AftermarketParts_Part.Parts_AftermarketPartNumber] FOREIGN KEY ([AftermarketPartNumber]) REFERENCES [Part].[Parts] ([PartNumber]),
);
GO

CREATE TRIGGER [HubAssembly].[Trigger_AftermarketParts_InsteadOf_Update] ON [HubAssembly].[AftermarketParts]
    INSTEAD OF UPDATE
    AS
    BEGIN
        SET NOCOUNT ON;
        DECLARE @AnyDuplicates AS BIT = 0;
        WITH cte AS (
            SELECT HubAssemblyNumber, Ranking, Part.f_PartTypeId(AftermarketPartNumber) AS PartTypeId, AftermarketPartNumber, UsageId
            FROM INSERTED
            UNION 
            SELECT HubAssemblyNumber, Ranking, Part.f_PartTypeId(AftermarketPartNumber), AftermarketPartNumber, UsageId
            FROM [HubAssembly].[AftermarketParts]
            WHERE HubAssemblyNumber IN (SELECT HubAssemblyNumber FROM INSERTED)
              AND Id NOT IN (SELECT Id FROM DELETED)
        )
        SELECT TOP 1 @AnyDuplicates = 1
        FROM cte
        GROUP BY HubAssemblyNumber, PartTypeId, Ranking, UsageId
        HAVING (COUNT(AftermarketPartNumber) > 1)
        IF @AnyDuplicates = 1
            THROW 50001, N'Performing this update would result in duplicate values for ([HubAssemblyNumber], [Ranking], [Part].[f_PartTypeId](AftermarketPartNumber), [UsageId]). This tuple should have distinct values for all rows in the table.', 1;
        ELSE
            BEGIN
                DELETE FROM [HubAssembly].[AftermarketParts] WHERE Id IN (SELECT Id FROM DELETED);
                INSERT INTO [HubAssembly].[AftermarketParts] ([HubAssemblyNumber], [AftermarketPartNumber], [Ranking], [UsageId])
                    SELECT [HubAssemblyNumber], [AftermarketPartNumber], [Ranking], [UsageId]
                    FROM INSERTED;
            END;
    END
GO

CREATE TRIGGER [HubAssembly].[Trigger_AftermarketParts_InsteadOf_Insert] ON [HubAssembly].[AftermarketParts]
    INSTEAD OF INSERT
    AS
    BEGIN
        SET NOCOUNT ON;
        DECLARE @AnyDuplicates AS BIT = 0;
        WITH cte AS (
            SELECT HubAssemblyNumber, Ranking, Part.f_PartTypeId(AftermarketPartNumber) AS PartTypeId, AftermarketPartNumber, UsageId
            FROM INSERTED
            UNION 
            SELECT HubAssemblyNumber, Ranking, Part.f_PartTypeId(AftermarketPartNumber), AftermarketPartNumber, UsageId
            FROM [HubAssembly].[AftermarketParts]
            WHERE HubAssemblyNumber IN (SELECT HubAssemblyNumber FROM INSERTED)
        )
        SELECT TOP 1 @AnyDuplicates = 1
        FROM cte
        GROUP BY HubAssemblyNumber, PartTypeId, Ranking, UsageId
        HAVING (COUNT(AftermarketPartNumber) > 1)
        IF @AnyDuplicates = 1
            THROW 50001, N'Performing this insert would result in duplicate values for ([HubAssemblyNumber], [Ranking], [Part].[f_PartTypeId](AftermarketPartNumber), [UsageId]). This tuple should have distinct values for all rows in the table.', 1;
        ELSE
            INSERT INTO [HubAssembly].[AftermarketParts] ([HubAssemblyNumber], [AftermarketPartNumber], [Ranking], [UsageId])
                SELECT [HubAssemblyNumber], [AftermarketPartNumber], [Ranking], [UsageId]
                FROM INSERTED;
    END
GO

CREATE UNIQUE CLUSTERED INDEX [HubAssemblyNumber_AftermarketPartNumber_UsageId_Unique]
    ON [HubAssembly].[AftermarketParts]([HubAssemblyNumber] ASC, [AftermarketPartNumber] ASC, [UsageId] ASC);
GO

这里有一些数据来填充这些 table:

SET IDENTITY_INSERT Part.Types ON;
GO
INSERT INTO Part.Types (Id, Name) VALUES (5, N'Wheel Stud');
INSERT INTO Part.Types (Id, Name) VALUES (7, N'ABS Tone Ring');
INSERT INTO Part.Types (Id, Name) VALUES (8, N'Port Plug');
INSERT INTO Part.Types (Id, Name) VALUES (14, N'Seal');
INSERT INTO Part.Types (Id, Name) VALUES (217, N'PreSet Complete Hub Rebuild Kit (Keyway)');
INSERT INTO Part.Types (Id, Name) VALUES (221, N'Wheel Seal and Spacer Kit');
INSERT INTO Part.Types (Id, Name) VALUES (115, N'Hub Assembly');
INSERT INTO Part.Types (Id, Name) VALUES (6, N'Double-Ended Stud');
INSERT INTO Part.Types (Id, Name) VALUES (101, N'Bearing Cup & Cone Assembly');
GO
SET IDENTITY_INSERT Part.Types OFF;
GO
INSERT INTO Part.Parts (PartNumber, PartTypeId) VALUES (N'103705', 7);
INSERT INTO Part.Parts (PartNumber, PartTypeId) VALUES (N'105272', 8);
INSERT INTO Part.Parts (PartNumber, PartTypeId) VALUES (N'10001405', 5);
INSERT INTO Part.Parts (PartNumber, PartTypeId) VALUES (N'10009780', 7);
INSERT INTO Part.Parts (PartNumber, PartTypeId) VALUES (N'10018436', 5);
INSERT INTO Part.Parts (PartNumber, PartTypeId) VALUES (N'10045871', 217);
INSERT INTO Part.Parts (PartNumber, PartTypeId) VALUES (N'10080124', 14);
INSERT INTO Part.Parts (PartNumber, PartTypeId) VALUES (N'10081518', 221);
INSERT INTO Part.Parts (PartNumber, PartTypeId) VALUES (N'10082204', 115);
INSERT INTO Part.Parts (PartNumber, PartTypeId) VALUES (N'10083796', 115);
INSERT INTO Part.Parts (PartNumber, PartTypeId) VALUES (N'10018442', 6);
INSERT INTO Part.Parts (PartNumber, PartTypeId) VALUES (N'10041905', 101);
INSERT INTO Part.Parts (PartNumber, PartTypeId) VALUES (N'10041906', 101);
INSERT INTO Part.Parts (PartNumber, PartTypeId) VALUES (N'10041915', 101);
INSERT INTO Part.Parts (PartNumber, PartTypeId) VALUES (N'10041916', 101);
GO

SET IDENTITY_INSERT HubAssembly.AftermarketPartUsages ON;
INSERT INTO HubAssembly.AftermarketPartUsages(Id, Name) VALUES (0, N'—');
INSERT INTO HubAssembly.AftermarketPartUsages(Id, Name) VALUES (1, N'Inboard Bearing Set');
INSERT INTO HubAssembly.AftermarketPartUsages(Id, Name) VALUES (2, N'Outboard Bearing Set');
INSERT INTO HubAssembly.AftermarketPartUsages(Id, Name) VALUES (3, N'Axle Stud');
GO
SET IDENTITY_INSERT HubAssembly.AftermarketPartUsages OFF;
GO

INSERT INTO HubAssembly.AftermarketParts (HubAssemblyNumber, AftermarketPartNumber, Ranking, UsageId) VALUES (N'10082204', N'10001405', 1, 0);
INSERT INTO HubAssembly.AftermarketParts (HubAssemblyNumber, AftermarketPartNumber, Ranking, UsageId) VALUES (N'10082204', N'10009780', 1, 0);
INSERT INTO HubAssembly.AftermarketParts (HubAssemblyNumber, AftermarketPartNumber, Ranking, UsageId) VALUES (N'10082204', N'10041905', 1, 1);
INSERT INTO HubAssembly.AftermarketParts (HubAssemblyNumber, AftermarketPartNumber, Ranking, UsageId) VALUES (N'10082204', N'10041906', 1, 2);
INSERT INTO HubAssembly.AftermarketParts (HubAssemblyNumber, AftermarketPartNumber, Ranking, UsageId) VALUES (N'10082204', N'10045871', 1, 0);
INSERT INTO HubAssembly.AftermarketParts (HubAssemblyNumber, AftermarketPartNumber, Ranking, UsageId) VALUES (N'10082204', N'10081518', 1, 0);
INSERT INTO HubAssembly.AftermarketParts (HubAssemblyNumber, AftermarketPartNumber, Ranking, UsageId) VALUES (N'10083796', N'10018436', 1, 0);
INSERT INTO HubAssembly.AftermarketParts (HubAssemblyNumber, AftermarketPartNumber, Ranking, UsageId) VALUES (N'10083796', N'10018442', 1, 3);
INSERT INTO HubAssembly.AftermarketParts (HubAssemblyNumber, AftermarketPartNumber, Ranking, UsageId) VALUES (N'10083796', N'10041915', 1, 1);
INSERT INTO HubAssembly.AftermarketParts (HubAssemblyNumber, AftermarketPartNumber, Ranking, UsageId) VALUES (N'10083796', N'10041916', 1, 2);
INSERT INTO HubAssembly.AftermarketParts (HubAssemblyNumber, AftermarketPartNumber, Ranking, UsageId) VALUES (N'10083796', N'10080124', 1, 0);
INSERT INTO HubAssembly.AftermarketParts (HubAssemblyNumber, AftermarketPartNumber, Ranking, UsageId) VALUES (N'10083796', N'103705', 1, 0);
INSERT INTO HubAssembly.AftermarketParts (HubAssemblyNumber, AftermarketPartNumber, Ranking, UsageId) VALUES (N'10083796', N'105272', 1, 0);
GO

INSTEAD OF 触发器强制跨 [HubAssembly] 中的四列(HubAssemblyNumber、Ranking、AftermarketPartNumber、UsageId)的唯一性。[AftermarketParts] table 和相关 [Part] 中的一列。[Parts] table 通过 Part.f_PartTypeId() 函数。

在插入和更新时在数据库中强制执行 two-table/five-column 唯一性验证是理想的。不幸的是,执行强制执行的触发器禁止将 table 转换为时间 table。但由于自动更改跟踪也非常有用,我想在 table 中添加时间跟踪。

还有其他方法可以实现这些双重好处吗?

答案很简单。只需将 INSTEAD OF 触发器替换为 AFTER 触发器就可以了。