确定SQL触发器中的旧主键

时间:2009-03-04 06:06:48

标签: sql-server sql-server-2000 triggers

我已经在某个地方做过这件事我确定了!

我有一个SQL Server 2000表,我需要记录对更新字段的更改并插入第二个Logging表。我正在使用的结构的简化版本如下:

MainTable
ID varchar(10) PRIMARY KEY
DESCRIPTION varchar(50)

LogTable
OLDID varchar(10)
NEWID varchar(10)

对于任何其他领域,这样的事情会很有效:

Select i.DESCRIPTION As New, d.DESCRIPTION As Old 
From Inserted i
LEFT JOIN Deleted d On i.ID=d.ID

...但很明显,如果ID被更改,连接将会失败。

我无法修改表格,我在这个数据库中唯一的功能就是创建一个触发器。

或者有人可以教我时间旅行,我会回到过去然后问自己我是怎么做到的?干杯:)


修改

我想我需要澄清一些事情。这是实际上不是我的数据库,它是一个我几乎无法控制的预先存在的系统,除了编写此触发器。

我的问题是,如果更改了主键,我该如何检索旧主键。我不需要被告知我不应该更改主键或追加外键等。这不是我的问题:))

8 个答案:

答案 0 :(得分:3)

DECLARE @OldKey int, @NewKey int;

SELECT @Oldkey = [ID] FROM DELETED;
SELECT @NewKey = [ID] FROM INSERTED;

仅当您有一行时才有效。否则,您没有“锚点”来链接新旧行。因此请检查您的触发器> INSERTED中的1。

答案 1 :(得分:2)

是否可以假设在触发器中呈现给您的INSERTED和DELETED表保证处于相同的顺序?

答案 2 :(得分:1)

我不认为这是可能的。想象一下,如果表中有4行:

1  Val1
2  Val2
3  Val3
4  Val4

现在发出以下更新:

UPDATE MainTable SET
ID = CASE ID WHEN 1 THEN 2 WHEN 2 THEN 1 ELSE ID END
Description = CASE ID WHEN 3 THEN 'Val4' WHEN 4 THEN 'Val3' ELSE Description END

现在,您如何区分第1行和第1行发生的情况? 2和第3行和第3行发生了什么更重要的是,你能描述一下它们之间有什么不同吗?所有告诉您哪些列已更新的内容都无济于事。

如果在这种情况下可以在表格上有一个额外的键(例如,描述是独一无二的),并且您的更新规则允许它,您可以编写触发器以防止同时更新两个键,然后您可以使用任何一个key尚未更新以关联两个表。

答案 3 :(得分:1)

如果你必须处理多行插入/更新,并且没有保证不会改变的备用密钥,我能看到的唯一方法是使用INSTEAD OF触发器。例如,在触发器中,您可以将原始插入/更新命令分成每行一个命令,在插入/更新之前获取每个旧ID。

答案 4 :(得分:0)

---- ----新 将一个标识列添加到应用程序无法更改的表中,然后您可以使用该新列将插入的内容连接到触发器中的已删除表:

ALTER TABLE YourTableName ADD
    PrivateID int NOT NULL IDENTITY (1, 1)
GO

---- ----老 不要更新/更改键值。你怎么能这样做并修复所有外键?

我不建议使用无法处理一组行的触发器。

如果您必须更改密钥,请使用正确的新密钥和值插入新行,如果您正在执行此操作,请使用SCOPE_IDENTITY()。删除旧行。记录它已更改为新行的密钥的旧行,您现在应该拥有该密钥。我希望您的日志中已更改的密钥没有外键...

答案 5 :(得分:0)

在SQL Server的触发器中,您可以访问两个表:已删除和已插入。这两个都已经提到过了。以下是它们如何运作,具体取决于触发器触发的操作:

插入操作

  • 已删除 - 未使用
  • inserted - 包含要添加到表中的新行

删除操作

  • 已删除 - 包含从表中删除的行
  • 已插入 - 未使用

更新操作

  • 已删除 - 包含在UPDATE操作之前存在的行
  • inserted - 包含UPDATE操作后存在的行

这些功能在各方面都像表一样。因此,完全可以使用基于行的操作,例如以下操作(操作仅存在于审计表中,DateChanged也是如此):

INSERT INTO MyAuditTable
(ID, FirstColumn, SecondColumn, ThirdColumn, Operation, DateChanged)
VALUES
SELECT ID, FirstColumn, SecondColumn, ThirdColumn, 'Update-Before', GETDATE()
FROM deleted
UNION ALL
SELECT ID, FirstColumn, SecondColumn, ThirdColumn, 'Update-After', GETDATE()
FROM inserted

答案 6 :(得分:0)

您可以在表MainTable上创建新的标识列(以exampleid为例),并使用此列关联已插入和已删除的表。 对于现有代码,此新列应该是透明的。

INSERT INTO LOG(OLDID, NEWID)
SELECT deleted.id AS OLDID, inserted.id AS NEWID
FROM inserted 
INNER JOIN deleted 
    ON inserted.correlationid = deleted.correlationid

请注意,您可以在日志表中插入重复的记录。

答案 7 :(得分:0)

当然,没有人应该更改桌面上的主键 - 但这正是触发器应该用于(部分),是为了防止人们做他们不应该做的事情。在Oracle或MySQL中编写一个触发器来拦截主键的更改并阻止它们,这是一项微不足道的任务,但在SQL Server中并不容易。

你当然希望能够做到的只是做这样的事情:

if exists
  (
  select *
    from inserted changed
           join deleted old
   where changed.rowID = old.rowID
     and changed.id != old.id
  )
... [roll it all back]

这就是为什么人们去谷歌搜索相当于ROWID的SQL Server。好吧,SQL Server没有它;所以你必须提出另一种方法。

一个快速但可悲的不是防爆的版本是编写一个而不是更新触发器,该触发器查看是否有任何插入的行在更新的表中找不到主键,反之亦然。这将捕获MOST,但不是全部错误:

if exists
  (
  select *
    from inserted lost
           left join updated match
             on match.id = lost.id
   where match.id is null
  union
  select *
    from deleted new
           left join inserted match
             on match.id = new.id
    where match.id is null
  )
  -- roll it all back

但是这仍然没有像......这样的更新。

update myTable
   set id = case
              when id = 1 then 2 
              when id = 2 then 1
              else id
              end

现在,我已经尝试过这样的假设:插入和删除的表的排序方式是同时通过插入和删除的表进行游标将为您提供正确匹配的行。而这个出现了。实际上,您将触发器转换为相当于Oracle中可用的for-each-row触发器并且在MySQL中是强制性的......但我认为大规模更新的性能会很差,因为这不是SQL Server的本机行为。此外,它取决于我无法在任何地方找到记录的假设,因此我不愿意依赖。但代码结构就像APPEARS在我的SQL Server 2008 R2安装上正常工作。本文末尾的脚本突出了快速但非爆炸性解决方案的行为以及第二个伪Oracle解决方案的行为。

如果有人能指出我的假设被记录并且由微软保证的地方,那我就是一个非常感激的人......

begin try
  drop table kpTest;
end try
begin catch
end catch
go

create table kpTest( id int primary key, name nvarchar(10) )
go

begin try
  drop trigger kpTest_ioU;
end try
begin catch
end catch
go

create trigger kpTest_ioU on kpTest
instead of update
as
begin
  if exists
    (
    select *
      from inserted lost
             left join deleted match
               on match.id = lost.id
     where match.id is null
    union
    select *
      from deleted new
             left join inserted match
               on match.id = new.id
      where match.id is null
    )
      raisError( 'Changed primary key', 16, 1 )
  else
    update kpTest
       set name = i.name
      from kpTest
             join inserted i
               on i.id = kpTest.id
    ;
end
go

insert into kpTest( id, name ) values( 0, 'zero' );
insert into kpTest( id, name ) values( 1, 'one' );
insert into kpTest( id, name ) values( 2, 'two' );
insert into kpTest( id, name ) values( 3, 'three' );

select * from kpTest;

/*
0   zero
1   one
2   two
3   three
*/

-- This throws an error, appropriately
update kpTest set id = 5, name = 'FIVE' where id = 1
go

select * from kpTest;

/*
0   zero
1   one
2   two
3   three
*/

-- This allows the change, inappropriately
update kpTest 
   set id = case   
              when id = 1 then 2
              when id = 2 then 1
              else id
              end
     , name = UPPER( name )
go

select * from kpTest

/*
0   ZERO
1   TWO   -- WRONG WRONG WRONG
2   ONE   -- WRONG WRONG WRONG
3   THREE
*/

-- Put it back
update kpTest 
   set id = case   
              when id = 1 then 2
              when id = 2 then 1
              else id
              end
     , name = LOWER( name )
go

select * from kpTest;

/*
0   zero
1   one
2   two
3   three
*/

drop trigger kpTest_ioU
go

create trigger kpTest_ioU on kpTest
instead of update
as
begin
  declare newIDs cursor for select id, name from inserted;
  declare oldIDs cursor for select id from deleted;
  declare @thisOldID int;
  declare @thisNewID int;
  declare @thisNewName nvarchar(10);
  declare @errorFound int;
  set @errorFound = 0;
  open newIDs;
  open oldIDs;
  fetch newIDs into @thisNewID, @thisNewName;
  fetch oldIDs into @thisOldID;
  while @@FETCH_STATUS = 0 and @errorFound = 0
    begin
      if @thisNewID != @thisOldID
        begin
          set @errorFound = 1;
          close newIDs;
          deallocate newIDs;
          close oldIDs;
          deallocate oldIDs;
          raisError( 'Primary key changed', 16, 1 );
        end
      else
        begin
          update kpTest
             set name = @thisNewName
           where id = @thisNewID
          ;
          fetch newIDs into @thisNewID, @thisNewName;
          fetch oldIDs into @thisOldID;
        end
    end;
  if @errorFound = 0
    begin
      close newIDs;
      deallocate newIDs;
      close oldIDs;
      deallocate oldIDs;
    end
end
go

-- Succeeds, appropriately
update kpTest
   set name = UPPER( name )
go

select * from kpTest;

/*
0   ZERO
1   ONE
2   TWO
3   THREE
*/

-- Succeeds, appropriately
update kpTest
   set name = LOWER( name )
go

select * from kpTest;

/*
0   zero
1   one
2   two
3   three
*/


-- Fails, appropriately
update kpTest 
   set id = case   
              when id = 1 then 2
              when id = 2 then 1
              else id
              end
go

select * from kpTest;

/*
0   zero
1   one
2   two
3   three
*/

-- Fails, appropriately
update kpTest 
   set id = id + 1
go

select * from kpTest;

/*
0   zero
1   one
2   two
3   three
*/

-- Succeeds, appropriately
update kpTest 
   set id = id, name = UPPER( name )
go

select * from kpTest;

/*
0   ZERO
1   ONE
2   TWO
3   THREE
*/

drop table kpTest
go