how to create before update trigger in sql server 2005 - sql-server-2005

Is there anyway where I can create a trigger which will execute before the update/delete takes place( and then the actual update/delete takes place)? and how can I drop a trigger from a table?

to drop a trigger use:
--SQL Server 2005+, drop the trigger, no error message if it does not exist yet
BEGIN TRY DROP TRIGGER dbo.TrigerYourTable END TRY BEGIN CATCH END CATCH
GO
--drop trigger pre-SQl Server 2005, no error message if it does not exist yet
if exists (select * from sysobjects where id = object_id(N'[dbo].[TrigerYourTable ]') and OBJECTPROPERTY(id, N'IsTrigger') = 1)
drop trigger [dbo].[TrigerYourTable ]
GO
OP said in a comment:
...suppose I have to check childcount of
a perticular user if that is more than
5 do not update the user.how can I do
that using instead of trigger?
You don't really need to prevent the original update, you can let it happen, and then in the trigger check for the problem and rollback if necessary. This is how to enforce the logic for one or many affected rows, when you need to JOIN to determine the childcount of the affected rows:
--create the trigger
CREATE TRIGGER dbo.TrigerYourTable ON dbo.YourTable
AFTER UPDATE
AS
SET NOCOUNT ON
IF EXISTS (SELECT
1
FROM INSERTED i
INNER JOIN YourChildrenTable c ON i.ParentID=c.ParentID
GROUP BY i.ParentID
HAVING COUNT(i.ParentID)>5
)
BEGIN
RAISERROR('Count of children can not exceed 5',16,1)
ROLLBACK
RETURN
END
GO
It will throw the error if there is a violation of the logic, and the original command will be subject to a rollback.
If childcount is a column within the affected table, then use a trigger like this to enforce the logic:
--create the trigger
CREATE TRIGGER dbo.TrigerYourTable ON dbo.YourTable
AFTER UPDATE
AS
SET NOCOUNT ON
IF EXISTS (SELECT 1 FROM INSERTED WHERE childcount>5)
BEGIN
RAISERROR('Count of children can not exceed 5',16,1)
ROLLBACK
RETURN
END
GO
If you just want to ignore the update for any rows that violate the rule try this:
--create the trigger
CREATE TRIGGER dbo.TrigerYourTable ON dbo.YourTable
INSTEAD OF UPDATE
AS
SET NOCOUNT ON
UPDATE y
SET col1=i.col1
,col2=i.col2
,col3=i.col3
,.... --list all columns except the PK column!
FROM dbo.YourTable y
INNER JOIN INSERTED i on y.PK=i.PK
WHERE i.childcount<=5
GO
It will only update rows that have a child count less than 5, ignoring all affected rows that fail the requirement (no error message).

This article from microsoft explains the syntax of creating triggers.
http://msdn.microsoft.com/en-us/library/ms189799.aspx
There isn't really a 'before' trigger, but you can use an INSTEAD OF trigger that allows you to jump in place of whatever action is attempted, then define your own action.
I've used that technique for versioning data.
CREATE TRIGGER [dbo].[Documents_CreateVersion]
ON [dbo].[Documents]
INSTEAD OF UPDATE
AS
BEGIN
DECLARE #DocumentID int
SELECT DocumentID = DocumentID FROM INSERTED
-- do something
END
INSERTED is a bit of a misnomer here, but it contains the details of the action before it occurs you can then define your own action with that data.
Edit:
As per comments below my response, my example can be dangerous if multiple rows are updated at once. My application doesn't allow for this so it's fine in this case. I would agree that the above is a bad practice regardless.

to drop trigger- use database_name
IF EXISTS (SELECT name FROM sysobjects
WHERE name = 'tgr_name' AND type = 'TR')
DROP TRIGGER tgr_name
GO

Here's a simple trigger that checks columns values, and fires before updating or inserting, and raises an error.
IF OBJECT_ID ('dbo.MyTableTrigger', 'TR') IS NOT NULL
DROP TRIGGER dbo.MyTableTrigger;
GO
CREATE TRIGGER MyTableTrigger
ON dbo.MyTable
FOR INSERT, UPDATE
AS
DECLARE #Col1ID INT
DECLARE #Col2ID INT
SELECT #Col1ID = Col1ID, #Col2ID = Col2ID FROM inserted
IF ((#Col1ID IS NOT NULL) AND (#Col2ID IS NOT NULL))
BEGIN
RAISERROR ('Col1ID and Col2ID cannot both be in MyTable at the same time.', 16, 10);
END

Related

Trigger to update table column after insert?

I need to update a column in table after any record is added in same table
Here is my sql code
CREATE TRIGGER [dbo].[EmployeeInsert]
ON [dbo].[APP_Employees]
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON;
DECLARE #EmployeeID AS bigint
SELECT #EmployeeID = ID FROM inserted
UPDATE [dbo].[APP_Employees]
SET [EmployeeTotalNumberOfAnnualLeave] = [EmployeeBalanceTheInitialNumberOfDaysOfAnnualLeaveIn]
WHERE ID=#EmployeeID
END
GO
and showing error
Msg 2714, Level 16, State 2, Procedure EmployeeInsert, Line 17
There is already an object named 'EmployeeInsert' in the database.
The error you're getting is because you have that trigger already, in your database. So if you want to create it again, you need to first drop the existing trigger (or use ALTER TRIGGER instead of CREATE TRIGGER to modify the existing trigger).
BUT: your fundamental flaw is that you seem to expect the trigger to be fired once per row - this is NOT the case in SQL Server. Instead, the trigger fires once per statement, and the pseudo table Inserted might contain multiple rows.
Given that that table might contain multiple rows - which one do you expect will be selected here??
SELECT #EmployeeID = ID FROM inserted
It's undefined - you might get the values from arbitrary rows in Inserted.
You need to rewrite your entire trigger with the knowledge the Inserted WILL contain multiple rows! You need to work with set-based operations - don't expect just a single row in Inserted !
-- drop the existing trigger
DROP TRIGGER [dbo].[EmployeeInsert]
GO
-- create a new trigger
CREATE TRIGGER [dbo].[EmployeeInsert]
ON [dbo].[APP_Employees]
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON;
-- update your table, using a set-based approach
-- from the "Inserted" pseudo table which CAN and WILL
-- contain multiple rows!
UPDATE [dbo].[APP_Employees]
SET [EmployeeTotalNumberOfAnnualLeave] = i.[EmployeeBalanceTheInitialNumberOfDaysOfAnnualLeaveIn]
FROM Inserted i
WHERE [dbo].[APP_Employees].ID = i.ID
END
GO

Deleted Virtual table in SQL Server is empty ON Delete trigger

I am trying to invoke an ON Delete trigger event, but when I try to select row from the Deleted table, it is empty.
Is there any other way to get Id of row on which delete event has fired?
Code:
set ANSI_NULLS ON
set QUOTED_IDENTIFIER ON
go
ALTER TRIGGER [audit]
ON [dbo].[S_PARTY]
FOR DELETE
AS
-- Insert statements for trigger here
DECLARE #id nvarchar(50)
SET #id = (Select ROW_ID from deleted )
BEGIN
-- Insert statements for trigger here
INSERT INTO [dbo].[S_AMF_AUDIT_ITEM] (ROW_ID)
VALUES (#id);
END
When trigger fires on this, I am getting error as S_AMF_AUDIT_ITEM doesn't allow null values. So can you please help me this to get Id of table on which delete command executes?
Your trigger is broken.
It doesn't take into account that the DELETE statement might affect zero or more than one rows.
The issue with NULL could occur for the statement DELETE FROM [dbo].[S_PARTY] WHERE 1 = 0.
A fixed version would be
ALTER TRIGGER [audit]
ON [dbo].[S_PARTY]
FOR DELETE
AS
BEGIN
INSERT INTO [dbo].[S_AMF_AUDIT_ITEM]
(ROW_ID)
SELECT ROW_ID
FROM deleted;
END
A trigger can fire for 0..N deleted rows. For example:
delete YourTable
where 1=2
Would run a for delete trigger on YourTable. So a trigger has to be able to deal with zero or multiple rows. Consider rewriting your trigger like:
ALTER TRIGGER [audit] ON [dbo].[S_PARTY] FOR DELETE
AS
INSERT dbo.S_AMF_AUDIT_ITEM
(ROW_ID)
SELECT ROW_ID
FROM deleted
That will work for any number of inserted rows.

T-SQL: How to deny update on one column of a table via trigger?

Question:
In our SQL-Server 2005 database, we have a table T_Groups.
T_Groups has, amongst other things, the fields ID (PK) and Name.
Now some idiot in our company used the name as key in a mapping table...
Which means now one may not alter a group name, because if one does, the mapping is gone...
Now, until this is resolved, I need to add a restriction to T_Groups, so one can't update the group's name.
Note that insert should still be possible, and an update that doesn't change the groupname should also be possible.
Also note that the user of the application & the developers have both dbo and sysadmin rights, so REVOKE/DENY won't work.
How can I do this with a trigger ?
CREATE TRIGGER dbo.yournametrigger ON T_Groups
FOR UPDATE
AS
BEGIN
IF UPDATE(name)
BEGIN
ROLLBACK
RAISERROR('Changes column name not allowed', 16, 1);
END
ELSE
BEGIN
--possible update that doesn't change the groupname
END
END
CREATE TRIGGER tg_name_me
ON tbl_name
INSTEAD OF UPDATE
AS
IF EXISTS (
SELECT *
FROM INSERTED I
JOIN DELETED D ON D.PK = I.PK AND ISNULL(D.name,I.name+'.') <> ISNULL(I.name,D.name+'.')
)
RAISERROR('Changes to the name in table tbl_name are NOT allowed', 16,1);
GO
Depending on your application framework for accessing the database, a cheaper way to check for changes is Alexander's answer. Some frameworks will generate SQL update statements that include all columns even if they have not changed, such as
UPDATE TBL
SET name = 'abc', -- unchanged
col2 = null, -- changed
... etc all columns
The UPDATE() function merely checks whether the column is present in the statement, not whether its value has changed. This particular statement will raise an error using UPDATE() but won't if tested using the more elaborate trigger as shown above.
This is an example of preserving some original values with an update trigger.
It works by setting the values for orig_author and orig_date to the values from the deleted pseudotable each time. It still performs the work and uses cycles.
CREATE TRIGGER [dbo].[tru_my_table] ON [dbo].[be_my_table]
AFTER UPDATE
AS
UPDATE [dbo].[be_my_table]
SET
orig_author = deleted.orig_author
orig_date = deleted.orig_date,
last_mod_by = SUSER_SNAME(),
last_mod_dt = getdate()
from deleted
WHERE deleted.my_table_id IN (SELECT DISTINCT my_table_id FROM Inserted)
ALTER TABLE [dbo].[be_my_table] ENABLE TRIGGER [tru_my_table]
GO
This example will lock any updates on SABENTIS_LOCATION.fk_sabentis_location through a trigger, and will output a detailed message indicating what objects are affected
ALTER TRIGGER dbo.SABENTIS_LOCATION_update_fk_sabentis_location ON SABENTIS_LOCATION
FOR UPDATE
AS
DECLARE #affected nvarchar(max)
SELECT #affected=STRING_AGG(convert(nvarchar(50), a.id), ', ')
FROM inserted a
JOIN deleted b ON a.id = b.id
WHERE a.fk_sabentis_location != b.fk_sabentis_location
IF #affected != ''
BEGIN
ROLLBACK TRAN
DECLARE #message nvarchar(max) = CONCAT('Update values on column fk_sabentis_location locked by custom trigger. Could not update entities: ', #affected);
RAISERROR(#message, 16, 1)
END
Some examples seem to be using:
IF UPDATE(name)
But this seems to evaluate to TRUE if the field is part of the update statement, even if the value itself has NOT CHANGED leading to false positives.

Rollback transaction from trigger

In MS SQL Server 2008 R2, we want a pre-insert and pre-update trigger which checks something and allows or rollbacks (via raiserror) the running insert/update.
Question: In INSTEAD OF trigger. Does one really has to explicitly write the insert or update? Because we want the default insert or update to be done and only do the "precheck".
Yes.
You do need to write the explicit INSERT or UPDATE.
The trigger runs INSTEAD OF the DML operation. If you leave the trigger blank then no action will happen other than the INSERTED / DELETED tables being created and populated in tempdb.
Although from discussion in the comments I would not use a trigger for this at all but use a unique filtered index CREATE UNIQUE INDEX ix ON T(a,b,c) WHERE c <> ''. This is likely to be more performant and avoid potential logic issues when dealing with concurrency.
You probably do not want an INSTEAD OF trigger unless you want to replace the actual insert or update. In your case, you want a FOR INSERT, UPDATE trigger instead.
This example trigger prints a message to the client when anyone tries to add or change data in the titles table.
USE pubs
IF EXISTS (SELECT name FROM sysobjects
WHERE name = 'reminder' AND type = 'TR')
DROP TRIGGER reminder
GO
CREATE TRIGGER reminder
ON titles
FOR INSERT, UPDATE
AS RAISERROR ('inserts and updates to the titles table is not allowed', 16, 1)
GO
You could also use things like IF EXISTS or COLUMNS_UPDATED as well.
Here another example that uses rollback.
USE pubs
IF EXISTS (SELECT name FROM sysobjects
WHERE name = 'employee_insupd' AND type = 'TR')
DROP TRIGGER employee_insupd
GO
CREATE TRIGGER employee_insupd
ON employee
FOR INSERT, UPDATE
AS
/* Get the range of level for this job type from the jobs table. */
DECLARE #min_lvl tinyint,
#max_lvl tinyint,
#emp_lvl tinyint,
#job_id smallint
SELECT #min_lvl = min_lvl,
#max_lvl = max_lvl,
#emp_lvl = i.job_lvl,
#job_id = i.job_id
FROM employee e INNER JOIN inserted i ON e.emp_id = i.emp_id
JOIN jobs j ON j.job_id = i.job_id
IF (#job_id = 1) and (#emp_lvl <> 10)
BEGIN
RAISERROR ('Job id 1 expects the default level of 10.', 16, 1)
ROLLBACK TRANSACTION
END
ELSE
IF NOT (#emp_lvl BETWEEN #min_lvl AND #max_lvl)
BEGIN
RAISERROR ('The level for job_id:%d should be between %d and %d.',
16, 1, #job_id, #min_lvl, #max_lvl)
ROLLBACK TRANSACTION
END
I'm not sure if you have a transaction or not, but in your case you would want something like the following:
USE myDatabase
IF EXISTS (SELECT name FROM sysobjects
WHERE name = 'myTable' AND type = 'TR')
DROP TRIGGER tr_myTrigger
GO
CREATE TRIGGER tr_myTrigger
ON myTable
FOR INSERT, UPDATE
AS
if(exists(select * from inserted where rtrim(c) <> ''))
begin
-- check to make sure the insert(s) are unique
if(exists(
select * from inserted i
join dbo.myTable t on i.a = t.a and i.b = t.b and i.c = t.c)
begin
raiserror('Duplicate(s) found', 16, 1)
rollback transaction
end
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.