SQL Server trigger to update row and save old version - sql-server

I setup the below trigger to save the old version of a modified row into an "Archive" table. It works fine as long as the commented part stays commented. Once I uncomment the UPDATE part, the trigger saves the row as it is AFTER the update, instead of BEFORE the update. What should I change ?
ALTER trigger [dbo].[TRG_AFTER_UPDATE_Test]
ON [dbo].[Test]
AFTER UPDATE
AS
BEGIN
INSERT INTO dbo.TestArchive(
[CustId]
,[CustName]
,[SomeValue]
,[Modified]
,[ModifiedBy]
)
SELECT s.[CustId]
,s.[CustName]
,s.[SomeValue]
,s.[Modified]
,s.[ModifiedBy]
FROM dbo.Test s
INNER JOIN deleted d ON s.CustId = d.CustId
--here is the part that makes the INSERT behave badly
/*
UPDATE dbo.Test
SET ModifiedBy = USER_NAME(),
Modified = GETDATE()
FROM dbo.Test s
INNER JOIN Inserted i ON s.CustId = i.CustId
*/
END

It looks like you have an infinite loop going on here with the update...
Your trigger is set to the update event of dbo.Test, and within that trigger you are updating the dbo.Test table on which the trigger is set, which in turn fires the update trigger ad infinitum....
I am not sure if it would work by disabling the trigger within itself before performing the update and then re-enabling it afterwards.
https://msdn.microsoft.com/en-us/library/ms189748.aspx

Try inserting the data from the deleted table and not the actual table. Change the columns in your select from s.[column name] to d.[column name].

Related

Fire Trigger if deleted.Column <> inserted.Column

This is similar to Compare deleted and inserted table in SQL Server 2008 but the results were not what I was looking for.
I do want the trigger to fire if a specific column changes, however the program we have does a delete of all information and an reinsert of all new information every time it is saved. there is no simple update.
I want to write to an audit table, but ONLY if that specific column has changed once a save has occurred.
ALTER TRIGGER [dbo].[deleted]
ON [dbo].[DispTech]
FOR DELETE, INSERT
AS
SET NOCOUNT ON;
IF (SELECT serviceman FROM deleted) <> (SELECT ServiceMan FROM inserted)
BEGIN
INSERT INTO misc.dbo.DeletedTest ("Status", dispatch, serviceman)
SELECT
'Deleted', d.Dispatch, d.ServiceMan
FROM
deleted d
INSERT INTO misc.dbo.DeletedTest ("Status", dispatch, serviceman)
SELECT
'Inserted', i.Dispatch, i.ServiceMan
FROM
inserted i
END
This does NOT work as it results back NULL for everything. I know I could sort it all out in the audit table if I dump everything in there each time, but I really want a cleaner set of data and want to use it for other processing.

T-SQL Trigger INSERT UPDATE

I'm fairly new in T-SQL but i have a question concerning triggers. I have written a trigger that is used in my stored procedure witch inserts and updates data.
Now the trigger works when any row is updated in table [dbo].[users]
I don't know how to modify this trigger so that if data is inserted into [dbo].[users] table activate the trigger (this works now), but how to acheive an update scenario on the same trigger that if only a specific row is UPDATED in [dbo].[users] table that only then the trigger should be activated.
For example
If a new user is inserted and all rows are inserted in this table - activate trigger
If an old trigger is updated but only a specific field in this table is updated (working_state is the name of a column) then only should the trigger should be activated.
Source code what I have is shown here:
ALTER TRIGGER [dbo].[t_temp_triger_name]
ON [dbo].[users]
AFTER INSERT, UPDATE, DELETE
AS
BEGIN
DECLARE #working_state char(1),
#user_code char(11)
DECLARE zm CURSOR FOR
SELECT
working_state,
user_code
FROM
inserted
-- Added update statement that activates the trigger only when a specific -
-- column update is executed
UPDATE [dbo].[users]
SET working_state = 1
FROM [dbo].[users] U
INNER JOIN DELETED D ON U.user_code= D.user_code
WHERE U.working_state<> D.working_state
OPEN zm
FETCH NEXT FROM zm INTO #working_state, #user_code
WHILE ##FETCH_STATUS = 0
BEGIN
EXEC [FNF].[dbo].[NamedProcedure]
#working_state, #user_code
FETCH NEXT FROM zm INTO #working_state, #user_code
END
CLOSE zm
DEALLOCATE zm
END
I have came up with a solution
Added this to my code after declaration
Everything works fine.
IF EXISTS (SELECT 1 FROM deleted)
BEGIN
DECLARE zm CURSOR FOR
SELECT
u.working_state,
u.user_code
FROM
inserted U
LEFT JOIN DELETED D ON U.user_code= D.user_code
WHERE U.working_state<> D.working_state
END
ELSE
BEGIN
DECLARE zm CURSOR FOR
SELECT
working_state,
user_code
FROM
inserted
END

trigger updates entire table even on single-row update

The following triggers are meant to automate updates when a row is updated in a locations table. Changes can occur one row at a time, or 1-n many rows at a time. However, when updating a single row, while the "locations_geteditdate" is enabled, a new time stamp is written to all 28K rows in the locations table. I know I'm missing something obvious here, thanks for the help.
ALTER TRIGGER [dbo].[locations_geteditdate]
ON [dbo].[TBL_LOCATIONS]
instead of update
AS
begin
declare #recs INT
select #recs = COUNT(*)
from dbo.TBL_LOCATIONS a
join inserted i on i.Location_ID = a.Location_ID
if #recs > 0
update dbo.TBL_LOCATIONS
SET EditDate = GETDATE()
end
GO
alter TRIGGER [dbo].[locations_move_topo]
ON [dbo].[TBL_LOCATIONS]
for update
AS
BEGIN
update dbo.TBL_LOCATIONS
set topo_name = dbo.TLU_TOPO_BOUNDS.name
FROM dbo.TBL_LOCATIONS
inner join dbo.TLU_TOPO_BOUNDS
on dbo.TBL_LOCATIONS.Location_ID = dbo.TBL_LOCATIONS.Location_ID
where (TLU_TOPO_BOUNDS.Shape.STContains(TBL_LOCATIONS.SHAPE) = 1) ;
END
Accepted answer:
alter TRIGGER [dbo].[locations_geteditdate]
ON [dbo].[TBL_LOCATIONS]
for update
as
begin
update dbo.TBL_LOCATIONS
SET EditDate = GETDATE()
from dbo.TBL_LOCATIONS locn
inner join inserted i on i.location_id = locn.Location_ID
end
GO
In your if condition (in locations_geteditdate) you have no where clause; therefore it is including all records:
if #recs > 0
update dbo.TBL_LOCATIONS
SET EditDate = GETDATE()
WHERE ???
end
You correctly used the inserted table to see what had been updated but only to identify a record count
So reading the code you've put in the trigger, it looks like what you're trying to do is just apply a timestamp to the table to show that when it has been updated.
You have at least these options for this:
1. If you don't actually need a recognisable datetime in there you can use a timestamp field instead of a datetime and get it automatically updated.
2. If you can control where updates are performed to the table you can just set EditDate there (i.e. in stored procedures)
However, assuming that you want a recognisable datetime and you can't control where updates to the table are happening which is why you're implementing a trigger rather than just have a proc set EditDate, you need to go forward with one of the two types of trigger:
A) So if you persist with an "instead of" trigger you need to understand that it replaces the update that would have happened. So its incumbent upon you to then do the work that it was going to. You check column by column what has changed:
e.g.
IF UPDATE (price)
BEGIN
UPDATE t
SET price = i.price
FROM TBL_LOCATIONS t join inserted i
ON i.locn_id = t.locn_id
END
... repeat for each column (you can merge the updates if it makes sense)
B) Alternatively you can change to an "after" trigger, allow the update to happen (so you don't have to code column by column to check what's been updated) BUT YOU MUST then have a check on the EditDate column and NOT perform an update if its the EditDate column that has changed. If you don't do this you'll be in an infinite loop - your proc calls the trigger which calls the trigger etc
i.e. something like:
IF NOT UPDATE(EditDate)
BEGIN
UPDATE dbo.TBL_LOCATIONS
SET EditDate = GETDATE()
FROM dbo.TBL_LOCATIONS locn
INNER JOIN inserted i on i.locn_id = locn.locn_id
END

How to prevent updates to a table, with an exception for one situation

I have a table that contains records that can become part of a bill. I can tell which ones are already part of a bill because the table has a BillId column that gets updated by the application code when that happens. I want to prevent updates to any record that has a non-null BillId. I'm thinking that the following should take care of that:
CREATE TRIGGER [Item_Update_AnyBilled]
ON [dbo].[Item]
FOR UPDATE
AS
BEGIN
SET NOCOUNT ON;
DECLARE #AnyBilled BIT;
SELECT TOP(1) #AnyBilled = 1
FROM inserted i
JOIN deleted d ON i.ItemId = d.ItemId
WHERE d.BillId IS NOT NULL;
IF COALESCE(#AnyBilled, 0) = 1 BEGIN
RAISERROR(2870486, 16, 1); -- Cannot update a record that is part of a bill.
ROLLBACK TRANSACTION;
END;
END;
However, there is one more wrinkle. The Item table also has a DATETIME Modified column, and a trigger that updates it.
CREATE TRIGGER [dbo].Item_Update_Modified
ON [dbo].[Item]
AFTER UPDATE
AS
BEGIN
SET NOCOUNT ON;
UPDATE a
SET Modified = getdate()
FROM Item a JOIN inserted i ON i.ItemId = a.ItemId
END
With these triggers in place, adding an Item to a Bill always causes the RAISERROR to fire. Presumably because when the BillId is populated, Item_Update_AnyBilled lets it through because the deleted.BillId is NULL, but the Item_Update_Modified then gets executed, and that secondary change causes Item_Update_AnyBilled to get executed again, and this time deleted.BillId is no longer NULL.
How can I prevent updates to the Item table except in the case where the BillId is being populated or when the only change is to the Modified column?
I'd prefer a solution that didn't require me to compare the inserted and deleted values of every column (or use COLUMNS_UPDATED()) as that would create a maintenance issue (someone would have to remember to update the trigger any time a new column is added to or deleted from the table). I am using SQL Server 2005.
Why not use an INSTEAD OF trigger? It requires a bit more work (namely a repeated UPDATE statement) but any time you can prevent work, instead of letting it happen and then rolling it back, you're going to be better off.
CREATE TRIGGER [dbo].[Item_BeforeUpdate_AnyBilled]
ON [dbo].[Item]
INSTEAD OF UPDATE
AS
BEGIN
SET NOCOUNT ON;
IF EXISTS
(
SELECT 1 FROM inserted i
JOIN deleted AS d ON i.ItemId = d.ItemId
WHERE d.BillId IS NULL -- it was NULL before, may not be NULL now
)
BEGIN
UPDATE src
SET col1 = i.col1 --, ... other columns
ModifiedDate = CURRENT_TIMESTAMP -- this eliminates need for other trigger
FROM dbo.Item AS src
INNER JOIN inserted AS i
ON i.ItemId = src.ItemId
AND (criteria to determine if at least one column has changed);
END
ELSE
BEGIN
RAISERROR(...);
END
END
GO
This doesn't fit perfectly. The criteria I've left out is left out for a reason: it can be complex to determine if a column value has changed, as it depends on the datatype, whether the column can be NULL, etc. AFAIK the built-in trigger functions can only tell if a certain column was specified, not whether the value actually changed from before.
EDIT considering that you're only concerned about the other columns that are updated due to the after trigger, I think the following INSTEAD OF trigger can replace both of your existing triggers and also deal with multiple rows updated at once (some without meeting your criteria):
CREATE TRIGGER [dbo].[Item_BeforeUpdate_AnyBilled]
ON [dbo].[Item]
INSTEAD OF UPDATE
AS
BEGIN
SET NOCOUNT ON;
UPDATE src SET col1 = i.col1 --, ... other columns,
ModifiedDate = CURRENT_TIMESTAMP
FROM dbo.Item AS src
INNER JOIN inserted AS i
ON src.ItemID = i.ItemID
INNER JOIN deleted AS d
ON i.ItemID = d.ItemID
WHERE d.BillID IS NULL;
IF ##ROWCOUNT = 0
BEGIN
RAISERROR(...);
END
END
GO

trigger not updating sql

Although this is completed successfully on completion, it is not having the desired update.
CREATE TRIGGER Trigger1
On dbo.[table1]
FOR UPDATE
AS
Declare #Id int;
SELECT #Id = Issue_Id FROM dbo.[table1]
INSERT INTO dbo.[storage]
SELECT Id, Title, project, Problem
FROM dbo.[table2]
WHERE Id = #Id
Is there something I am doing wrong or that I can't use variables within the scope of a trigger?
Many thanks
To support multirow updates
CREATE TRIGGER Trigger1 On dbo.[table1] FOR UPDATE
AS
SET NOCOUNT ON
INSERT INTO dbo.[storage]
SELECT t.Id, t.Title, t.project, t.Problem
FROM dbo.[table2] t
JOIN INSERTED I ON t.ID = I.ID
GO
If table2 is actually table1 (which makes more sense: how is table1 related to storage and table2?)...
CREATE TRIGGER Trigger1 On dbo.[table1] FOR UPDATE
AS
SET NOCOUNT ON
INSERT INTO dbo.[storage]
SELECT Id, Title, project, Problem
FROM INSERTED
GO
To handle multple updates and the inserted table in one go:
CREATE TRIGGER Trigger1
On dbo.[table1]
FOR UPDATE
AS
INSERT INTO dbo.[storage]
SELECT Id, Title, project, Problem
FROM dbo.[table2] t2
JOIN Inserted i ON i.Issue_ID = t2.Id
Please go through the below suggestion.
Instead of the below line
SELECT #Id = Issue_Id FROM dbo.[table1]
It had to be following.
SELECT Issue_Id FROM Inserted
Following is the updated one.
CREATE TRIGGER Trigger1
On dbo.[table1]
FOR UPDATE
AS
SET NOCOUNT ON
Declare #Id int;
With CTE as
(
SELECT Issue_Id FROM Inserted I
Inner Join [table1] T on T.Issue_Id = I.Issue_Id
)
INSERT INTO dbo.[storage]
SELECT Id, Title, project, Problem
FROM dbo.[table2]
Inner Join CTE c on c.Issue_Id = Id
For more information
In SQL server the records which are being inserted / modified or deleted occupies themselves in two temporary tables available in a DML trigger. These tables are INSERTED and DELETED. The INSERTED table has inserted or updated records. The DELETED table has the old state of the records being updated or deleted.
The others have correctly answered that you should be using inserted and a join, to build a proper trigger. But:
Based on your comments to other's answers - you should never attempt to access any resource outside of your own database from a trigger, let along from another server.
Try to decouple the trigger activity from the cross server activity - say have your trigger add a row to a queue table (or use real service broker queues), and have an independent component be responsible for servicing these requests.
Otherwise, if there are any e.g. network issues, not only does your trigger break, but it forces a rollback for the original update also - it makes your local database unusable.
This also means that the independent component can cope with timeouts, and perform appropriate retries, etc.
below line should be removed
SELECT #Id = Issue_Id FROM dbo.[table1]
It should be following.
SELECT Issue_Id FROM Inserted

Resources