web-dev-qa-db-ja.com

条件付きの更新トリガー

ユーザーテーブルの特定のユーザーに対して更新をトリガーしようとしています。基本的に誰かがそのユーザーを変更しようとする場合、実行されたクエリをロールバックしてユーザーを無効にします(SPを使用)。ただし、そのユーザーでない場合は、更新コミットを続行します。

これは私が作成したコードです:

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER TRIGGER [dbo].[Users_Trigger_UPDATE]  
ON [dbo].[Users]  
AFTER UPDATE  
AS 
BEGIN
SET NOCOUNT ON;
DECLARE @user_id as nvarchar(30);
DECLARE @action as nvarchar(30) = 'UPDATE';
DECLARE @source as nvarchar(30) = (SELECT client_net_address FROM 
sys.dm_exec_connections WHERE session_id = @@SPID);
    SELECT
      @user_id = inserted.UserID
    FROM
      inserted
   IF @user_id = 'Admin'
    BEGIN
        ROLLBACK TRAN;
        EXEC [dbo].[DisableUser] @user_id, @action, @source;
        PRINT 'Admin User Triggered';
    END
ELSE
    BEGIN
        PRINT 'Other User';
        COMMIT;
    END
END

しかし、「トランザクションがトリガーで終了しました。バッチは中止されました。」というエラーが表示されます。

TRY...CATCHを使用しようとしましたが、エラーは同じです。ネストされたトランザクションを作成しなかったので、ROLLBACKまたはCOMMITを使用すると、UPDATEをトリガーした元のクエリに適用されると想定しています。

INSTEAD OFトリガーを試しましたが、結果は同じです。

トリガーの結果は正しいです。DB側で必要なことを行っています。問題は、このテーブルを使用しているソフトウェアにTry...Catchがあり、トリガーがエラーを返すため、結果が正しい、最後のトリガーにエラーがあるため、ソフトウェアはログインエラーを出します。

2
Michael R.

ここで試みられているアプローチ(つまり、トリガー内でトランザクションを操作する)をnot主張していますが、少なくともトリガーがどのように機能するかを明確に理解できるように、何が起こっているのかを説明したいと思いました。

トリガーの基本的な動作は次のとおりです。

  1. トリガーが実行されるとき、それは常にトランザクション内にあります。トランザクションがDMLステートメントの実行時にまだアクティブになっていない場合、個々のDMLステートメントは、システムが開始したトランザクション(自動コミットと呼ばれる)内で実行されます。意味:
    • DMLステートメントの前に@@TRANCOUNT == 0の場合、トリガーで@@TRANCOUNTは1になります
    • DMLステートメントの前に@@TRANCOUNT> 0の場合、@@TRANCOUNTはトリガー内の同じ値になります
  2. @@TRANCOUNTを実行の前後で同じにする必要があるストアドプロシージャとは異なり、トリガーでは、実行後に@@TRANCOUNTを高くしたり低くしたりできますが、1つの例外があります(次の項目を参照)。
  3. トリガーが@@TRANCOUNTが0で終了する場合(トリガーが終了する前)、
    1. トランザクションがトリガーで終了し、バッチが中止されていることを示すエラーが表示されます
    2. 中止されたバッチは、トランザクションを終了したものを元に戻す/元に戻すことはできません。
      • トランザクションがコミットされた場合でも、データはコミットされます。
      • トランザクションがロールバックされた場合、それらの変更は失われます。
  4. COMMITまたはROLLBACKによってトランザクションが終了すると、insertedおよびdeletedテーブルには行がなくなります。トランザクションの終了後にそれらから何かが必要な場合は、変数または一時テーブル(またはテーブル変数)にキャプチャする必要がありますbeforeトランザクションが終了します。
  5. トリガーは暗黙のXACT_ABORT ONで実行されます。 TRY...CATCH構文がある場合でも、これにより状況が複雑になる可能性があります。場合によっては、XACT_ABORT OFFを明示的に設定する必要があります。

とはいえ、トリガー内のトランザクションは確かにcanCOMMIT/ROLLBACKです。簡単に言うと、これらのアクションのいずれかを実行する場合は、トリガーが終了する前にBEGIN TRAN;を発行するだけで、バッチの中止に関するエラーが発生しません。これは、ROLLBACKの場合、ROLLBACKが操作全体をキャンセルできるようにするのではなく、これが発生したという事実をマスクすることを想定しています。これは、トリガー内で通常使用される方法です。

ただし、現実はそれほど単純ではありません。

  1. トリガーが複数ある場合、トリガーの実行順序によって異なる動作が発生する可能性があります。トリガーが3つ以下であると想定すると、「最初」と「最後」のトリガーを設定することで順序を制御できます。 3つを超えるトリガーは、すべてのトリガーの順序を制御できない可能性があります。
  2. ネストされたトリガーがある場合、トリガーでトランザクションを終了すると、予期しない動作が発生する可能性があります。
  3. トリガーが実行される前にアクティブなトランザクションがある場合、トリガーでトランザクションを終了すると、トリガーの後に発生していることから制御が奪われます。

トリガーの起動前にすでにアクティブなトランザクションが存在する可能性を考慮するには、先頭で@@TRANCOUNTをキャプチャしてから、正しい数のBEGIN TRAN;、および場合によってはCOMMIT TRAN;ステートメントを発行する必要があります。

DECLARE @TranCount INT = @@TRANCOUNT,
        @Index INT = 0;

...

-- For an actual COMMIT, do the following:
WHILE (@Index < @TranCount)
BEGIN;
   COMMIT TRAN;
   SET @Index += 1;
END;

....

-- If either COMMIT or ROLLBACK occurs, do the following:
SET @Index = 0; -- reset in case COMMIT was processed above
WHILE (@Index < @TranCount)
BEGIN;
   BEGIN TRAN;
   SET @Index += 1;
END;

繰り返しますが、thisはnot推奨ですこれは、既存のトランザクションの性質を変更して、予期しない、直感的でない動作をさせるためです。フローは次のようになります。

  1. トランザクションの開始[〜#〜] a [〜#〜]
  2. 何かをする
  3. この表に挿入
    1. トランザクション[〜#〜] a [〜#〜]mightコミットまたはロールバックされます。この場合、
      1. 上記の「何かを行う」として実行されたアクションは、コミットまたはロールバックされます。
      2. トランザクション[〜#〜] b [〜#〜]が開始されます
    2. トランザクション[〜#〜] a [〜#〜]mightそのままにしておくと、トランザクションがない[〜#〜] b [〜#〜]
  4. トリガーが存在した後
    1. トランザクション[〜#〜] a [〜#〜]がコミットまたはロールバックされた場合、そのアクションを変更することはできません。トランザクション[〜#〜] b [〜#〜]が存在するようになり、この時点で行われた変更は、トランザクション[~~~~ aで行われた変更から完全に切り離されます。 〜#〜]
    2. トランザクション[〜#〜] a [〜#〜]がアクティブのままだった場合:
      1. 以前に実行されたアクションは、COMMITまたはROLLBACKで引き続き使用できます
      2. トリガーの終了後に実行されるアクションは、トリガーがアトミックユニットとして起動する前に実行されるアクションと一緒にグループ化されます(これが通常の動作であり、したがって予期される動作です)。

これらすべての最も明確なリファレンスは、Erland Sommarskogによる SQL Serverでのエラーおよびトランザクション処理 です。

3
Solomon Rutzky
CREATE TABLE USERS(ID INT, NAME VARCHAR(10), ROLE VARCHAR(10), ENABLED INT);
INSERT INTO USERS VALUES
(1, 'USER1', 'ADMIN', 1),
(2, 'USER2', 'USER', 1);
GO
 2行が影響を受けました
CREATE TRIGGER TRG_INS_OF_UPD ON USERS
INSTEAD OF UPDATE
AS
BEGIN

    IF EXISTS (SELECT 1
               FROM   inserted
               WHERE  ROLE = 'ADMIN')
    BEGIN
        --ROLLBACK TRANSACTION;
        UPDATE USERS
        SET    ENABLED=0
        WHERE  ID IN (SELECT ID FROM inserted WHERE ROLE = 'ADMIN');
    END

    -- commit other updates
    UPDATE u
    SET    ID = i.ID,
           NAME = i.NAME,
           ROLE = i.ROLE,
           ENABLED = i.ENABLED
    FROM   USERS u
    JOIN   inserted i
    ON     i.ID = u.ID
    WHERE  i.ROLE <> 'ADMIN';

END
GO
UPDATE USERS SET NAME='ME';
GO
 4行が影響を受けました
SELECT * FROM USERS;
GO
 ID | NAME |ロール|有効
-:| :- :---- | ------:
 1 | USER1 |管理| 0 
 2 |私|ユーザー| 1 

dbfiddle ---(ここ

2
McNets

したがって、COMMITステートメントのELSEは役に立たなかったので、ロールバックしないと、AFTER UPDATEを使用したので、Updateが実行されます。

COMMITを削除すると、通常の状況ではトリガーはエラーを発生させません。

0
Michael R.