INSTEAD OF 触发器内的确定性

Han*_*non 1 trigger sql-server referential-integrity

参考我之前关于首选设计的问题,以避免循环或多个更新路径参照完整性通过ON UPDATE CASCADE和级联更新和删除ON DELETE CASCADE,我正在尝试使用触发器作为替代方案。

对于这个例子,我试图尽可能地简化设计。

我有一张Parent桌子和一张Child桌子;ParentChild共享一列,ParentName。每当Parent.ParentName发生变化时,我都想在Child.ParentName.

够简单吗?如果我更新 中的单行Parent,我可以使用inserteddeleted表很容易地知道要更新ParentChild表中的哪一行。像这样的东西:

UPDATE Parent
SET Parent.ParentName = inserted.ParentName
FROM Parent
    INNER JOIN deleted ON Parent.ParentName = deleted.ParentName
    CROSS JOIN inserted;

UPDATE Child
SET Child.ParentName = inserted.ParentName
FROM Child
    INNER JOIN deleted ON Child.ParentName = deleted.ParentName
    CROSS JOIN inserted;
Run Code Online (Sandbox Code Playgroud)

很明显,如果Parent在一个UPDATE语句中修改了多于一行,这将不起作用。

表:

IF OBJECT_ID(N'dbo.Parent', N'U') IS NOT NULL
DROP TABLE dbo.Parent;
CREATE TABLE dbo.Parent
(
    ParentName int NOT NULL
        PRIMARY KEY
    , IsActive bit NOT NULL
);

IF OBJECT_ID(N'dbo.Child', N'U') IS NOT NULL
DROP TABLE dbo.Child;
CREATE TABLE dbo.Child
(
    ChildName int NOT NULL
        PRIMARY KEY
    , ParentName int NOT NULL
    , IsActive bit NOT NULL
);
GO

INSERT INTO dbo.Parent (ParentName, IsActive)
VALUES (0, 1)
    , (1, 1)
    , (2, 1);

INSERT INTO dbo.Child (ChildName, ParentName, IsActive)
VALUES (7, 0, 1)
    , (8, 1, 1)
    , (9, 2, 1);

SELECT *
FROM dbo.Parent;
Run Code Online (Sandbox Code Playgroud)
???????????????????????????
? 父母名字 ?活跃 ?
???????????????????????????
? 0 ? 1 ?
? 1 ? 1 ?
? 2 ? 1 ?
???????????????????????????
SELECT *
FROM dbo.Child;
Run Code Online (Sandbox Code Playgroud)
??????????????????????????????????????????
? 孩子姓名 ? 父母名字 ?活跃 ?
??????????????????????????????????????????
? 7 ? 0 ? 1 ?
? 8 ? 1 ? 1 ?
? 9 ? 2 ? 1 ?
??????????????????????????????????????????

触发:

CREATE TRIGGER DRI
ON dbo.Parent
INSTEAD OF UPDATE
AS
BEGIN
    UPDATE Parent
    SET Parent.ParentName = inserted.ParentName
    FROM Parent
        INNER JOIN deleted ON Parent.ParentName = deleted.ParentName
        CROSS JOIN inserted;

    UPDATE Child
    SET Child.ParentName = inserted.ParentName
    FROM Child
        INNER JOIN deleted ON Child.ParentName = deleted.ParentName
        CROSS JOIN inserted;
END
GO
Run Code Online (Sandbox Code Playgroud)

测试它:

UPDATE dbo.Parent SET ParentName = 6 WHERE ParentName = 2;
Run Code Online (Sandbox Code Playgroud)

结果:

SELECT *
FROM dbo.Parent;
Run Code Online (Sandbox Code Playgroud)
???????????????????????????
? 父母名字 ?活跃 ?
???????????????????????????
? 0 ? 1 ?
? 1 ? 1 ?
? 6 ? 1 ? <-- 此行已更改
???????????????????????????
SELECT *
FROM dbo.Child;
Run Code Online (Sandbox Code Playgroud)
??????????????????????????????????????????
? 孩子姓名 ? 父母名字 ?活跃 ?
??????????????????????????????????????????
? 7 ? 0 ? 1 ?
? 8 ? 1 ? 1 ?
? 9 ? 6 ? 1 ? <-- 此行已更改
??????????????????????????????????????????

但是,如果我更新多行,则会得到以下信息:

UPDATE dbo.Parent SET ParentName = ParentName + 1;

SELECT *
FROM dbo.Parent;
Run Code Online (Sandbox Code Playgroud)
???????????????????????????
? 父母名字 ?活跃 ?
???????????????????????????
? 1 ? 1 ?
? 1 ? 1 ?
? 1 ? 1 ?
???????????????????????????
SELECT *
FROM dbo.Child;
Run Code Online (Sandbox Code Playgroud)
??????????????????????????????????????????
? 孩子姓名 ? 父母名字 ?活跃 ?
??????????????????????????????????????????
? 7 ? 1 ? 1 ?
? 8 ? 1 ? 1 ?
? 9 ? 1 ? 1 ?
??????????????????????????????????????????

我可以通过使触发器拒绝更新超过一行的更新来解决这个问题,但这不是很可扩展,并且会促进 RBAR。

CREATE TRIGGER DRI
ON dbo.Parent
INSTEAD OF UPDATE
AS
BEGIN
    DECLARE @msg nvarchar(1000);
    SET @msg = 'Only a single row can be updated per batch.';
    IF (SELECT COUNT(1) FROM inserted) > 1 
    BEGIN
        RAISERROR (@msg, 10, 1);
    END
    ELSE
    BEGIN
        UPDATE Parent
        SET Parent.ParentName = inserted.ParentName
        FROM Parent
            INNER JOIN deleted ON Parent.ParentName = deleted.ParentName
            CROSS JOIN inserted;

        UPDATE Child
        SET Child.ParentName = inserted.ParentName
        FROM Child
            INNER JOIN deleted ON Child.ParentName = deleted.ParentName
            CROSS JOIN inserted;
    END
END
GO
Run Code Online (Sandbox Code Playgroud)

我能做些什么来缓解这种情况吗?显然,我不想在IDENTITY这些表中添加一列,因为这将否定使用自然键的目的。

我想ROW_NUMBER()在触发器中使用inserteddeleted表上的一种伪列;但是我不能ORDER BYOVER(...)子句中使用“适当的” ,这使得行号不确定:

CREATE TRIGGER DRI
ON dbo.Parent
INSTEAD OF UPDATE
AS
BEGIN
    ;WITH i AS (
        SELECT *
            , rn = ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) 
        FROM inserted
        )
    , d AS (
        SELECT *
            , rn = ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) 
        FROM deleted
        )
    UPDATE dbo.Parent
    SET Parent.ParentName = i.ParentName
    FROM i
        INNER JOIN d ON i.rn = d.rn
    WHERE Parent.ParentName = d.ParentName

    ;WITH i AS (
        SELECT *
            , rn = ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) 
        FROM inserted
        )
    , d AS (
        SELECT *
            , rn = ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) 
        FROM deleted
        )
    UPDATE dbo.Child
    SET Child.ParentName = i.ParentName
    FROM i
        INNER JOIN d ON i.rn = d.rn
    WHERE Child.ParentName = d.ParentName
END
GO
Run Code Online (Sandbox Code Playgroud)

考试:

UPDATE dbo.Parent SET ParentName = ParentName + 1;

SELECT *
FROM dbo.Parent;
Run Code Online (Sandbox Code Playgroud)
???????????????????????????
? 父母名字 ?活跃 ?
???????????????????????????
? 1 ? 1 ?
? 2 ? 1 ?
? 3 ? 1 ?
???????????????????????????
SELECT *
FROM dbo.Child;
Run Code Online (Sandbox Code Playgroud)
??????????????????????????????????????????
? 孩子姓名 ? 父母名字 ?活跃 ?
??????????????????????????????????????????
? 7 ? 1 ? 1 ?
? 8 ? 2 ? 1 ?
? 9 ? 3 ? 1 ?
??????????????????????????????????????????

似乎确实解决了问题,但我担心它不可靠。

Pau*_*ite 6

我想ROW_NUMBER()在触发器中使用作为一种伪列......

别再想这个了。插入删除的伪表中的行顺序无法保证。您可能会观察到它“工作”,但您将依赖于无证行为。

缺少用于关联更新行(或更新的伪表或每行触发器等)的公开唯一行标识符的标准解决方法是添加不可变的备用键,例如具有IDENTITY属性的列. 我意识到这违背了问题的设计目标,但这并没有改变事实。

rowversion列也可以工作,但那是 8 个字节,而是误用。

连接物品: