SQL トリガーの古い主キーを特定する
-
03-07-2019 - |
質問
以前どこかでやったことがありますが、確かにそう思います!
SQL Server 2000 テーブルがあり、更新時にフィールドへの変更を記録し、2 番目のログ テーブルに挿入する必要があります。私が使用している構造の簡略版は以下のとおりです。
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 が変更されると明らかに結合は失敗します。
テーブルを変更することはできません。このデータベースで私ができる唯一の権限は、トリガーを作成することです。
あるいは、私にタイムトラベルを教えてくれる人はいますか?過去に戻って、どうやってこれをしたのか当時の自分に問いかけてみましょう。乾杯 :)
編集:
ここでいくつかのことを明確にする必要があると思います。これは 実際には私のデータベースではありません, 、これは既存のシステムであり、このトリガーを作成すること以外はほとんど制御できません。
私の質問は、主キーが変更された場合に古い主キーを取得するにはどうすればよいですかということです。主キーを変更すべきではないとか、外部キーを追いかける必要はないなどと言われる必要はありません。それは私の問題ではありません:)
解決
トリガーで表示されるINSERTEDテーブルとDELETEDテーブルが同じ順序であることが保証されていると仮定することは可能ですか?
他のヒント
DECLARE @OldKey int, @NewKey int;
SELECT @Oldkey = [ID] FROM DELETED;
SELECT @NewKey = [ID] FROM INSERTED;
これは、単一の行がある場合にのみ機能します。それ以外の場合、「アンカー」はありません。古い行と新しい行をリンクします。そのため、>のトリガーを確認してください。 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.さらに重要なこととして、それらの違いを説明できますか?どの列が更新されたかを示すものはすべて役に立ちません。
この場合、テーブルに追加のキーがあり(たとえば、説明は一意)、更新ルールで許可されている場合、両方のキーの同時更新を防ぐトリガーを記述できます。キーは、2つのテーブルを関連付けるために更新されていません。
複数行の挿入/更新を処理する必要があり、変更しないことが保証されている代替キーがない場合、これを確認できる唯一の方法は、INSTEAD OFトリガーを使用することです。たとえば、トリガーでは、元の挿入/更新コマンドを行ごとに1つのコマンドに分割し、挿入/更新する前に古いIDをそれぞれ取得できます。
SQL Serverのトリガー内では、削除と挿入の2つのテーブルにアクセスできます。これらは両方ともすでに言及されています。トリガーが実行されるアクションに応じて、それらがどのように機能するかを以下に示します。
操作を挿入
- 削除-使用されていません
- 挿入済み-テーブルに追加される新しい行が含まれています
操作の削除
- deleted-テーブルから削除される行が含まれています
- 挿入済み-使用されていません
更新操作
- deleted-UPDATE操作前に存在していた行を含む
- 挿入済み-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
---- new ---- アプリケーションが変更できないID列をテーブルに追加すると、その新しい列を使用して、トリガー内の削除されたテーブルに挿入されたテーブルを結合できます。
ALTER TABLE YourTableName ADD
PrivateID int NOT NULL IDENTITY (1, 1)
GO
----古い---- キー値を更新/変更しないでください。どうすればこれを行い、すべての外部キーを修正できますか?
行のセットを処理できないトリガーを使用することはお勧めしません。
キーを変更する必要がある場合は、適切な新しいキーと値で新しい行を挿入し、それがあなたのしていることであればSCOPE_IDENTITY()を使用します。古い行を削除します。古い行を新しい行のキーに変更したことをログに記録します。これは現在必要です。ログの変更されたキーに外部キーがないことを願っています...
テーブルMainTableに新しいID列(たとえば、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]
人々が 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 インストール環境では正しく動作するようです。この投稿の最後にあるスクリプトでは、高速ではあるが防爆ではないソリューションの動作と、2 番目の擬似 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