SQL trigger - Always update or conditionally - sql

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.

Related

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

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.

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 Server : create trigger to replace old value to new value on another table

I am using SQL Server 2008. I want to create a trigger for update which will fire on update of user table.
Trigger functionality: replace user_tbl updated mobile number to user_work_tbl.
CREATE TRIGGER [dbo].[tr_User_Modified]
ON [dbo].[user_tbl]
AFTER UPDATE
AS BEGIN
SET NOCOUNT ON;
DECLARE #MobileNo varchar(11)
IF UPDATE (mobile_no)
BEGIN
DECLARE #MobileNo VARCHAR(50)
SELECT #MobileNo = mobile_no
FROM [dbo].user_tbl
UPDATE [dbo].[user_work_tbl]
SET mobile_no = #MobileNo
WHERE [dbo].[user_work_tbl].mobile_no = #oldMobileNo // here I have a problem
END
END;
In the comment "here I have a problem" I need a mobile number which exists in user_tbl before update so that the only row of user_work_tbl gets updated.
Any suggestions to do this are also accepted.
Thanks for your all response
You need to join three tables together in your trigger - user_work_tbl, inserted and deleted. However, its not clear at the moment exactly what conditions are required:
CREATE TRIGGER [dbo].[tr_User_Modified]
ON [dbo].[user_tbl]
AFTER UPDATE
AS BEGIN
SET NOCOUNT ON;
IF UPDATE (mobile_no)
BEGIN
UPDATE u
SET mobile_no=i.mobile_no
FROM user_work_tbl u
inner join
deleted d
on u.mobile_no = d.mobile_no
inner join
inserted i
on
i.PKCol = d.PKCol --What's the PK of user_tbl?
END
END;
inserted and deleted are pseudo-tables that contain the new and old rows that were affected by a particular statement, and have the same schema as the original table. They're only accessible from within the trigger.
Note, also, that the above trigger is correct, even when multiple rows are updated in user_tbl - provided you can correctly relate inserted and deleted in the final ON clause.
You can get the old phone number from the table deleted and the new one from inserted, but you should use user primary key the update the rows.

SQL Insert, Update Trigger - Can you update the inserted table?

I have an SQL Trigger FOR INSERT, UPDATE I created which basically does the following:
Gets a LineID (PrimaryID for the table) and RegionID From the Inserted table and stores this in INT variables.
It then does a check on joining tables to find what the RegionID should be and if the RegionID is not equal what it should be from the Inserted table, then it should update that record.
CREATE TRIGGER [dbo].[TestTrigger]
ON [dbo].[PurchaseOrderLine]
FOR INSERT, UPDATE
AS
-- Find RegionID and PurchaseOrderLineID
DECLARE #RegionID AS INT
DECLARE #PurchaseOrderLineID AS INT
SELECT #RegionID = RegionID, #PurchaseOrderLineID = PurchaseOrderLineID FROM Inserted
-- Find PurchaserRegionID (if any) for the Inserted Line
DECLARE #PurchaserRegionID AS INT
SELECT #PurchaserRegionID = PurchaserRegionID
FROM
(...
) UpdateRegionTable
WHERE UpdateRegionTable.PurchaseOrderLineID = #PurchaseOrderLineID
-- Check to see if the PurchaserRegionID has a value
IF #PurchaserRegionID IS NOT NULL
BEGIN
-- If PurchaserRegionID has a value, compare it with the current RegionID of the Inserted PurchaseOrderLine, and if not equal then update it
IF #PurchaserRegionID <> #RegionID
BEGIN
UPDATE PurchaseOrderLine
SET RegionID = #PurchaserRegionID
WHERE PurchaseOrderLineID = #PurchaseOrderLineID
END
END
The problem I have is that it is not updating the record and I'm guessing, it is because the record hasn't been inserted yet into the PurchaseOrderLine table and I'm doing an update on that. But can you update the row which will be inserted from the Inserted table?
The major problem with your trigger is that it's written in assumption that you always get only one row in INSERTED virtual table.
SQL Server triggers are statement-triggers not row-triggers. You have to take that fact into consideration.
Now if I understand correctly the logic behind this trigger then you need just one update statement in it
CREATE TRIGGER TestTrigger ON PurchaseOrderLine
FOR INSERT, UPDATE
AS
UPDATE l
SET RegionID = u.PurchaserRegionID
FROM PurchaseOrderLine l JOIN INSERTED i
ON l.PurchaseOrderLineID = i.PurchaseOrderLineID JOIN
(
SELECT PurchaseOrderLineID, PurchaserRegionID
FROM UpdateRegionTable -- !!! change this for your proper subquery
) u ON l.PurchaseOrderLineID = u.PurchaseOrderLineID
For this example I've created a fake table UpdateRegionTable. You have to change it to the proper query that returns PurchaseOrderLineID, PurchaserRegionID (in your code you replaced it with ...). Make sure that it returns all necessary rows, not one.
Here is SQLFiddle demo
I think the problem could be that you are making the update to PurchaceOrderLine inside the trigger that is monitoring updates to the same table as well. Try to alter the trigger to just monitor the inserts, than if this works, you can make some changes or break your trigger on two: one for inserts, another for updates.
This has been resolved. I resolved the problem by adding the trigger to another table as the IF #PurchaserRegionID IS NOT NULL was always false.

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.