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?
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