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

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.

Related

If my for update trigger raise error, my update statement should fail

To ensure version control, I created a For Update trigger on my table. I have two tables. Account table, step one Second, the Account history table, which is utilized in the trigger, has a column called Version. If any of my columns are modified, I have Version+1 written in the column, and the old record from the Account table will be inserted in the Account history in the trigger. Additionally, I have a trigger new condition written. The newer version ought to be grated. version, If I run an update query on my main (Account) table to perform negative testing while keeping the older version, I get a trigger-defined error, but my update statement still updates the Account table, even though it shouldn't. I need to add transaction(BEGIN TRY BEGIN CATCH TRAN) on my update?, If my trigger fails my update statement should fail
ALTER TRIGGER tr_AccountHistory
ON account
FOR UPDATE
AS
BEGIN
SELECT old.column
FROM deleted
SELECT new.Version
FROM inserted
SELECT old.Version FROM deleted
IF #Old_Version >= #New_Version
BEGIN
RAISERROR ('Improper version information provided',16,1);
END
ELSE
BEGIN
INSERT INTO AccountHistory
(
insert column
)
VALUES
(
old.column
);
END
END
UPDATE account
SET id= 123456,
Version = 1
WHERE id =1
Instead of using RAISERROR, you should use THROW. This will respect XACT_ABORT and automatically rollback the transaction.
You also have other fatal flaws in your trigger:
It expects there to be exactly one row modified. It may be multiple or zero rows.
You have not declared any variables and are instead selecting back out to the client.
Either way, you should just join the inserted and deleted tables by primary key.
CREATE OR ALTER TRIGGER tr_AccountHistory
ON account
FOR UPDATE
AS
SET NOCOUNT ON;
IF NOT EXISTS (SELECT 1 FROM inserted) -- early bailout
RETURN;
IF EXISTS (SELECT 1
FROM inserted i
JOIN deleted d ON d.YourPrimaryKey = i.YourPrimaryKey
WHERE d.Version >= i.Version
)
THROW 50001, 'Improper version information provided', 1;
INSERT INTO AccountHistory
(
insert column
)
SELECT
columsHere
FROM deleted;

how to execute trigger when only one column changes

I have the following trigger, and I need that its only executed when one column value changes, is that possible?
ALTER TRIGGER [dbo].[TR_HISTORICO]
ON [dbo].[Tbl_Contactos]
AFTER UPDATE
AS
BEGIN
IF UPDATE (primerNombre) -- sólo si actualiza PRIMER NOMBRE
BEGIN
INSERT INTO [dbo].[Tbl_Historico] ([fecha],[idUsuario],[valorNuevo], [idContacto],[tipoHistorico] )
SELECT getdate(), 1, [dbo].[Encrypt]([dbo].[Decrypt](primerNombre)), [idContacto], 1
FROM INSERTED
END
END
The problem is the code is executed always even if another column changes
The problem is probably the way you are doing updates in your code. It may be updating every field and not only the one that changed.
In this case you need to check to see if there is a difference between the values in the inserted and deleted pseudo tables. Or fix your code so that it only updates what needs to be updated.
Comparing the value of primerNombre from the inserted and deleted tables
ALTER TRIGGER [dbo].[TR_HISTORICO] ON [dbo].[Tbl_Contactos]
AFTER UPDATE AS
BEGIN
INSERT INTO [dbo].[Tbl_Historico] ([fecha],[idUsuario],[valorNuevo], [idContacto],[tipoHistorico] )
SELECT getdate(), 1, [dbo].[Encrypt]([dbo].[Decrypt](i.primerNombre)), i.[idContacto], 1
FROM INSERTED i
inner join deleted d
on i.idContacto = d.idContacto
where i.primerNombre <> d.primerNombre
END
If primerNombre is nullable, the where will need to handle null comparisons as well.

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 Server : want a column to be nullable only for existing and newly created rows

Is it possible to make a column that can only be NULL from the beginning, but once it has been set to a non-null value then it can never be reset to NULL?
I'm guessing it might be possible with an update constraint using both the inserted and deleted table, but that seems somewhat complicated given the goal of the task.
Bonus question: Since it doesn't seem easy to do this, is this a sign of bad design (on the part of us / SQL Server)?
Since you're allowing the filed to be NULL when the ROW is Inserted, you'll have to use a Trigger for Update
CREATE TRIGGER trigger1 ON table1
FOR UPDATE
AS
BEGIN
SET NOCOUNT ON
DECLARE #field1 VARCAHR(MAX)
select #field1 = field1 FROM INSERTED
IF #field1 is NULL
BEGIN
RAISERROR('A NULL Values are not allowed', 15, 1)
ROLLBACK
END
END
"Allowing null value in a column initially, but once set to non null value, it must not allow null value for the same row"
This is clearly a business logic. It must be handled in controller part of MVC. Trying to handle it in database layer will break the MVC principle, and thus is a bad design.
If you still want to handle it in DBMS, then, as suggested in other answers, you can use triggers.
The trigger may look like this. You join INSERTED on DELETED to check if column was NOT NULL and became NULL:
CREATE TRIGGER trCheckForNulls ON SomeTable
AFTER UPDATE
AS
BEGIN
IF UPDATE(SomeColumn)
BEGIN
IF EXISTS(
SELECT * FROM INSERTED i
JOIN DELETED d ON i.PK = d.PK
WHERE d.SomeColumn IS NOT NULL AND i.SomeColumn IS NULL)
BEGIN
RAISERROR('SomeError', 16, 1)
ROLLBACK
END
END
END

how to create before update trigger in 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