Recursive Update trigger issue in SQL 2005 - sql-server-2005

Below is the code snippet with comments which describes the problem statement. We have an update trigger which internally calls another update trigger on the same table inspite of Recursive Trigger Enabled Property Set to false.
Would like to understand the reason for this as this is causing a havoc in my applications.
/* Drop statements for the table and triggers*/
IF EXISTS (SELECT * FROM sys.triggers WHERE object_id = OBJECT_ID(N'[dbo]. [t_upd_TestTrigger_002]'))
DROP TRIGGER [dbo].[t_upd_TestTrigger_002]
IF EXISTS (SELECT * FROM sys.triggers WHERE object_id = OBJECT_ID(N'[dbo].[t_upd_TestTrigger_002]'))
DROP TRIGGER [dbo].[t_upd_TestTrigger_001]
IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[TestTrigger]') AND type in (N'U'))
DROP TABLE [dbo].[TestTrigger]
CREATE TABLE [dbo].[TestTrigger] /*Creating a test table*/
(
[InternalKey] INT NOT NULL,
[UserModified] varchar(50) DEFAULT SUSER_SNAME()
)
/* Please run the snippet below as seperate batch, else you will get
an error that 'CREATE TRIGGER' must be the first statement in a
query batch.
CREATING A UPDATE TRIGGER FOR THE TEST TABLE
*/
CREATE TRIGGER [t_upd_TestTrigger_001] ON [dbo].[TestTrigger]
FOR UPDATE
AS
BEGIN
--This trigger has some business logic which gets executed
print 'In Trigger 001 '
END
/* Please run the snippet below as separate batch, else you will
get an error that 'CREATE TRIGGER' must be the first statement
in a query batch.
CREATING Another UPDATE TRIGGER FOR THE TEST TABLE
This trigger updates the audit fields in the table and it has to be
a separate trigger, We cannot combine this with other update triggers -
So table TestTrigger will have two FOR UPDATE triggers
*/
CREATE TRIGGER [t_upd_TestTrigger_002] ON [dbo].[TestTrigger]
FOR UPDATE
AS
print 'bad guy starts'
UPDATE SRC
SET UserModified = SUSER_SNAME()
FROM inserted AS INS
INNER JOIN dbo.[TestTrigger] AS SRC
ON INS.InternalKey = SRC.InternalKey
print 'bad guy ends'
/* INSERTING TEST VALUE IN THE TEST TRIGGER TABLE*/
INSERT INTO dbo.[TestTrigger](InternalKey,UserModified)
SELECT 1 ,'Tester1' UNION ALL
SELECT 2,'Tester2' UNION ALL
SELECT 3 ,'Tester3'
/* TestTrigger table has 3 records, we will update the InternalKey
of first record from 1 to 4. We would expect following actions
1) [t_upd_TestTrigger_001] to be executed once
2) [t_upd_TestTrigger_002] to be executed once
3) A message that (1 row(s) affected) only once.
On Execution, i find that [t_upd_TestTrigger_002] internally triggers
[t_upd_TestTrigger_001].
Please note Database level property Recursive Triggers enabled is
set to false.
*/
/*UPDATE THE TABLE SEE THE MESSAGE IN RESULT WINDOW*/
UPDATE dbo.[TestTrigger]
SET InternalKey = 4
WHERE InternalKey = 1

"Recursive Triggers enabled" does not affect transitive triggers.
Which means that if trigger A updates a table in a manner that activates trigger B, and trigger B updates the same table, so that trigger A is run again, SQL Server has no way of detecting and inhibiting this endless loop. Especially since trigger B can update other tables, and a trigger on them could update the original table again - this could become as complex as you like.
Eventually, the trigger nesting level limit will be reached, and the loop stops.
I suspect that both of your triggers update the source table in some way. SQL Server can only detect recursive triggers if a trigger is activating itself. I suppose that's not the case for you. Restructuring the triggers is the only clean way out.
As a (hackery) idea: You could append a field to the table (data-type and value is irrelevant) that is updated by no operation but by triggers. Then change your second-order triggers to update that field. Add an IF UPDATE() check for that field to your first-order trigger. Prevent the now redundant update if the field has been set. If that makes sense. ;-)
MSDN: Using Nested Triggers, see sections "Direct recursion" and "Indirect recursion".

You can use IF UPDATE(), as Tomalak described, to skip trigger logic if UserModified is being updated.
Another possibility is to move the UserModified column to a separate table to avoid recursion.

If you want to stop this kind of behaviour across the database totally, "To disable indirect recursion, set the nested triggers server option to 0 using sp_configure. For more information, see Using Nested Triggers."
Of course, there's always the consideration that you may want to actually use nested triggers.

Related

Only ALTER a TRIGGER if is exists

I am implementing an auditing system on my database. It uses triggers on each table to log changes.
I need to make modifications to these triggers and so am producing ALTER scripts for each one.
What I'd like to do is only have these triggers be altered if they exist, ideally like so:
IF EXISTS (SELECT * FROM sysobjects WHERE type = 'TR' AND name = 'MyTable_Audit_Update')
BEGIN
ALTER TRIGGER [dbo].[MyTable_Audit_Update] ON [dbo].[MyTable]
AFTER Update
...
END
However when I do this I get an error saying "Invalid syntax near keyword TRIGGER"
The reason that these triggers may not exist is that auditing can be enabled/disabled on tables which the end user can specify. This involves either creating or dropping the triggers. I am unable to make the changes to the triggers upon creation as they are dynamically created and so I must still provide a way altering the triggers should they exist.
The alter statement has to be the first in the batch. So for sql server it would be:
IF EXISTS (SELECT * FROM sysobjects WHERE type = 'TR' AND name = 'MyTable_Audit_Update')
BEGIN
EXEC('ALTER TRIGGER [dbo].[MyTable_Audit_Update] ON [dbo].[MyTable]
AFTER Update
...')
END
This might be a similar issue to one that I found with Sybase years ago, where I found that when trying to execute a create table statement conditionally, the DDL is executed prior to assessing the conditional statement. The only workaround was to use execute immediate.
Unlike CREATE TRIGGER, I failed to find a reference that explicitly states that
CREATE TRIGGER must be the first statement in the batch
but it seems that this restriction applies to ALTER TABLE too.
The simple way to do this would be to DROP the TRIGGER and re-create it:
IF EXISTS (SELECT * FROM sysobjects WHERE type = 'TR' AND name = 'MyTable_Audit_Update')
DROP TRIGGER MyTable_Audit_Update
GO
CREATE TRIGGER [dbo].[MyTable_Audit_Update] ON [dbo].[MyTable]
AFTER Update
...
END

SQL Server 2012 trigger: Auditing. How to see previous value of a row without shadow table

I am trying to create an auditing table. I have a table called person.address in the AdventureWorks 2012 database.
I am using a trigger to capture changes to the table, the only problem is I do not know if it is possible to use a trigger to capture a row BEFORE it is edited. I am trying to save resources and overheads so trying to not use a shadow table. I know there is no "Before Insert" trigger. But is there any way to capture the information contained in a row, and when someone does an insert or update, this row can be written to my audit.table before the insert is completed?
Thank you.
Given a simplistic table with two rows:
CREATE TABLE dbo.foo(a INT PRIMARY KEY);
INSERT dbo.foo(a) VALUES(1),(2);
Then an update trigger simply to demonstrate:
CREATE TRIGGER dbo.trfoo ON dbo.foo FOR UPDATE
AS
BEGIN
SET NOCOUNT ON;
SELECT a FROM inserted;
SELECT a FROM deleted;
END
GO
The result of an action, such as:
UPDATE dbo.foo SET a += 1;
Results in:
a -- this is the *new* version of these rows
----
3
2
a -- this is the *old* version of these rows
----
2
1
Also, there is an INSTEAD OF INSERT trigger, which allows you to perform actions before the insert (they're not called BEFORE triggers because you still have to perform the insert yourself). More info here.

SQL Server Trigger not triggered after insert and update

I want to copy the contents of a column into another column in the same table. Therefore, I created this trigger:
ALTER TRIGGER [dbo].[kennscht_copy_to_prodverpt]
ON [dbo].[Stammdaten]
AFTER INSERT
AS
UPDATE Stammdaten
SET PRODVERPT = (SELECT KENNSCHT FROM INSERTED)
WHERE SNR = (SELECT SNR FROM INSERTED);
But when I use an UPDATE on the table to update KENNSCHT to a different value, PRODVERPT is not updated as well. Now, you could argue that is because the trigger is on AFTER INSERT and not AFTER UPDATE, but when I change it so it's triggered by UPDATE and INSERT, whenever I update any row, I get an error message from SQL Server
Cannot update row because it would make the row not unique or update multiple rows (2 rows) [sic]
What is going on here? Either the trigger doesn't do anything, or it's messing up the whole table, making it un-updateable.
Update: I also tried the following trigger:
UPDATE s
SET s.PRODVERPT = i.KENNSCHT
FROM Stammdaten s
INNER JOIN INSERTED i ON i.SNR = s.SNR;
But it has exactly the same behaviour. If I use only AFTER INSERT, nothing changes and if I use AFTER INSERT, UPDATE I get the error message above.
Update 2: There are no multiple rows in the table, I already checked that because I thought it might be connected to the issue.
If you run this trigger as an AFTER UPDATE trigger, it runs recursively, since it always issues another UPDATE statement against the same table, which leads to another execution of the trigger.
To work around this, you either need to make the update trigger an INSTEAD OF UPDATE trigger or test if the KENNSCHT column was modified at all. For the latter you can use the UPDATE() function like this:
ALTER TRIGGER [dbo].[kennscht_copy_to_prodverpt_after_update]
ON [dbo].[Stammdaten]
AFTER UPDATE
AS
BEGIN
SET NOCOUNT ON
IF (UPDATE(KENNSCHT))
BEGIN
UPDATE s
SET s.PRODVERPT = i.KENNSCHT
FROM Stammdaten s
INNER JOIN INSERTED i ON i.SNR = s.SNR
END
END

How can I do a BEFORE UPDATED trigger with sql server?

I'm using Sqlserver express and I can't do before updated trigger. There's a other way to do that?
MSSQL does not support BEFORE triggers. The closest you have is INSTEAD OF triggers but their behavior is different to that of BEFORE triggers in MySQL.
You can learn more about them here, and note that INSTEAD OF triggers "Specifies that the trigger is executed instead of the triggering SQL statement, thus overriding the actions of the triggering statements." Thus, actions on the update may not take place if the trigger is not properly written/handled. Cascading actions are also affected.
You may instead want to use a different approach to what you are trying to achieve.
It is true that there aren't "before triggers" in MSSQL. However, you could still track the changes that were made on the table, by using the "inserted" and "deleted" tables together. When an update causes the trigger to fire, the "inserted" table stores the new values and the "deleted" table stores the old values. Once having this info, you could relatively easy simulate the "before trigger" behaviour.
Can't be sure if this applied to SQL Server Express, but you can still access the "before" data even if your trigger is happening AFTER the update. You need to read the data from either the deleted or inserted table that is created on the fly when the table is changed. This is essentially what #Stamen says, but I still needed to explore further to understand that (helpful!) answer.
The deleted table stores copies of the affected rows during DELETE and
UPDATE statements. During the execution of a DELETE or UPDATE
statement, rows are deleted from the trigger table and transferred to
the deleted table...
The inserted table stores copies of the affected rows during INSERT
and UPDATE statements. During an insert or update transaction, new
rows are added to both the inserted table and the trigger table...
https://msdn.microsoft.com/en-us/library/ms191300.aspx
So you can create your trigger to read data from one of those tables, e.g.
CREATE TRIGGER <TriggerName> ON <TableName>
AFTER UPDATE
AS
BEGIN
INSERT INTO <HistoryTable> ( <columns...>, DateChanged )
SELECT <columns...>, getdate()
FROM deleted;
END;
My example is based on the one here:
http://www.seemoredata.com/en/showthread.php?134-Example-of-BEFORE-UPDATE-trigger-in-Sql-Server-good-for-Type-2-dimension-table-updates
sql-server triggers
T-SQL supports only AFTER and INSTEAD OF triggers, it does not feature a BEFORE trigger, as found in some other RDBMSs.
I believe you will want to use an INSTEAD OF trigger.
All "normal" triggers in SQL Server are "AFTER ..." triggers. There are no "BEFORE ..." triggers.
To do something before an update, check out INSTEAD OF UPDATE Triggers.
To do a BEFORE UPDATE in SQL Server I use a trick. I do a false update of the record (UPDATE Table SET Field = Field), in such way I get the previous image of the record.
Remember that when you use an instead trigger, it will not commit the insert unless you specifically tell it to in the trigger. Instead of really means do this instead of what you normally do, so none of the normal insert actions would happen.
Full example:
CREATE TRIGGER [dbo].[trig_020_Original_010_010_Gamechanger]
ON [dbo].[T_Original]
AFTER UPDATE
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
DECLARE #Old_Gamechanger int;
DECLARE #New_Gamechanger int;
-- Insert statements for trigger here
SELECT #Old_Gamechanger = Gamechanger from DELETED;
SELECT #New_Gamechanger = Gamechanger from INSERTED;
IF #Old_Gamechanger != #New_Gamechanger
BEGIN
INSERT INTO [dbo].T_History(ChangeDate, Reason, Callcenter_ID, Old_Gamechanger, New_Gamechanger)
SELECT GETDATE(), 'Time for a change', Callcenter_ID, #Old_Gamechanger, #New_Gamechanger
FROM deleted
;
END
END
The updated or deleted values are stored in DELETED. we can get it by the below method in trigger
Full example,
CREATE TRIGGER PRODUCT_UPDATE ON PRODUCTS
FOR UPDATE
AS
BEGIN
DECLARE #PRODUCT_NAME_OLD VARCHAR(100)
DECLARE #PRODUCT_NAME_NEW VARCHAR(100)
SELECT #PRODUCT_NAME_OLD = product_name from DELETED
SELECT #PRODUCT_NAME_NEW = product_name from INSERTED
END

How do I prevent a database trigger from recursing?

I've got the following trigger on a table for a SQL Server 2008 database. It's recursing, so I need to stop it.
After I insert or update a record, I'm trying to simply update a single field on that table.
Here's the trigger :
ALTER TRIGGER [dbo].[tblMediaAfterInsertOrUpdate]
ON [dbo].[tblMedia]
BEFORE INSERT, UPDATE
AS
BEGIN
SET NOCOUNT ON
DECLARE #IdMedia INTEGER,
#NewSubject NVARCHAR(200)
SELECT #IdMedia = IdMedia, #NewSubject = Title
FROM INSERTED
-- Now update the unique subject field.
-- NOTE: dbo.CreateUniqueSubject is my own function.
-- It just does some string manipulation.
UPDATE tblMedia
SET UniqueTitle = dbo.CreateUniqueSubject(#NewSubject) +
CAST((IdMedia) AS VARCHAR(10))
WHERE tblMedia.IdMedia = #IdMedia
END
Can anyone tell me how I can prevent the trigger's insert from kicking off another trigger again?
Not sure if it is pertinent to the OP's question anymore, but in case you came here to find out how to prevent recursion or mutual recursion from happening in a trigger, you can test for this like so:
IF TRIGGER_NESTLEVEL() <= 1/*this update is not coming from some other trigger*/
MSDN link
I see three possibilities:
Disable trigger recursion:
This will prevent a trigger fired to call another trigger or calling itself again. To do this, execute this command:
ALTER DATABASE MyDataBase SET RECURSIVE_TRIGGERS OFF
GO
Use a trigger INSTEAD OF UPDATE, INSERT
Using a INSTEAD OF trigger you can control any column being updated/inserted, and even replacing before calling the command.
Control the trigger by preventing using IF UPDATE
Testing the column will tell you with a reasonable accuracy if you trigger is calling itself. To do this use the IF UPDATE() clause like:
ALTER TRIGGER [dbo].[tblMediaAfterInsertOrUpdate]
ON [dbo].[tblMedia]
FOR INSERT, UPDATE
AS
BEGIN
SET NOCOUNT ON
DECLARE #IdMedia INTEGER,
#NewSubject NVARCHAR(200)
IF UPDATE(UniqueTitle)
RETURN;
-- What is the new subject being inserted?
SELECT #IdMedia = IdMedia, #NewSubject = Title
FROM INSERTED
-- Now update the unique subject field.
-- NOTE: dbo.CreateUniqueSubject is my own function.
-- It just does some string manipulation.
UPDATE tblMedia
SET UniqueTitle = dbo.CreateUniqueSubject(#NewSubject) +
CAST((IdMedia) AS VARCHAR(10))
WHERE tblMedia.IdMedia = #IdMedia
END
TRIGGER_NESTLEVEL can be used to prevent recursion of a specific trigger, but it is important to pass the object id of the trigger into the function. Otherwise you will also prevent the trigger from firing when an insert or update is made by another trigger:
IF TRIGGER_NESTLEVEL(OBJECT_ID('dbo.mytrigger')) > 1
BEGIN
PRINT 'mytrigger exiting because TRIGGER_NESTLEVEL > 1 ';
RETURN;
END;
From MSDN:
When no parameters are specified, TRIGGER_NESTLEVEL returns the total
number of triggers on the call stack. This includes itself.
Reference:
Avoiding recursive triggers
ALTER DATABASE <dbname> SET RECURSIVE_TRIGGERS OFF
RECURSIVE_TRIGGERS { ON | OFF }
ON Recursive firing of AFTER triggers is allowed.
OFF Only direct recursive firing of AFTER triggers is not allowed. To
also disable indirect recursion of
AFTER triggers, set the nested
triggers server option to 0 by using
sp_configure.
Only direct recursion is prevented when RECURSIVE_TRIGGERS is set to OFF.
To disable indirect recursion, you
must also set the nested triggers
server option to 0.
The status of this option can be determined by examining the
is_recursive_triggers_on column in the
sys.databases catalog view or the
IsRecursiveTriggersEnabled property of
the DATABASEPROPERTYEX function.
I think i got it :)
When the title is getting 'updated' (read: inserted or updated), then update the unique subject. When the trigger gets ran a second time, the uniquesubject field is getting updated, so it stop and leaves the trigger.
Also, i've made it handle MULTIPLE rows that get changed -> I always forget about this with triggers.
ALTER TRIGGER [dbo].[tblMediaAfterInsert]
ON [dbo].[tblMedia]
FOR INSERT, UPDATE
AS
BEGIN
SET NOCOUNT ON
-- If the Title is getting inserted OR updated then update the unique subject.
IF UPDATE(Title) BEGIN
-- Now update all the unique subject fields that have been inserted or updated.
UPDATE tblMedia
SET UniqueTitle = dbo.CreateUniqueSubject(b.Title) +
CAST((b.IdMedia) AS VARCHAR(10))
FROM tblMedia a
INNER JOIN INSERTED b on a.IdMedia = b.IdMedia
END
END
You can have a separate NULLABLE column indicating whether the UniqueTitle was set.
Set it to true value in a trigger, and have the trigger do nothing if it's value is true in "INSERTED"
For completeness sake, I will add a few things. If you have a particular after trigger that you only want to run once, you can set it up to run last using sp_settriggerorder.
I would also consider if it might not be best to combine the triggers that are doing the recursion into one trigger.