Вопрос

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

У меня есть таблица 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

...Но очевидно, что соединение завершилось бы неудачей, если бы идентификатор был изменен.

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

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


Редактировать:

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

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

Это было полезно?

Решение

Можно ли предположить, что ВСТАВЛЕННЫЕ и УДАЛЕННЫЕ таблицы, представленные вам в триггере, гарантированно будут находиться в одном и том же порядке?

Другие советы

DECLARE @OldKey int, @NewKey int;

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

Это работает только в том случае, если у вас есть одна строка.В противном случае у вас нет "якоря" для связывания старых и новых строк.Итак, проверьте в своем триггере, что ВСТАВЛЕНО > 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 и 2, от того, что произошло со строками 3 и 4.И что еще более важно, можете ли вы описать, в чем разница между ними?Все, что сообщает вам, какие столбцы были обновлены, вам не поможет.

Если в этом случае возможно, чтобы в таблице был дополнительный ключ (напримерОписание УНИКАЛЬНО), и ваши правила обновления позволяют это, вы могли бы написать триггер для предотвращения одновременного обновления обоих ключей, а затем вы можете использовать любой ключ, который не был обновлен, для сопоставления двух таблиц.

Если вам необходимо обрабатывать многорядные вставки / обновления, и нет альтернативного ключа, который гарантированно не изменится, единственный способ, который я вижу для этого, - использовать триггер ВМЕСТО.Например, в триггере вы могли бы разбить исходную команду insert / update на одну команду для каждой строки, захватывая каждый старый идентификатор перед вставкой / обновлением.

Внутри триггеров в 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

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

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

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

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

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

Вы можете создать новый столбец идентификатора в таблице 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

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

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

Быстрый, но, к сожалению, не защищенный от бомб вариант заключается в написании триггера instead вместо update, который проверяет, есть ли у какой-либо из вставленных строк первичный ключ, которого нет в обновленной таблице, или наоборот.Это позволило бы выявить БОЛЬШИНСТВО, но не все ошибки:

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

Теперь я попытался сделать предположение, что вставленные и удаленные таблицы упорядочены таким образом, что одновременный просмотр вставленных и удаленных таблиц даст вам правильно совпадающие строки.И это, похоже, работает.По сути, вы превращаете триггер в эквивалент триггеров for-each-row, доступных в 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
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top