Определите старый первичный ключ в триггере SQL - PullRequest
5 голосов
/ 04 марта 2009

Я уже делал это где-то, я в этом уверен!

У меня есть таблица SQL Server 2000, которая необходима для регистрации изменений полей при обновлении и вставки во вторую таблицу ведения журнала. Упрощенная версия структуры, которую я использую, приведена ниже:

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 был изменен.

Я не могу изменить таблицы таким образом, единственное, что у меня есть в этой базе данных, - это создать триггер.

В качестве альтернативы есть кто-то, кто может научить меня путешествовать во времени, и я вернусь в прошлое и задам себе вопрос, как я это сделал? Приветствия:)


Edit:

Я думаю, мне нужно кое-что прояснить здесь. Это на самом деле не моя база данных , это уже существующая система, которой я почти не управляю, кроме написания этого триггера.

У меня вопрос, как мне получить старый первичный ключ, если указанный первичный ключ был изменен. Мне не нужно говорить, что я не должен менять первичный ключ или преследовать внешние ключи и т. Д. Это не моя проблема:)

Ответы [ 8 ]

3 голосов
/ 04 марта 2009
DECLARE @OldKey int, @NewKey int;

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

Это работает, только если у вас есть одна строка. В противном случае у вас нет «якоря» для связи старых и новых строк. Так что проверьте в своем триггере значение> 1 во INSERTED.

2 голосов
/ 25 июня 2009

Можно ли предположить, что таблицы INSERTED и DELETED, представленные вам в триггере, гарантированно будут в том же порядке?

1 голос
/ 05 марта 2009

Если вам нужно обрабатывать вставки / обновления в несколько строк, и нет альтернативного ключа, который гарантированно не изменится, единственный способ увидеть это - использовать триггер INSTEAD OF. Например, в триггере вы можете разбить исходную команду вставки / обновления на одну команду на строку, захватывая каждый старый идентификатор перед вставкой / обновлением.

1 голос
/ 04 марта 2009

Я не думаю, что это возможно. Представьте, что у вас есть 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 и 2 и что случилось со строками 3 и 4. И что более важно, вы можете описать, что между ними отличается? Все, что говорит вам, какие столбцы были обновлены, вам не поможет.

Если в этом случае возможно наличие дополнительного ключа на таблице (например, Description is UNIQUE), и ваши правила обновления позволяют это сделать, вы можете написать триггер, чтобы предотвратить одновременное обновление обоих ключей, а затем использовать любой Ключ не был обновлен для сопоставления двух таблиц.

0 голосов
/ 12 сентября 2013

Конечно, никто не должен менять первичный ключ на столе - но это именно то, для чего должны (частично) использоваться триггеры, чтобы люди не могли делать то, что им не следует делать. В 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]

Вот почему люди ищут Google, эквивалентный ROWID для SQL Server. Ну, SQL Server не имеет его; поэтому вам нужно придумать другой подход.

Быстрая, но, к сожалению, не защищенная от бомб, версия должна написать триггер вместо обновления, который проверяет, есть ли у какой-либо из вставленных строк первичный ключ, которого нет в обновленной таблице, или наоборот. Это отразит большинство, но не все ошибки:

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 и обязательных в MySQL ... но я бы предположил, что производительность при массовых обновлениях будет плохой, поскольку это не является собственным поведением SQL Server. Также это зависит от предположения, что я не могу найти документально нигде и поэтому не хочу зависеть. Но код, структурированный таким образом, кажется, работает правильно на моей установке SQL Server 2008 R2. Сценарий в конце этого поста освещает как поведение быстрого, но не защищенного от бомб решения, так и поведение второго, псевдо Oracle-решения.

Если бы кто-нибудь мог указать мне куда-нибудь, где мое предположение задокументировано и гарантировано Microsoft, я был бы очень благодарным парнем ...

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
0 голосов
/ 17 ноября 2009

Вы можете создать новый столбец идентификации в таблице MainTable (названный, например, correlationid) и сопоставить вставленные и удаленные таблицы, используя этот столбец. Этот новый столбец должен быть прозрачным для существующего кода.

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

Обратите внимание, вы можете вставить повторяющиеся записи в таблицу журнала.

0 голосов
/ 05 марта 2009

Внутри триггеров в SQL Server у вас есть доступ к двум таблицам: удаленным и вставленным. Оба из них уже были упомянуты. Вот как они функционируют в зависимости от того, какое действие вызывает триггер:

РАБОТА С ВСТАВКОЙ

  • удалено - не используется
  • вставлено - содержит новые строки, добавляемые в таблицу

УДАЛЕНИЕ ОПЕРАЦИИ

  • удалено - содержит строки, удаляемые из таблицы
  • вставлено - не используется

ОБНОВЛЕНИЕ ОПЕРАЦИИ

  • удалено - содержит строки в том виде, в котором они существовали до операции ОБНОВЛЕНИЕ
  • вставлено - содержит строки в том виде, в котором они будут существовать после операции ОБНОВЛЕНИЕ

Эти функции во всех отношениях похожи на таблицы. Поэтому вполне возможно использовать основанную на строке операцию, например что-то вроде следующего (Операция существует только в таблице аудита, как и 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
0 голосов
/ 04 марта 2009

---- ---- новый добавьте столбец идентификаторов в таблицу, которую приложение не может изменить, вы можете использовать этот новый столбец для присоединения вставленных к удаленным таблицам в триггере:

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

---- старый ---- Никогда не обновляйте / не меняйте значения ключей. Как вы можете это сделать и исправить все ваши внешние ключи?

Я бы не рекомендовал использовать триггер, который не может обрабатывать набор строк.

Если вам необходимо изменить ключ, вставьте новую строку с соответствующим новым ключом и значениями, используйте SCOPE_IDENTITY (), если это то, что вы делаете. Удалить старый ряд. Войдите для старой строки, чтобы она была заменена ключом новой строки, который вы должны теперь иметь. Я надеюсь, что в измененном ключе в вашем журнале нет внешнего ключа ...

...