Multiple Rows with in SQL INSTEAD of Trigger - sql

looking a bit of direction or guidance as to why I’m getting multiple rows using my trigger. Basically I have a web app that controls Asset Types (i.e Laptops, Phones etc), what I’m trying to do with this trigger is when the Asset Type Name (at_typedesc) changes that I log to an audit table (in this case sql_log) what the old name was and what the new name is.
This is working, but for some reason I get multiple lines written at the INSERT TO SQL_LOG statement. It does write the old name & new name, but then I’ll get 3 additional rows which has the old name showing the new name...
This is currently on a 2008 SQL Server.
-- create the trigger
go
create trigger trg_InsteadOfUpdate on [dbo].[lkp_asset_types]
instead of update
as
begin
DECLARE #triggerAction varchar(1)
-- determine the TRIGGER action
-- this allows us to tell if its an INSERT or an UPDATE
SELECT #triggerAction = CASE
WHEN EXISTS(SELECT 1 FROM INSERTED)
AND EXISTS(SELECT 1 FROM deleted) THEN 'U'
WHEN EXISTS(SELECT 1 FROM inserted) THEN 'I'
ELSE 'D' END;
-- get the orginally asset name from the DELETED table
-- this contains the rows as they were BEFORE the UPDATE Statement
DECLARE #orgAssetTypeName varchar(255)
SET #orgAssetTypeName = (SELECT top 1 at_typedesc from lkp_asset_types WHERE at_id = (select at_id from deleted))
-- UPDATE to the new asset name based on the NEW value in the INSERTED Table
update lkp_asset_types
set at_typedesc = (select at_typedesc from inserted)
where at_id = (select at_id from inserted)
-- get the new asset name from the INSERTED table
-- this contains the rows as they were AFTER the UPDATE Statement
DECLARE #newAssetTypeName varchar(255)
SET #newAssetTypeName = (SELECT top 1 at_typedesc from lkp_asset_types WHERE at_id = (select at_id from inserted))
insert into sql_log
(sql_log)
values ('SQL PRE Changed from : ' + #orgAssetTypeName + ' to: ' + #newAssetTypeName + '. Action = ' + #triggerAction)
end
go

Logic like this in a trigger in SQL Server is just broken:
where at_id = (select at_id from inserted)
I really wish the SQL Server parser issued a warning when encountering such constructs.
There is no guarantee that inserted has only one value (nor deleted).
That is how SQL Server defines triggers: as set operations. If multiple rows are inserted at the same, then the inserted and deleted "tables" have multiple rows.
That part is simple. You will need to rewrite the trigger to take this into account.

On checking my web code the update button was performing additional updates, this caused the trigger to fire more than once thus causing duplicate rows.

create table dbo.lkp_asset_types_test
(
at_id int identity,
at_typedesc varchar(100)
)
go
create trigger trg_InsteadOfUpdate_test on [dbo].[lkp_asset_types_test]
instead of update
as
begin
select 'trigger fired!!!!'
if not exists(select * from inserted)
and not exists(select * from deleted)
begin
return;
end
--update (maybe only the diffs?)
update t
set at_typedesc = i.at_typedesc
from dbo.lkp_asset_types_test as t
join inserted as i on t.at_id = i.at_id;
--where t.at_typedesc <> i.at_typedesc & nulls??
--insert into sql_log(sql_log)
select
'SQL PRE Changed from : ' + isnull(d.at_typedesc, '*null*') + ' to: ' + isnull(i.at_typedesc, '*null*') + '. Action = U'
from inserted as i
join deleted as d on i.at_id = d.at_id
--where i.at_typedesc <> d.at_typedesc & nulls ??
end
go
insert into dbo.lkp_asset_types_test(at_typedesc) values ('A'), ('B'), ('C'), ('D'), (NULL);
go
update dbo.lkp_asset_types_test
set at_typedesc = case when at_id%2=0 then isnull(at_typedesc, 'X') else isnull(at_typedesc, '') + 'xyz' end
go
select *
from dbo.lkp_asset_types_test;
go
update dbo.lkp_asset_types_test
set at_typedesc = case when at_id%2=0 then at_typedesc else at_typedesc + 'xyz' end
where 1=2
go
--
drop table lkp_asset_types_test;

Related

Update on a table using inner join in a trigger

I have two tables in the database, they have a one-to-many relationship (Example: The table dbo.Tree [tree_id, tree_code] has many dbo.Fruits[id, name, father_tree_id, father_tree_code]) .
I'm trying to create a trigger for when the column codigo_arvore_pai of dbo.Frutos is updated or inserted into it, the column father_tree_code of dbo.Frutos is updated with the value corresponding to the tree_id of the dbo.Tree table. The condition for this is code_tree of dbo.Tree to be equal code_tree_father of dbo.Fruits.
CREATE TRIGGER [dbo].[tr_updateFruit] on [dbo].[Fruits]
AFTER INSERT, UPDATE
AS
IF (UPDATE(father_tree_code))
BEGIN
UPDATE dbo.Fruits
SET id_arvore_pai = A.id_arvore
FROM dbo.Fruits as obj
INNER JOIN dbo.Tree A ON obj.father_tree_code = A.tree_code
WHERE obj.Id IN (SELECT DISTINCT obj.Id FROM dbo.Fruits)
END;
What's wrong?
When the command to update the SQL server is executed, in fact, first the record is deleted and then the new record is inserted again with new changes, giving the illusion to the user that the editing has been done on the desired fields. But in fact, the update command is a two-step command that consists of a deletion and an insertion. First, you must specify the type of action or command executed. You can use the following code inside of the trigger for this reason:
DECLARE #WhatActionHappened as char(1);
SET #WhatActionHappened = (CASE WHEN EXISTS(SELECT * FROM INSERTED)
AND EXISTS(SELECT * FROM DELETED)
THEN 'U' -- Set to Updated.
WHEN EXISTS(SELECT * FROM INSERTED)
THEN 'I' -- Set to Insert.
WHEN EXISTS(SELECT * FROM DELETED)
THEN 'D' -- Set to Deleted.
ELSE NULL -- Skip. It may have been a "failed delete".
END)
After that, you can use the trigger as follows to insert and edit.
CREATE TRIGGER [dbo].[tr_updateFruit] on [dbo].[Fruits]
AFTER INSERT, UPDATE
AS
BEGIN
DECLARE #WhatActionHappened as char(1);
SET #WhatActionHappened = (CASE WHEN EXISTS(SELECT * FROM INSERTED)
AND EXISTS(SELECT * FROM DELETED)
THEN 'U' -- Set to Updated.
WHEN EXISTS(SELECT * FROM INSERTED)
THEN 'I' -- Set to Insert.
--WHEN EXISTS(SELECT * FROM DELETED)
--THEN 'D' -- Set to Deleted.
ELSE NULL -- Skip. It may have been a "failed delete".
END)
IF(#WhatActionHappened = 'U' OR #WhatActionHappened = 'I')
BEGIN
UPDATE dbo.Fruits
SET id_arvore_pai = A.id_arvore
FROM dbo.Fruits as obj
INNER JOIN dbo.Tree A ON obj.father_tree_code = A.tree_code
WHERE obj.Id IN (SELECT DISTINCT i.Id FROM Inserted i)
END
END

Update Trigger For Multiple Rows

I am trying to Insert data in a table named "Candidate_Post_Info_Table_ChangeLogs" whenever a record is updated in another table named "Candidate_Personal_Info_Table". my code works fine whenever a single record is updated but when i try to updated multiple rows it gives error:
"Sub query returned more then 1 value".
Following is my code :
ALTER TRIGGER [dbo].[Candidate_PostInfo_UPDATE]
ON [dbo].[Candidate_Post_Info_Table]
AFTER UPDATE
AS
BEGIN
IF ##ROWCOUNT = 0
RETURN
DECLARE #Candidate_Post_ID int
DECLARE #Candidate_ID varchar(50)
DECLARE #Action VARCHAR(50)
DECLARE #OldValue VARCHAR(MAX)
DECLARE #NewValue VARCHAR(MAX)
DECLARE #Admin_id int
IF UPDATE(Verified)
BEGIN
SET #Action = 'Changed Verification Status'
SET #Candidate_Post_ID = (Select ID From inserted)
SET #Candidate_ID = (Select Identity_Number from inserted)
SET #NewValue = (Select Verified From inserted)
SET #OldValue = (Select Verified From deleted)
IF(#NewValue != #OldValue)
BEGIN
INSERT INTO Candidate_Post_Info_Table_ChangeLogs(Candidate_Post_ID, Candidate_ID, Change_DateTime, action, NewValue, OldValue, Admin_ID)
VALUES(#Candidate_Post_ID, #Candidate_ID, GETDATE(), #Action, #NewValue, #OldValue, '1')
END
END
END
i have searched stack overflow for this issue but couldn't get any related answer specific to this scenario.
When you insert/update multiple rows into a table, the Inserted temporary table used by the system holds all of the values from all of the rows that were inserted or updated.
Therefore, if you do an update to 6 rows, the Inserted table will also have 6 rows, and doing something like this:
SET #Candidate_Post_ID = (Select ID From inserted)
Will return an error, just the same as doing this:
SET #Candidate_Post_ID = (SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6)
From the looks of things, you tried to do this with an iterative approach. Set-based is better. Maybe consider doing it like this in the body of your TRIGGER (without all of the parameters...):
IF UPDATE(Verified)
BEGIN
INSERT INTO Candidate_Post_Info_Table_ChangeLogs
(
Candidate_Post_ID
,Candidate_ID
,Change_DateTime
,action
,NewValue
,OldValue
,Admin_ID
)
SELECT
I.ID
,I.Identity_Number
,GETDATE()
,'Changed Verification Status'
,I.Verified
,O.Verified
,'1'
FROM Inserted I
INNER JOIN Deleted O
ON I.ID = O.ID -- Check this condition to make sure it's a unique join per row
WHERE I.Verified <> O.Verified
END
A similar case was solved in the following thread using cursors.... please check it
SQL Server A trigger to work on multiple row inserts
Also the below thread gives the solution based on set based approach
SQL Server - Rewrite trigger to avoid cursor based approach
*Both the above threads are from stack overflow...

How to identify the operation type(insert,update,delete) in SQL Server trigger

We are using the following trigger in SQL Server to maintain the history now I need to identify the operations just like insert,update or delete. I found some information HERE but it doesn't works with the SQL Server.
CREATE TRIGGER audit_guest_details ON [PMS].[GSDTLTBL]
FOR INSERT,UPDATE,DELETE
AS
DECLARE #SRLNUB1 INT;
DECLARE #UPDFLG1 DECIMAL(3,0);
SELECT #SRLNUB1 = I.SRLNUB FROM inserted I;
SELECT #UPDFLG1 = I.UPDFLG FROM inserted I;
BEGIN
/* Here I need to identify the operation and insert the operation type in the GUEST_ADT 3rd field */
insert into dbo.GUEST_ADT values(#SRLNUB1,#UPDFLG1,?);
PRINT 'BEFORE INSERT trigger fired.'
END;
GO
But here I need to identify the operation and want to insert operation type accordingly.
Here I don't want to create three trigger for every operations
For Inserted : Rows are in inserted only.
For Updated: Rows are in inserted and deleted.
For Deleted: Rows are in deleted only.
DECLARE #event_type varchar(42)
IF EXISTS(SELECT * FROM inserted)
IF EXISTS(SELECT * FROM deleted)
SELECT #event_type = 'update'
ELSE
SELECT #event_type = 'insert'
ELSE
IF EXISTS(SELECT * FROM deleted)
SELECT #event_type = 'delete'
ELSE
--no rows affected - cannot determine event
SELECT #event_type = 'unknown'
This is a simplified version of Mikhail's answer that uses a searched CASE expression.
DECLARE #Operation varchar(7) =
CASE WHEN EXISTS(SELECT * FROM inserted) AND EXISTS(SELECT * FROM deleted)
THEN 'Update'
WHEN EXISTS(SELECT * FROM inserted)
THEN 'Insert'
WHEN EXISTS(SELECT * FROM deleted)
THEN 'Delete'
ELSE
NULL --Unknown
END;
Since you can get multiple rows at once we do it as follows.
INSERT INTO Log_Table
(
LogDate
,LogAction
-- your field list here
,Field0
-- Example : Tracking new and old value for a specific field
-- Make sure that the [Field1_Old] is nullable or has a default value
,Field1,Field1_Old
)
SELECT
LogDate=GETDATE()
,LogAction = CASE WHEN d.[PK_Field] IS NULL THEN 'I' ELSE 'U' END
,i.Field0
,i.Field1, d.Field1
FROM inserted i
LEFT JOIN deleted d on i.[PK_Field]=d.[PK_Field]
WHERE i.[PK_Field] IS NOT NULL
INSERT INTO Log_Table
(
LogDate
,LogAction
-- your field list here
,Field0
-- Example : Tracking new and old value for a specific field
-- Make sure that the [Field1_Old] is nullable or has a default value
,Field1,Field1_Old
)
SELECT
LogDate=GETDATE()
,LogAction = 'D'
,d.Field0
,d.Field1, NULL
FROM deleted d
LEFT JOIN inserted i on i.[PK_Field]=d.[PK_Field]
WHERE i.[PK_Field] IS NULL
create trigger my_trigger on my_table
after update , delete , insert
as
declare #inserting bit
declare #deleting bit
declare #updating bit = 0
select #inserting = coalesce (max(1),0) where exists (select 1 from inserted)
select #deleting = coalesce (max(1),0) where exists (select 1 from deleted )
select #inserting = 0
, #deleting = 0
, #updating = 1
where #inserting = 1 and #deleting = 1
print 'Inserting = ' + ltrim (#inserting)
+ ', Deleting = ' + ltrim (#deleting)
+ ', Updating = ' + ltrim (#updating)
If all three are zero, there are no rows affected and I think there is no way to tell whether it is an update/delete/insert.

How do you reference new columns immediately after they are created in your SQL script

How do I need to write my SQL script to ensure my new column is visible on following lines after it is created.
This is the general form of my SQL:
BEGIN TRANSACTION
if (not exists(select 1 from THIS_TABLE))
BEGIN
ALTER TABLE THIS_TABLE add THIS_COLUMN int
END
COMMIT
BEGIN TRANSACTION
IF (NOT EXISTS (SELECT 1 FROM THIS_TABLE
WHERE THIS_COLUMN = 1))
BEGIN
UPDATE THIS_TABLE SET THIS_COLUMN = 1
END
COMMIT
This is the error I'm getting:
Invalid column name 'THIS_COLUMN'.
on this line:
IF (NOT EXISTS (SELECT 1 FROM THIS_TABLE
WHERE THIS_COLUMN = 1))
The column has to be created before a query that uses it can be parsed. You can accomplish this by putting the update in a different batch, using the "go" keyword:
alter table t1 add c1 int
go
update t1 set c1 = 1
Or by running the second transaction as dynamic SQL:
alter table t1 add c1 int
exec ('update t1 set c1 = 1')
What Andomar said is correct, you need to use the go keyword.
However, the big problem is that your logic looks wrong. Let me go through each use case:
If THIS_TABLE is not empty
If the table is not empty, the if below returns false and you will never add the new column.
if (not exists(select 1 from THIS_TABLE))
BEGIN
ALTER TABLE THIS_TABLE add THIS_COLUMN int
END
Then, the next script obviously fails, because there is no such column THIS_COLUMN:
IF (NOT EXISTS (SELECT 1 FROM THIS_TABLE
WHERE THIS_COLUMN = 1))
If THIS_TABLE is empty
If the table is empty, the column is added:
if (not exists(select 1 from THIS_TABLE))
BEGIN
ALTER TABLE THIS_TABLE add THIS_COLUMN int
END
But then the next if will always be true and the update statement will affect zero rows (because table is empty).
IF (NOT EXISTS (SELECT 1 FROM THIS_TABLE
WHERE THIS_COLUMN = 1))
BEGIN
UPDATE THIS_TABLE SET THIS_COLUMN = 1
END

Update trigger insert Null

I am trying to create trigger on SQL Server 2008. I want that if i update field in tabele log that the new value update field in another table Doc. This is the code for trigger:
Create TRIGGER dbo.DocSt
ON dbo.log
AFTER UPDATE
IF (SELECT COUNT(*) FROM inserted) > 0
BEGIN
IF (SELECT COUNT(*) FROM deleted) > 0
BEGIN
UPDATE [dbo].[Doc]
SET
[ID_user] = (select ID_user from inserted)
WHERE
IDN= (select id_doc from inserted)
END
END
When I update field in table log triger update table Doc but it insert NULL.
What i am doing wrong? Thanks!
This code won't ever work - what happens in your UPDATE statement updates 10 rows?? What does this select give you:
SET [ID_user] = (select ID_user from inserted)
You're trying to set a single value to a whole return set from a SELECT statement - that won't work, obviously.
You need to create an UPDATE statement that joins with the Inserted pseudo-table:
CREATE TRIGGER dbo.DocSt
ON dbo.log AFTER UPDATE
UPDATE [dbo].[Doc]
FROM Inserted i
SET [ID_user] = i.ID_User
WHERE IDN = i.id_doc
That way, for each entry in Inserted, you're joining your table dbo.Doc to it and update the ID_user column.