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