SQL Server before trigger - sql-server

I need to control table values uniqueness. It cannot be done by an index or a constraint (error message must show data from another table). I thought of after trigger but since it fires after the insert the below trigger will fire even if values are unique.
--table
CREATE TABLE Names (Id IDENTITY(1,1) NOT NULL, Name VARCHAR(20) NOT NULL)
--first record
INSERT INTO Names VALUES ('John')
--trigger
CREATE TRIGGER [dbo].[Names_Insert_Trigger]
ON [dbo].[Names]
FOR INSERT, UPDATE
AS
SET NOCOUNT ON
IF EXISTS (SELECT Name
FROM inserted
WHERE EXISTS (SELECT * FROM Names N JOIN inserted ON N.Name=inserted.Name))
BEGIN
RAISERROR('This name is already registered in file XYZ.', 16, 1)
ROLLBACK TRAN
SET NOCOUNT OFF
RETURN
END
SET NOCOUNT OFF
--I add another record with different value and the trigger fires
INSERT INTO Names VALUES ('Steven')
I also thought of an instead of insert trigger but the actual table has identity set and will likely get new columns in the future which would require updating the trigger code at each change so I can't use the below code:
CREATE TRIGGER [dbo].[Names_Insert_Trigger]
ON [dbo].[Names]
INSTEAD OF INSERT
AS
SET NOCOUNT ON
IF EXISTS (SELECT Name
FROM inserted
WHERE EXISTS (SELECT * FROM Names N JOIN inserted ON N.Name=inserted.Name))
BEGIN
RAISERROR('This name is already registed in file XYZ.', 16, 1)
ROLLBACK TRAN
SET NOCOUNT OFF
RETURN
END
ELSE
INSERT INTO Names
SELECT * FROM inserted
SET NOCOUNT OFF
Any ideas how to solve it?
Regards,
Przemek

You can use an after trigger. Just use COUNT instead of EXISTS. You should still have a non-unique index on name to optimize performance and concurrency.
IF (SELECT COUNT(*)
FROM inserted AS i
JOIN dbo.Names AS N ON
N.Name = i.Name
GROUP BY N.Name
) > 1
BEGIN
RAISERROR...
END;

The real solution is to use the UNIQUE constraint to this problem, it's designed to solve it and it's much more performant and safer than a trigger for this usage. The error message is better built client-side and ignore the server genereated one, save for determining the exact reason.
But if you really want to follow the trigger route, use the AFTER version, but fix the query for detect duplicates:
SELECT * FROM (
SELECT Name
FROM Names
WHERE id NOT IN (SELECT id FROM inserted)
) PreviousNames
INNER JOIN inserted ON PreviousNames.Name = inserted.Name
(I'm just showing the query to check duplication that goes into the IF EXIST instruction, not the whole trigger).
It begins by creating a subquery that gets the names NOT being inserted (so that you don't get a false positive), then simply joins again to inserted to check if any value is in both tables.
There is an additional problem that can happen when using SNAPSHOT issolation level. In this mode, the trigger will NOT see the changes made by other connections, nor they'll be blocked until the trigger ends. I'm not quite familiar with the details, but will leave this article as reference and possible solutions: https://sqlserverfast.com/?s=snapshot+integrity

Related

Trigger for both insert and update

I'm trying to create a trigger that will prevent a user from inserting to or updating the quantity in my orderLines table if the amount is greater than the quantity on the products table.
Is there a way to do it in a single trigger or do I have to create to separate ones for both insert and update actions?
Below is how my trigger starts:
CREATE TRIGGER OrdersLines_ITrig
ON ordersLines
FOR INSERT
AS
Depends on the BEGIN/END blocks:
Triggers have special INSERTED and DELETED tables to track "before" and "after" data. So you can use something like IF EXISTS (SELECT * FROM DELETED) to detect an update. You only have rows in DELETED on update, but there are always rows in INSERTED.
CREATE TRIGGER dbo.TriggerName
ON dbo.TableName
AFTER INSERT, UPDATE
AS
BEGIN
SET NOCOUNT ON;
IF EXISTS (SELECT * FROM inserted) AND EXISTS (SELECT * FROM deleted)
BEGIN
----Do update
END;
IF EXISTS (SELECT * FROM Inserted) AND NOT EXISTS (SELECT * FROM deleted)
BEGIN
-----Do insert
END;
END
This is a simple requirement you can handle using CHECK CONSTRAINT itself. If you are defining trigger, you have to properly rollback the transaction. Have proper error message etc. You can simply have a check constraint, which will do all these things for you.
I would suggest you to do below steps:
Create a user defined function, which returns TRUE or FALSE, based on the quantity in the Product table.
CREATE FUNCTION CheckQuantity(#productID INT)
RETURNS BIT
AS
BEGIN
---LOGIC
END
Leverage the user defined function in the CHECK constraint.
ALTER TABLE OrderLines ADD CONSTRAINT CHK_Quantity CHECK( dbo.CheckQuantity(ProductId) = = 1)

SQL Server: Capturing All the columns that have changed in a separate table

In my SQl Server I have a table of around 40 attributes/columns. There is a daily load which might update any of these columns. I want to capture the changes in these columns in a separate table with a reason code column telling which column value changed. There might be instances where more than one column value might get changed in a single daily load, in that case the changed log table should capture all these changes separately in rows with each row depicting the individual change.
For Example:
TableA(column1(pk),column2,column3,column4)
values(1,100,ABC,999)
After update:
TableA(column1(pk),column2,column3,column4)
values(1,100,ACD,901)
The corresponding change log table should have two entries:
TabChangeLog(column1,before,after,reason);
values(1,ABC,ACD,'column3 changed')
values(1,999,901,'column4 changed')
I tried implementing this through triggers but am not able to figure out a way to separate each of these changes in separate rows when there are more than one changes. Please help
You need to create a trigger like :
create trigger trigger_name
instead of update as
if update(column1)
begin
insert into TabChangeLog
select inserted.column1, inserted.column3, deleted.column3, 'column3', 'update/change'
from inserted i inner join deleted d
on i.column1 = d.column2
end
if update(column2)
begin
insert into TabChangeLog
select inserted.column1, inserted.column2, deleted.column2, 'column2', 'update/change'
from inserted i inner join deleted d
on i.column1 = d.column2
end
...
https://www.tutorialgateway.org/instead-of-update-triggers-in-sql-server/
Microsoft SQL Server 2016 has a thing called Temporal Tables which would probably simplify your job a lot. It lets you rewind a dataset through time to see the changes:
https://learn.microsoft.com/en-us/sql/relational-databases/tables/temporal-tables?view=sql-server-2017
If you don't want to go that route and use triggers instead. UPDATE triggers have two tables inserted and deleted that let you know what the row state was before and after.
*Edit: These are tables so you have to use SELECT INTO etc to interact with them you can't do conditional logic (if /else)
CREATE TABLE [dbo].[Table1](
[Id] [int] NOT NULL,
[Tail] [int] NOT NULL,
CONSTRAINT [PK_Table1_1] PRIMARY KEY CLUSTERED
(
[Id] ASC
)
)
CREATE TABLE Table1_Audit
(
Audit varchar(100)
)
--drop trigger Table1_OnUPDATE
CREATE TRIGGER Table1_OnUPDATE
ON dbo.Table1
AFTER UPDATE
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
-- Insert statements for trigger here
INSERT INTO Table1_Audit ([Audit])
select CONCAT('Tail changed to' ,inserted.Tail,' for pk Id=',inserted.Id) from inserted inner join
deleted on inserted.Id = deleted.Id --pk must be the same
where
inserted.Tail <> deleted.Tail --field x must be different
END
GO
--truncate table Table1_Audit
--update Table1 set Tail = 5
select * from Table1_Audit

SQL AFTER INSERT,UPDATE Trigger with condition if update specific column

I would like to create a trigger that inserts new datasets into the FREE08 table (see example code) either after insert on FREE03. This part works. But i also want to insert new dataset into FREE08 only if specific column (FK2) of FREE03 is updated. Thought this works with the "IF UPDATE(FK2) statement in the trigger.
What happens is i got a new dataset in FREE08 everytime i have any update in FREE03.
How can i get that?
Thanks for your support
USE [DB]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TRIGGER [dbo].[Einsatzhistorie Wechsler]
ON [dbo].[FREE03]
AFTER INSERT,UPDATE
AS
SET NOCOUNT ON;
IF UPDATE(FK2)
BEGIN
INSERT INTO FREE08
(
FK1,FK2,TEXT3,DATE1,TEXT2,DATE4
)
Select FK2,FK1,ID,DATE1,TEXT1,DATE11 From inserted
end
I've never found a real use for the UPDATE function, since it tells you whether a column was subject to UPDATE (I.e. appeared in the SET clause), not whether data actually changed. In addition, of course, triggers fire once per statement, not once per row, so your trigger may be dealing with a mixture of rows, some of which had their FK2 changed and some not.
It's usually better to use deleted and compare the previous/current values:
CREATE TRIGGER [dbo].[Einsatzhistorie Wechsler]
ON [dbo].[FREE03]
AFTER INSERT,UPDATE
AS
SET NOCOUNT ON;
INSERT INTO FREE08
(
FK1,FK2,TEXT3,DATE1,TEXT2,DATE4
)
Select i.FK2,i.FK1,i.ID,i.DATE1,i.TEXT1,i.DATE11
From inserted i
left join deleted d
on
i.ID = d.ID and
i.FK2 = d.FK2
where d.ID is null
Here I've assumed a) that ID represents an immutable primary key for the table and b) That FK2 isn't nullable.

Create a trigger that won't let updating the primary key columns

I have a table that has a composite primary key made from 3 columns, let's say A, B, C. I want to create a trigger that on UPDATE will check that these three columns won't be changed. This is what I have so far, but it doesn't seem to work:
CREATE TRIGGER TableTrigger
ON Table
AFTER INSERT, UPDATE AS
BEGIN
IF (EXISTS (SELECT * FROM inserted) AND EXISTS (SELECT * FROM deleted))
BEGIN
-- Update Operation
IF (SELECT COUNT(*) FROM inserted WHERE A IS NOT NULL OR B IS NOT NULL OR C IS NOT NULL) > 0
BEGIN
RAISERROR('Error, you cannot change Primary Key columns', 16, 1)
ROLLBACK
RETURN
END
END
I was expecting that if I update some values in a table, in inserted the values for the columns I don't update to be NULL, but it's not like that. I read somewhere that I need to look both in inserted and deleted to see if these values changed. So my question is this, can I check this without using a cursor?
Thank you.
You could do
CREATE TRIGGER TableTrigger
ON Table
AFTER UPDATE AS
BEGIN
IF UPDATE(A) OR UPDATE(B) OR UPDATE(C)
BEGIN
RAISERROR('Error, you cannot change Primary Key columns', 16, 1)
ROLLBACK
RETURN
END
END
Or deny update permissions on those columns.
Both approaches would deny any attempt to update the PK columns irrespective of whether or not the values actually change. SQL Server does not have row level triggers and unless there is an IDENTITY column in the table (guaranteed immutable) there is no reliable way to tell in a trigger if the PK was actually updated.
For example the INSERTED and DELETED tables in an UPDATE trigger on the table below would be identical for both the UPDATE statements.
CREATE TABLE T(C INT PRIMARY KEY);
INSERT INTO T VALUES (1),(-1)
/*Both values swapped*/
UPDATE T SET C = -C
/*Both values left the same*/
UPDATE T SET C = C

Creating a trigger on SQL Server 2008 R2 to catch a record update

Using Great Plains here and one of our users keeps screwing up customer data so we want to put a trigger on the customer table and so we can find out who it is.
Anyway, I created a table called audit_RM00101 as follows:
DATE nchar(10)
CUSTNMBR char(15)
CUSTNAME char(65)
UPSZONE char(3)
SALSTERR char(15)
USERID nchar(100)
I want to capture those same fields from the table I want to audit so I wrote the trigger as follows:
CREATE TRIGGER CatchCustomerRegionUpdate
ON RM00101
FOR UPDATE
AS
DECLARE #UserID VARCHAR(128)
SELECT #UserID = system_user
INSERT INTO audit_RM00101
SELECT DATE, CUSTNMBR, CUSTNAME, UPSZONE, SALSTERR, #UserID FROM UPDATED
The trigger gets created just fine but when I try to test it by updating a customer record in Great Plains, Great Plains throws up an ugly error and the trigger doesn't get fired.
What am I doing wrong here?
Thanks.
in a trigger, you get the DELETED and INSERTED tables, there is no UPDATED, so replace FROM UPDATED with FROM INSERTED
also try to fix your USERID column, your audit_RM00101.USERID is a nchar(100) while #UserID is a VARCHAR(128).
EDIT based on OPs comment: Ah, so there is no way to audit when a table is updated by using a trigger?
in a trigger when deleting, DELETED is populated, but INSERTED is empty
in a trigger when updating, DELETED is populated with the original value, and INSERTED is populated with the newly updated values
in a trigger when inserting, DELETED is empty, but INSERTED has the newly inserted values
There is no UPDATED in SQL Server; just inserted and deleted.
Also, it makes sense to add IF ##ROWCOUNT = 0 RETURN in the very beginning of triger's body.
When UPDATE takes place, both inserted and deleted tables are not empty. You may add the following code to make sure you handle UPDATE, not insert/delete:
IF EXISTS(SELECT * FROM inserted) AND EXISTS (SELECT * FROM deleted)
BEGIN
-- handle update
END ;
It's not really important for your trigger because you specify just FOR UPDATE, it would be important if you had, for instance, FOR UPDATE, INSERT, DELETE.
we have only two magic tables called INSERTED and DELETED
update indirectly is a Delete statement followed by Insert statement. so you have to update the column's value which is present in INSERTED.
CREATE TRIGGER CatchCustomerRegionUpdate
ON RM00101
AFTER UPDATE
AS
BEGIN
DECLARE #INSERTED INT, #DELETED INT
SET #INSERTED = SELECT COUNT(*) FROM INSERTED
SET #DELETED = SELECT COUNT(*) FROM DELETED
IF #INSERTED = 1 AND #DELETED = 1
BEGIN
UPDATE TABLE1
SET COL1 = INSERTED_COL1
WHERE IDCOL = INSERTED_IDCOL
END
END

Resources