web-dev-qa-db-ja.com

SQL Serverの「AFTER INSERT」トリガーに、挿入されたばかりの行が表示されない

このトリガーを検討してください。

ALTER TRIGGER myTrigger 
   ON someTable 
   AFTER INSERT
AS BEGIN
  DELETE FROM someTable
         WHERE ISNUMERIC(someField) = 1
END

テーブルsomeTableがあり、人々が悪いレコードを挿入しないようにしています。この質問のために、不良レコードには、すべて数値のフィールド「someField」があります。

もちろん、これを行う正しい方法はトリガーを使用することではありませんが、ソースコードを制御するのではなく、SQLデータベースだけを制御します。そのため、不良行の挿入を実際に防止することはできませんが、すぐに削除できます。これは私のニーズに十分対応できます。

トリガーは機能しますが、1つの問題が発生します。起動すると、挿入されたばかりの不良レコードは削除されないようです。古い不良レコードは削除されますが、挿入されたばかりの不良レコードは削除されません。そのため、他の誰かが来て別のINSERTを実行するまで削除されない1つの不良レコードが頻繁に存在します。

これはトリガーの理解に問題がありますか?トリガーの実行中に、新しく挿入された行はまだコミットされていませんか?

43
Joel Spolsky

トリガーは変更されたデータ(InsertedまたはDeleted)を変更できません。変更すると、変更がトリガーを再度呼び出したときに無限再帰を取得できます。 1つのオプションは、トリガーがトランザクションをロールバックすることです。

Edit:この理由は、SQLの標準では、挿入された行と削除された行はトリガーによって変更できないことです。その根本的な理由は、変更によって無限再帰が発生する可能性があるためです。一般的な場合、この評価には、相互に再帰的なカスケードでの複数のトリガーが含まれます。このような更新を許可するかどうかをシステムにインテリジェントに決定させることは、計算上手に負えません。本質的には halting problem。

これに対する受け入れられた解決策は、トランザクションをロールバックできますが、トリガーが変更データを変更することを許可しないことです。

create table Foo (
       FooID int
      ,SomeField varchar (10)
)
go

create trigger FooInsert
    on Foo after insert as
    begin
        delete inserted
         where isnumeric (SomeField) = 1
    end
go


Msg 286, Level 16, State 1, Procedure FooInsert, Line 5
The logical tables INSERTED and DELETED cannot be updated.

このような何かがトランザクションをロールバックします。

create table Foo (
       FooID int
      ,SomeField varchar (10)
)
go

create trigger FooInsert
    on Foo for insert as
    if exists (
       select 1
         from inserted 
        where isnumeric (SomeField) = 1) begin
              rollback transaction
    end
go

insert Foo values (1, '1')

Msg 3609, Level 16, State 1, Line 1
The transaction ended in the trigger. The batch has been aborted.

論理を逆にすることができます。無効な行を挿入した後に削除する代わりに、INSTEAD OF挿入するトリガーのみ行が有効であることを確認する場合。

CREATE TRIGGER mytrigger ON sometable
INSTEAD OF INSERT
AS BEGIN
  DECLARE @isnum TINYINT;

  SELECT @isnum = ISNUMERIC(somefield) FROM inserted;

  IF (@isnum = 1)
    INSERT INTO sometable SELECT * FROM inserted;
  ELSE
    RAISERROR('somefield must be numeric', 16, 1)
      WITH SETERROR;
END

アプリケーションがエラーを処理したくない場合(Joelが彼のアプリでそうであるように)、RAISERRORをしないでください。トリガーをサイレントに作成するだけですnot無効な挿入を行います。

これをSQL Server Express 2005で実行しましたが、動作します。ご了承ください INSTEAD OFトリガーしないトリガーが定義されている同じテーブルに挿入すると、再帰が発生します。

39
Bill Karwin

Billのコードを修正したバージョンを次に示します。

CREATE TRIGGER mytrigger ON sometable
INSTEAD OF INSERT
AS BEGIN
  INSERT INTO sometable SELECT * FROM inserted WHERE ISNUMERIC(somefield) = 1 FROM inserted;
  INSERT INTO sometableRejects SELECT * FROM inserted WHERE ISNUMERIC(somefield) = 0 FROM inserted;
END

これにより、挿入は常に成功し、偽のレコードはsometableRejectsにスローされ、後で処理できます。拒否テーブルで、int、tinyintなどではなく、すべてにnvarcharフィールドを使用することが重要です。なぜなら、それらが拒否された場合、それはデータが期待したものではないからです。

これにより、Billのトリガーが失敗する複数レコードの挿入の問題も解決されます。 10個のレコードを同時に挿入し(select-insert-intoを実行する場合など)、そのうちの1つだけが偽である場合、Billのトリガーはそれらすべてを不良としてフラグ付けします。これは、良いレコードと悪いレコードをいくつでも処理します。

私は、データウェアハウジングプロジェクトでこのトリックを使用しました。このプロジェクトでは、挿入するアプリケーションはビジネスロジックが適切かどうか判断できず、代わりにトリガーでビジネスロジックを実行しました。パフォーマンスは本当に厄介ですが、挿入を失敗させない場合は機能します。

28
Brent Ozar

CHECK制約を使用できると思います。まさにそれが発明されたものです。

ALTER TABLE someTable 
ADD CONSTRAINT someField_check CHECK (ISNUMERIC(someField) = 1) ;

私の以前の答え(少しだけ過剰かもしれません):

正しい方法は、INSTEAD OFトリガーを使用して、間違ったデータが挿入されるのを防ぐことです(事後的に削除するのではなく)

12
Dmitry Khalatov

更新:トリガーからのDELETEは、MSSql 7とMSSql 2008の両方で機能します。

私はリレーショナルの第一人者でも、SQL標準でもありません。ただし、受け入れられた答えに反して、MSSQLはr 再帰的およびネストされたトリガー評価 の両方をうまく処理します。他のRDBMSについては知りません。

関連するオプションは 'recursive triggers' and 'nested triggers' です。ネストされたトリガーは32レベルに制限されており、デフォルトは1です。再帰トリガーはデフォルトでオフになっており、制限についての話はありません-しかし、率直に言って、私はそれらをオンにしたことがないため、避けられないもので何が起こるかわかりませんスタックオーバーフロー。 MSSQLはあなたのspidを殺すだけだと思われます(または再帰的な制限があります)。

もちろん、受け入れられた答えが間違っている理由であることを示しているだけで、間違っているわけではありません。ただし、INSTEAD OFトリガーの前に、挿入したばかりの行を陽気に更新するON INSERTトリガーを作成したことを思い出します。これはすべて正常に機能し、予想通りでした。

挿入したばかりの行を削除する簡単なテストも機能します。

 CREATE TABLE Test ( Id int IDENTITY(1,1), Column1 varchar(10) )
 GO

 CREATE TRIGGER trTest ON Test 
 FOR INSERT 
 AS
    SET NOCOUNT ON
    DELETE FROM Test WHERE Column1 = 'ABCDEF'
 GO

 INSERT INTO Test (Column1) VALUES ('ABCDEF')
 --SCOPE_IDENTITY() should be the same, but doesn't exist in SQL 7
 PRINT @@IDENTITY --Will print 1. Run it again, and it'll print 2, 3, etc.
 GO

 SELECT * FROM Test --No rows
 GO

ここで何か他のことが起こっています。

7
Mark Brackett

CREATE TRIGGER ドキュメントから:

deletedおよびinsertedは論理(概念)テーブルです。これらは、トリガーが定義されているテーブル、つまりユーザーアクションが試行されるテーブルと構造的に類似しており、ユーザーアクションによって変更される可能性のある行の古い値または新しい値を保持します。たとえば、削除されたテーブルのすべての値を取得するには、SELECT * FROM deleted

少なくとも、新しいデータを見る方法を提供します。

しかし、通常のテーブルを照会するときに挿入されたデータが表示されないことを指定しているドキュメントには何も表示されません...

4
Jon Skeet

私はこの参照を見つけました:

create trigger myTrigger
on SomeTable
for insert 
as 
if (select count(*) 
    from SomeTable, inserted 
    where IsNumeric(SomeField) = 1) <> 0
/* Cancel the insert and print a message.*/
  begin
    rollback transaction 
    print "You can't do that!"  
  end  
/* Otherwise, allow it. */
else
  print "Added successfully."

私はそれをテストしていませんが、論理的には、挿入されたデータを削除するのではなく、挿入したデータを削除する必要があります。挿入を完全に防ぐため、挿入を元に戻す必要はありません。パフォーマンスが向上するため、最終的にはより高い負荷をより簡単に処理する必要があります。

編集:もちろん、そこにis挿入が他の有効なトランザクション内で発生した場合、woleトランザクションがロールバックされる可能性があるため、そのシナリオを考慮して挿入が必要かどうかを判断する必要があります無効なデータ行は、完全に無効なトランザクションを構成します...

3
BenAlabaster

MS-SQLには、再帰的なトリガーの起動を防ぐ設定があります。これは、ストアドプロシージャsp_configureを介して構成されます。このプロシージャでは、再帰トリガーまたはネストされたトリガーをオンまたはオフにできます。

この場合、再帰トリガーをオフにして、主キーを介して挿入されたテーブルからレコードをリンクし、レコードに変更を加えることができます。

質問の特定のケースでは、結果はこの特定のトリガーを再起動しないレコードを削除するため、実際には問題ではありませんが、一般的には有効なアプローチである可能性があります。この方法で楽観的同時実行を実装しました。

この方法で使用できるトリガーのコードは次のとおりです。

ALTER TRIGGER myTrigger
    ON someTable
    AFTER INSERT
AS BEGIN
DELETE FROM someTable
    INNER JOIN inserted on inserted.primarykey = someTable.primarykey
    WHERE ISNUMERIC(inserted.someField) = 1
END
0
Yishai

私はこの質問に出くわして、挿入ステートメントとトリガー中のイベントのシーケンスの詳細を探しました。最終的に、SQL 2016(EXPRESS)の動作を確認するための簡単なテストをいくつかコーディングしました。他の人が同様の情報を検索するのに役立つ可能性があるため、共有することが適切だと思いました。

私のテストに基づいて、「挿入」テーブルからデータを選択し、それを使用して挿入データ自体を更新することができます。そして、興味深いことに、挿入されたデータは、最終結果が表示される時点でトリガーが完了するまで、他のクエリには表示されません(少なくともテストできる限り)。再帰的なトリガーなどについてこれをテストしませんでした(ネストされたトリガーは、テーブルに挿入されたデータの完全な可視性を期待しますが、それは単なる推測です)。

たとえば、整数フィールド「field」と主キーフィールド「pk」を持つテーブル「table」と、挿入トリガーの次のコードがあると仮定します。

select @value=field,@pk=pk from inserted
update table set field=@value+1 where pk=@pk
waitfor delay '00:00:15'

「フィールド」に値1の行を挿入すると、行の値は2になります。さらに、SSMSで別のウィンドウを開いて次の操作を実行すると、select * from table where pk = @pk

@pkは最初に挿入したプライマリキーです。クエリは15秒が経過するまで空であり、更新された値(field = 2)が表示されます。

トリガーの実行中に他のクエリに表示されるデータに興味がありました(明らかに新しいデータはありません)。削除も追加してテストしました:

select @value=field,@pk=pk from inserted
update table set field=@value+1 where pk=@pk
delete from table where pk=@pk
waitfor delay '00:00:15'

繰り返しますが、挿入の実行には15秒かかりました。別のセッションで実行されているクエリは、挿入+トリガーの実行中または実行後に新しいデータを示しませんでした(データが挿入されていないように見えても、IDが増加することを期待します)。

0
mszil

「トリガー」は、「トリガー」が想定していないことを実行しています。 Sql Server Agentを簡単に実行できます

DELETE FROM someTable
WHERE ISNUMERIC(someField) = 1

1秒ごとに。プログラミング中にテーブルにエラーを挿入しないようにするために、ニースリトルSPを作成してください。SPの良い点の1つは、パラメーターがタイプセーフであることです。

0
LongChalk

INSERTは有効ですが、後で別のUPDATEが実行されますが、それは無効ですが、トリガーを起動しませんか?

0
SqlACID

上記で概説した手法は、オプションをかなりよく説明しています。しかし、ユーザーは何を見ていますか?あなたとソフトウェアの責任者との間のこのような基本的な対立が、ユーザーとの混乱や敵対関係に陥ることができないことは想像できません。

他の人はあなたが加えた変更を問題のエスカレーションとして簡単に見ることができるので、私は行き詰まりから他の方法を見つけるためにできる限りのことをします。

編集:

最初の「削除取り消し」を採点し、この質問が最初に現れたときに上記を投稿することを認めます。ジョエル・スポスキーからのものだと知ったとき、私はもちろんニワトリを出しました。しかし、どこか近くに着陸したようです。投票は必要ありませんが、記録に載せます。

IME、トリガーは、ビジネスルールの範囲外のきめ細かな整合性制約以外の正しい答えになることはめったにありません。

0
dkretz