SQL Server 2012 trigger: Execute dynamic sql for e

2019-06-09 07:10发布

问题:

I have a trigger in a table which keeps all the changes (Insert, Update, Delete). When I insert only one row per time it works fine. But when I am trying to insert multiple rows at once I receive this error :

Subquery returned more than 1 value. This is not permitted when the subquery follows =, !=, <, <= , >, >= or when the subquery is used as an expression.

Here is the code of the trigger ( I removed some parts that are not needed to shorten the code like variable declarations etc.)

UPDATE: The actual error is in these lines when #tempTrigT contains more than one rows:

Select * into #tempTrigT from (select * from deleted where @Action in ( 'U','D')) A UNION (select * from inserted where @Action ='I')

    set @sql = 'set @audit_oldvalue=(select cast([' +@Item +'] as NVARCHAR(4000)) from #tempTrigT)';
    EXEC SP_EXECUTESQL @sql,N'@audit_oldvalue sql_variant OUTPUT',@audit_oldvalue OUTPUT -- If inserted @audit_oldvalue gets the new value

    set @sql = 'set @audit_value=(select cast(i.[' +@Item +'] as NVARCHAR(4000)) from dbo.TForms i  inner join #tempTrigT d on i.id = d.id)';
    EXEC SP_EXECUTESQL @sql,N'@audit_value sql_variant OUTPUT',@audit_value OUTPUT

How I can change it to work for multiple rows as well?

回答1:

You are missing a row identifier so that you are only handling one row per loop. Something like:

  • DECLARE @ID int = (SELECT MIN(id) FROM #tempTrigT) to define a row at the start of your loop
  • WHERE id = @ID to filter to this row throughout the loop
  • DELETE FROM #tempTrigT WHERE id = @ID at the end of your loop, when that id is done processing

Then again, that may not even work if the id can repeat in #tempTrigT.

And with all that said...

I would definitely consider separating this into multiple triggers and save yourself the complexity you are facing by trying to loop through deleted or inserted records and handle them all accordingly. I would also consider simplifying your audit process. The end goal is to be able to look back at what records used to be, which you can achieve really simply:

INSERT INTO [dbo].[AuditTrailTForms] (TForms_Cols, ChangeDate, Change_User, Change_Type)
    SELECT T.*, GETDATE(), COALESCE(ModifiedBy,suser_name()), 'Inserted'
    FROM inserted i
    JOIN TForms T on i.id = T.id

Then you can worry about making it easier to view which column values changed later on when you query these tables:

SELECT *
FROM (SELECT *, GETDATE(), 'Current', 'Current'
      FROM TForms
      WHERE ID = @AuditID
         UNION ALL
      SELECT *
      FROM AuditTrailTForms
      WHERE ID = @AuditID
      --AND Change_Type = 
      --AND Change_User = 
     ) T
ORDER BY ChangeDate DESC

Edit: Using an identity column:

You can use an identity column to define a row for each loop like so:

DECLARE @TotalRows int = (SELECT MAX(identityColumn) FROM #tempTrigT
DECLARE @RowID int = 1
WHILE @RowID <= @TotalRows
    BEGIN
        --Do stuff
        --For Example
            SET @sql = 'set @audit_oldvalue=(SELECT cast([' +@Item +'] as NVARCHAR(4000)) 
                                             FROM #tempTrigT 
                                             WHERE T.IdentityColumn = @RowID)';
            EXEC SP_EXECUTESQL @sql,N'@audit_oldvalue sql_variant OUTPUT',@audit_oldvalue OUTPUT

        --then increment to the next row when you're done
        SET @RowID = @RowID + 1
    END