更新行时,如何同时保留旧值并更新链接表?

When updating a row, how to also keep the old values and update the linking tables?

我有两张桌子; ItemTableProductTableProductTableItemID 列链接到 ItemTableID 列。两个表的 ID 列都是主键和标识列。

像这样:

ItemTable:

ID    Col      ColOther    Latest    Time
100   'old'    'oldother'  1         <Autogenerated timestamp>


ProductTable:

ID    ItemID   Value   ValueOther   Latest   Time
12    100      'foo'   'bar'        1        <Autogenerated timestamp>

每当我想 手动 UPDATE ItemTable 中的一行时,通常只需一个查询即可完成:

query = \
    """
    UPDATE ItemTable
    SET Col = ?, ColOther = ?
    WHERE ID = 100;
    """
cursor.execute(query, 'new', 'newother')

我不想像上面那样只做 UPDATE,我想为 ItemTable 做这些事情:

然后 ProductTable:

这个自动 INSERTUPDATE 最好只用一个纯 SQL 查询(可能使用 OUTPUT,但我不熟悉),或者它可以用一些 Python 代码来实现。我该怎么做呢?

想要的最终结果:

ItemTable:

ID    Col      ColOther     Latest    Time
100   'old'    'oldother'   0         <Autogenerated timestamp>
250   'new'    'newother'   1         <Autogenerated timestamp>


ProductTable:

ID    ItemID   Value   ValueOther   Latest   Time
12    100      'foo'   'bar'        0        <Autogenerated timestamp>
110   250      'foo'   'bar'        1        <Autogenerated timestamp>

为此使用 OUTPUT

我不会为此使用触发器,触发器中的太多 activity 会导致在发生其他更改时发生不希望的事情。相反,我正在使用存储过程

改题后我重写了答案:

-- create test tables and test data
CREATE table item
(ID int identity, 
Col varchar(99), 
ColOther varchar(99),
Latest bit default 1,
Time datetime default getdate())

INSERT item(col, colother) 
values('old','oldother')

CREATE table product
(ID int identity,
ItemID int,
[Value] varchar(20),
ValueOther varchar(20),
Latest bit default 1,
Time datetime default getdate())

INSERT product(itemid, [value], valueother)
values(1, 'foo', 'bar')

go
-- create procedure
CREATE procedure p_insert
(
  @id int,
  @col varchar(99),
  @colOther varchar(99)
)
as
BEGIN tran t
DECLARE @out table(IDold int, IDnew int)
INSERT item(col, colother, Latest)
OUTPUT @id, inserted.id INTO @out
SELECT @col, @colother, 1
FROM item
WHERE 
  id = @id
  and latest = 1

UPDATE i
SET Latest=0
FROM item i
JOIN @out o 
ON o.IDold = i.id
and i.Latest=1

DECLARE @p table
(itemid int,
[value] varchar(20),
valueother varchar(20))

UPDATE p
SET Latest=0
OUTPUT o.IDnew, deleted.[value],deleted.[valueother] 
INTO @p
FROM product p
JOIN @out o
ON p.ItemID = o.IDold
WHERE p.Latest=1

INSERT product(itemid, [value], valueother,  Latest)
SELECT itemid, value, valueother, 1
FROM @p
commit tran t

要对此进行测试:

exec p_insert 1, 'a','b'

请注意,这仅在您尝试更新 Latest=1 的现有行时才有效。

下面的触发器将执行您想要的操作:

CREATE TRIGGER ItemTable_OnUpdate
ON ItemTable
INSTEAD OF UPDATE
AS
BEGIN
    DECLARE @newId int, @oldId int


    INSERT INTO ItemTable(Col, ColOther, Latest)
    SELECT Col, ColOther, 1 FROM INSERTED

    --get old and new ids
    SELECT @newId=@@IDENTITY, @oldId=ID FROM INSERTED

    UPDATE ItemTable SET Latest=0 WHERE ID=@oldId


    --updating ProductTable
    INSERT INTO ProductTable (ItemID, [Value], ValueOther, Latest)
    SELECT @newId, [Value], ValueOther, 1 FROM ProductTable WHERE ItemID=@oldId


    UPDATE ProductTable SET Latest=0 WHERE ItemID=@oldId
END;

创建触发器后,任何更新都会在 ItemTable 中插入新记录,更新上一条记录。使用 Latest = 0,并根据您的要求更新子项 table。

下面是我更新后的输出:

UPDATE ItemTable SET col='new', ColOther='newother' WHERE ID=12;

项目表:

ID  Col   ColOther  Latest  Time
12  old   oldother  0       0x00000000000007F0
13  new   newother  1       0x00000000000007EF

产品表:

ID  ItemID  Value   ValueOther  Latest  Time
18  12      foo     bar         0       0x00000000000007F2
19  13      foo     bar         1       0x00000000000007F1

对于多行更新,我们可以使用下面的触发器,因为上面的触发器被设计为请求者指定的更新(一行)的参考。我相信下面将处理多行更新。请尝试一下。

create TRIGGER ItemTable_OnUpdate
ON ItemTable
INSTEAD OF UPDATE
AS
BEGIN

    Declare @s table( IdNew int,IdOld int)


MERGE INTO ItemTable AS dest
USING INSERTED AS ins ON 1=0   -- always false
    WHEN NOT MATCHED BY TARGET          -- happens for every row, because 1 is never 0
        THEN 
            INSERT (Col, ColOther, Latest)
                     VALUES (Col, ColOther, Latest)
            OUTPUT inserted.ID, ins.ID INTO @s (IdNew, IdOld);


UPDATE ItemTable SET Latest=0 WHERE ID in (select IdOld from @s)


    --updating ProductTable
    INSERT INTO ProductTable (ItemID, [Value], ValueOther, Latest)
    SELECT s.IdNew, pt.[Value], pt.ValueOther, 1 FROM @s s
        inner join ProductTable pt on pt.ItemID=s.IdOld


    UPDATE ProductTable SET Latest=0 WHERE ItemID in (select IdOld from @s)
END;

如果您使用的是 SQL Server 的现代版本(即 >= SQL Server 2016 SP1)

您是否考虑过为此使用 Temporal Table? 您可以在 Microsofts BOL

阅读更多信息

这是跟踪 tables 更改的一种非常聪明的方法。它表现良好,但改变 table 结构有点工作。