Trigger for both insert and update - sql-server

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)

Related

Issue with Trigger

I am working with an insert trigger and work fine. I am creating an insert trigger and take a backup in tblHist table.
I have two tables:
tblUser - creating this table for insert,update and delete purpose
tblHist - creating this table for store a record for history purpose
tblUser table design:
tblHist table design:
Then I create an insert and update trigger:
ALTER TRIGGER [dbo].[trgr_tblUser_AFTERINSERT]
ON [dbo].[tblUser]
AFTER INSERT, UPDATE
--,DELETE
AS
BEGIN
DECLARE #userid int, #username varchar(50),
#useraddress varchar(50), #countryname varchar(5),
#statename varchar(50), #cityname varchar(50);
BEGIN
SELECT
#userid = u.userid, #username = u.username,
#useraddress = u.useraddress,
#countryname = u.countryname,
#statename = u.statename, #cityname = u.cityname
FROM tblUser u;
INSERT INTO tblHist (userid, username, useraddress, countryname, statename, cityname)
VALUES (#userid, #username, #useraddress, #countryname, #statename, #cityname);
PRINT 'AFTER INSERT update trigger fired.'
END
END
When I insert a record into the tblUser table, then it inserts a record into the tblHist table - this is working fine.
See below
Then I update a record then insert a history in tblHist table working fine.
but issue is when I add a code for delete a record functionality in trgr_tblUser_AFTERINSERT then delete functionality not work
And when I create a delete trigger separately then work fine
See below
ALTER TRIGGER [dbo].[trgr_tblUser_AFTERDELETE]
ON [dbo].[tblUser]
FOR DELETE
AS
BEGIN
DECLARE #userid int, #username varchar(50),
#useraddress varchar(50), #countryname varchar(5),
#statename varchar(50), #cityname varchar(50);
SELECT
#userid = u.userid, #username = u.username,
#useraddress = u.useraddress, #countryname = u.countryname,
#statename = u.statename, #cityname = u.cityname
FROM deleted u;
INSERT INTO tblHist (userid, username, useraddress, countryname, statename, cityname)
VALUES (#userid, #username, #useraddress, #countryname, #statename, #cityname);
PRINT 'AFTER DELETE trigger fired.'
END
I want to add insert, update, delete trigger functionality In one trigger but not work.
What I am trying:
exist select 1 --- but not work
which place I am doing wrong need help
I highly doubt that your first trigger will work properly .... you're just selecting an arbitrary rows from your tblUser table - not even one that's necessarily just been inserted or updated ....
I would strongly recommend these changes:
creating a separate trigger for each operation - that makes the trigger simpler, since you don't need to first figure out what you're dealing with....
add a ModifiedOn DATETIME2(3) column to your tblHist to record the date & time stamp when the change occurred
also possibly add an Operation column to your tblHist - so that you can understand what operation (insert, update, delete) caused this entry in the history table
properly handle the Inserted and Deleted pseudo tables in your trigger code taking into account they can (and will!) contain multiple rows - handle them in a proper, set-based fashion
drop the PRINT - makes no sense inside a trigger....
Code would be something like:
CREATE OR ALTER TRIGGER dbo.trgr_tblUser_AfterInsert
ON dbo.tblUser
AFTER INSERT
AS
BEGIN
-- do an "INSERT INTO" ...
INSERT INTO tblHist (ModifiedOn, userid, username, useraddress, countryname, statename, cityname)
-- based on the "Inserted" pseudo table, and use proper set-based approach
SELECT
SYSDATETIME(),
i.userid, i.username, i.useraddress, i.countryname, i.statename, i.cityname
FROM
Inserted i;
END
END
and
CREATE OR ALTER TRIGGER dbo.trgr_tblUser_AfterDelete
ON dbo.tblUser
AFTER DELETE
AS
BEGIN
INSERT INTO tblHist (ModifiedOn, userid, username, useraddress, countryname, statename, cityname)
SELECT
SYSDATETIME(),
d.userid, d.username, d.useraddress, d.countryname, d.statename, d.cityname
FROM
Deleted d;
END
END
If you want to check whether it was an insert, update or delete, you have to examine the row count of the inserted and deleted pseudotables - for an insert, records are only present in inserted, for delete they are only present in deleted. An update has both so you can tell the old values (deleted) and the new (inserted)
Make your life easy; put twice as many columns in your hist (should be called UserHist, no?) table as your user table, and make them e.g. old_username, new_username.. an insert the result of a full outer join between the inserted and deleted tables, this way you can tell if it was an insert, update or delete and particularly for an insert, what changed to what
Alternatively, use something like
IF EXISTS(SELECT null FROM inserted)
IF EXISTS(SELECT null FROM deleted)
--it was an update
ELSE
--it was an insert
END;
ELSE
--it was a delete
END;
Or, make 3 separate triggers
Final point of note, you're doing these queries wrong - you're declaring a bunch of variables (that can only hold a single value)and selecting the values from inserted/deleted into them but those pseudotables can have more than one row, if the query affected multiple rows (such as DELETE FROM user WHERE name = 'John')
You should be doing your operations in a bunch-of-rows way, not a "single row" way:
INSERT INTO tblHist
SELECT * FROM inserted
This can insert multiple rows into hist, and this is the way you should always think about doing things in SQLServer.. Even if you only ever insert one row and your inserted pseudotable has one row, you must get into the habit of treating it as "a collection of rows with one entry" so that any code you write won't fall apart when one day it becomes "a collection of rows with multiple entries"
Note in one of your attempts you did not select from inserted, you selected from the users table - this is wrong:
Of course, an AFTER delete trigger will insert nothing if you just deleted the only row from tblusers, but you shouldn't be using tblusers anyway

MSSQL trigger or constraint to prevent or allow update/delete based on column value

I have a table of Customers, and I want to prevent update/insert when the row has Status column that is anything other than 1.
What would be the best way to create that kind of functionality direstly on server?
You can create a stored procedure and force its usage to update the table.
create procedure updateCustomers #custID int,#newValue nvarchar(50)
with execute as superUser
as
update customers
set someColumn=#newValue
where custID=#custID and status=1
1) If you want to prevent any INSERT/UPDATE action when the new Status is diff. that 1 then following trigger will prevent such actions. Anyways, this requirement makes no sense because it's much simple to use a check constraint thus: ALTER TABLE dbo.Customer ADD CONSTRAINT ... CHECK ([Status] <> 1)
CREATE TRIGGER dbo.trgIU_Customer_PreventIU
ON dbo.Customer
AFTER INSERT, UPDATE
AS
BEGIN
SET NOCOUNT ON
IF EXISTS(SELECT * FROM inserted i WHERE i.[Status] IS NOT NULL AND i.[Status] <> 1)
BEGIN
RAISERROR('Can''t insert or update rows with Status <> 1', 16, 1)
ROLLBACK
END
END
2) But if you want to simply avoid any update when current [Status] is 1 then following trigger could be used:
CREATE TRIGGER dbo.trgU_Customer_PreventU
ON dbo.Customer
AFTER UPDATE
AS
BEGIN
SET NOCOUNT ON
IF EXISTS(SELECT * FROM deleted d WHERE d.[Status] = 1)
BEGIN
RAISERROR('Can''t update rows with Status = 1', 16, 1)
ROLLBACK
END
END
You can try like this:
CREATE TRIGGER [dbo].[PreventUpdate]
ON [dbo].[Customers]
INSTEAD OF UPDATE
AS
BEGIN
SET NOCOUNT ON;
IF EXISTS
(
SELECT 1 FROM Customers c
WHERE c.Status <> 1 )
BEGIN
RAISERROR(...);
END
ELSE
BEGIN
--You update code
END
END
GO
What would be the best way
Do not use a trigger for this purpose. Triggers exist to enforce referential integrity, and using them for other purposes will lead to tears.
Deny users privileges on the table, and provide stored procedures instead, such as doublet28 suggests. That gives you the opportunity to provide a meaningful error message, and leaves the DBA free to update the table without tripping on the trigger (which will be needed sooner than later).

SQL Server before trigger

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

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