How to make a generic trigger to set the audit fields? - sql

I want to automatically set the audit fields (UpdatedBy/UpdatedOn) on my tables. To that end, I have a trigger on every table that looks like this:
CREATE TRIGGER [dbo].[tr_AsyncMessage_Upd] ON [dbo].[AsyncMessage] AFTER UPDATE
AS
BEGIN
SET NOCOUNT ON;
UPDATE m
SET
m.UpdatedOn = CURRENT_TIMESTAMP
,m.UpdatedBy = SUSER_SNAME()
FROM dbo.AsyncMessage m
INNER JOIN inserted AS i
ON m.AsyncMessageID = i.AsyncMessageID
END
However, I'd rather not have to write a trigger for every single table. Is there a way to tell SQL Server to auto set them for me? Or is there a way to have a single trigger to cover all tables?

Try something like this. Copy the output of this, and check it over before you run it. This only works if the table has a primary key, and has the updatedby and updatedon columns.
SELECT 'CREATE TRIGGER tr_'+TABLE_NAME+'_Update ON '+TABLE_SCHEMA+'.'+TABLE_NAME+'
AFTER UPDATE
AS
BEGIN
UPDATE T SET UPDATEDBY=SYSTEM_USER, UPDATEDON=CURRENT_TIMESTAMP
FROM '+TABLE_SCHEMA+'.'+TABLE_NAME+' T
JOIN inserted I ON T.'+COLUMN_NAME+'=I.'+COLUMN_NAME+'
END'
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE CU
WHERE OBJECTPROPERTY(OBJECT_ID(CONSTRAINT_SCHEMA + '.' + CONSTRAINT_NAME), 'IsPrimaryKey') = 1
AND EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS C WHERE C.COLUMN_NAME='UpdatedOn' AND CU.TABLE_NAME=C.TABLE_NAME)
AND EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS C2 WHERE C2.COLUMN_NAME='UpdatedBy' AND CU.TABLE_NAME=C2.TABLE_NAME)

Sql is not C#, don't try to treat it as if it was. What is good practice in C# is not necessarily a good practice in SQL. SQL is declaractive, and generally has limited ability to modify multiple tables at the same time.
Simply write the trigger once per table, if you are doing this for a lot of tables, then write something that writes the trigger for you, and then run the results.

Related

SQL Server 2014 : FileTable trigger on update File_Stream.PathName()

I have a FileTable FT and another table AT. In AT, I have extended metadata properties of the file in FT.
I have tried to create a trigger ON FT FOR UPDATE that will update the file path that is in AT.
Here is what I've tried:
ALTER TRIGGER [dbo].[u_filepath]
ON [FileDB].[dbo].[FT]
FOR UPDATE
AS
Declare #newpath nvarchar(1000)
Declare #oldpath nvarchar(1000)
select #newpath = file_stream.pathname()
from inserted
select #oldpath = file_stream.pathname()
from deleted
update AT
set [Path] = #newpath
where [Path] = #oldpath
GO
When I execute the query, it spins. I'm planning on leaving it running overnight just in case it decides to do something.
I want the Path column in AT to update to the updated file_stream.PathName() from FT.
Is the trigger logical?
Should I store the file_stream BLOB in my AT Path column instead of the actual path?
Your trigger has MAJOR flaw in that you seem to assume it'll be called once per row - that is not the case.
The trigger will fire once per statement, so if your UPDATE statement that causes this trigger to fire inserts 25 rows, you'll get the trigger fired once, but then the Deleted and Inserted pseudo tables will each contain 25 rows.
Which of those 25 rows will your code select here?
select #newpath = file_stream.pathname()
from inserted
It's non-deterministic, you'll get one arbitrary row and you will be ignoring all other rows.
You need to rewrite your trigger to take this into account - use a set-based approach to updating your data - not a row-based one - something like this:
ALTER TRIGGER [dbo].[u_filepath]
ON [FileDB].[dbo].[FT]
FOR UPDATE
AS
-- create a CTE to get the primary key, old and new path names
WITH UpdatedPaths AS
(
SELECT
i.PrimaryKey,
OldPathName = d.file_stream.pathname(),
NewPathName = i.file_stream.pathname()
FROM
Inserted i
INNER JOIN
Deleted d ON i.PrimaryKey = d.PrimaryKey
)
-- update the "AT" table where the old and new path names don't match
UPDATE dbo.AT
SET [Path] = up.NewPathName
FROM UpdatedPaths up
WHERE
up.OldPathName <> up.NewPathName
dbo.AT.PrimaryKey = up.PrimaryKey
GO

SQL trigger - Always update or conditionally

In my scenario a single password in table B needs to be updated when this password changes in table A. I've been given a trigger which does this, but the trigger always updates the value, even when the password in table A isn't modified, but one of the other fields is modified.
This seems like overkill to me, because the trigger can be modified to update only when specifically the password field is modified.
Is the provided solution decent, or would it be better (performance wise mainly) to change the trigger and add a condition on which to actually modify the row. I can imagine the cost of conditionally updating being greater than blindly changing the value every time.
Current code:
CREATE TRIGGER [UserSync]
ON [dbo].[Import]
FOR UPDATE
AS
BEGIN
DECLARE #UserName NVARCHAR(128)
DECLARE #Password NVARCHAR(128)
SELECT #UserName=Username
, #Password=Password
FROM INSERTED
UPDATE UserLogin
SET Password = #Password
WHERE Name = #UserName
END
A better was to write this, to a) allow for updates of multiple rows as per #marc_s and b) only update where it has changed, is:
CREATE TRIGGER [UserSync]
ON [dbo].[Import]
FOR UPDATE
AS
BEGIN
UPDATE UserLogin
SET Password = i.password
from UserLogon u
inner join inserted i on i.Name = u.Name
inner join deleted d on d.Name = i.Name
WHERE i.password <> u.password
END
So, for each user in the transaction the old (deleted) and the new (inserted) are matched against the underlying table (UserLogon). Where the new password is different to the old, the underlying table is updated.

How can I automate / script an update that modifies 100+ triggers at once (or in a loop)?

Summary
I am trying to figure out a way I can automatically alter 100+ triggers at once or in a loop. The triggers are all identical except for the respective table that they are associated with.
Details:
I have a SQL server database and I have 300+tables and over 100+ of those tables have a trigger that prevents anyone from deleting any records.
As you can see below, trigger simply sets "IsDeleted" field to 1 if a person tries to do a delete (this way I never lose anything)
For example a trigger in my Person table would look like this:
ALTER TRIGGER [TD_Person]
ON [Person]
INSTEAD OF DELETE
AS
SET NOCOUNT ON
UPDATE x
SET [IsDeleted] = 1
FROM [Person] x
INNER JOIN
deleted d
ON x.[Id] = d.[Id]
the issue is that i now realize that I need to update another field (besides IsDeleted) called LastUpdated (which is a datetime field) as part of this trigger. This way, I am capturing the time in which the delete occurred
To make the change is quite simple. Instead of just:
SET [IsDeleted] = 1
I would do this:
SET [IsDeleted] = 1, LastUpdated = GETDATE()
in the trigger above. I tested the change and it works fine.
My issue is that i don't want to manually go into each of my table and have paste ", LastUpdated = GETDATE()" into the trigger one by one.
I can isolate these triggers by doing something like this:
Select *
FROM sys.triggers t
where t.Name like 'MyDeleteTrigger_%'
ESCAPE '\'
but I can't figure out how I could leverage this to modify each of these triggers?
Is there any way to somehow automate this update to a trigger that exists in so many tables to avoid tedious copy and paste over again?
You can use following query in order to automatically change your triggers:
DECLARE #Command NVARCHAR(MAX) = ''
SELECT #Command = #Command +
'ALTER TRIGGER [TD_'+OBJECT_NAME(t.parent_id)+']'+CHAR(13) +
'ON ['+OBJECT_NAME(t.parent_Id)+']'+CHAR(13) +
'INSTEAD OF DELETE'+CHAR(13)+
'AS'+CHAR(13)+
'SET NOCOUNT ON'+CHAR(13)+CHAR(13)+
'UPDATE x'+CHAR(13)+
'SET [IsDeleted] = 1, LastUpdated = GETDATE()'+CHAR(13)+
'FROM ['+OBJECT_SCHEMA_NAME(t.parent_id)+'].['+OBJECT_NAME(t.parent_id)+'] x'+CHAR(13)+
'INNER JOIN deleted d ON x.[Id] = d.[Id]'+CHAR(13)+
'End;'+CHAR(13)
FROM sys.triggers t
INNER JOIN sys.sql_modules m ON m.object_id = t.object_id
WHERE is_instead_of_trigger = 1
and m.definition like '%INSTEAD OF DELETE%'
--SELECT #Command
EXEC(#Command)

SQL Trigger update another table

I am newbie to triggers... can anybody help me with a trigger?
I have Table:
Name | Number
I want to write a trigger when my table receives a query like
update MyTable
set Number = Number + 1
where Name = 'myname'
When this query is running, the trigger should update another table for example:
Update MyTable 2
set Column = 'something'
where Name = 'myname (above name)
Thank you very much !
You will need to write an UPDATE trigger on table 1, to update table 2 accordingly.
Be aware: triggers in SQL Server are not called once per row that gets updated - they're called once per statement, and the internal "pseudo" tables Inserted and Deleted will contain multiple rows, so you need to take that into account when writing your trigger.
In your case, I'd write something like:
-- UPDATE trigger on "dbo.Table1"
CREATE TRIGGER Table1Updated
ON dbo.table1 FOR UPDATE
AS
BEGIN
-- update table2, using the same rows as were updated in table1
UPDATE t2
SET t2.Column = 'something'
FROM dbo.Table2 t2
INNER JOIN Inserted i ON t2.ID = i.ID
END
GO
The trick is to use the Inserted pseudo table (which contains the new values after the UPDATE - it has the exact same structure as your table the trigger is written for - here dbo.Table1) in a set-based fashion - join that to your dbo.Table2 on some column that they have in common (an ID or something).
create a trigger on table 1 for update:
CREATE TRIGGER dbo.update_trigger
ON table1
AFTER UPDATE
AS
BEGIN
DECLARE #Name VARCHAR(50)
SELECT #Name=Name FROM INSERTED
Update MyTable 2
SET Column = 'something'
WHERE Name = #Name
END
GO
try this ;)

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.