How to write a sql trigger correctly - sql-server

I want to add a trigger and I never wrote one.
When I update or add a new values to my table, I want to check that some value is allowed.
I started the trigger but I don't really know how to end it:
CREATE TRIGGER isAguide
ON tblDiving
AFTER INSERT, UPDATE AS
BEGIN
DECLARE #Found bit
select #Found = count(*)
from tblAuthorized
WHERE diver_number = #id
and level_name = 'guide'
I want to allow adding ONLY if #found=1.
#id is the value that the user is trying to add to the table.
So first, how do I get the value the user entered in this 'id' Column?
And second, How do I continue the trigger I wrote?
Is it ok I chose AFTER INSERT?
I want to add something like:
if(#found=1) THEN allow adding/updating
else don't allow.
Ty

If you're using an AFTER trigger you can throw an error and roll back the transaction if the id is not found.
CREATE TRIGGER isAguide
ON tblDiving AFTER INSERT, UPDATE AS
BEGIN
DECLARE #Found bit, #id int;
SET #id = SELECT id FROM inserted;
SET #Found =
(
SELECT count(*)
FROM tblAuthorized
WHERE diver_number = #id
AND level_name = 'guide'
);
IF #Found = 0
THEN
RAISERROR ('The ID entered is not valid', 16, 1);
ROLLBACK TRANSACTION;
RETURN
END;
END;

Instead of writing a trigger I'd recommend to create a separate table "Guides" with the diver number in there as the only column (being a primary key).
Add a foreign key reference to the related Id column of your tblDiving table.
Not only will this be compliant with a normalized data structure, it'll also be faster.
Finally you'll need to consider the scenario of changing the level_name of a diver_number from 'guide' to something different after there are some rows inserted into the tblDiving table. How do you want the app to react?

Related

Trigger AFTER INSERT, UPDATE, DELETE to call stored procedure with table name and primary key

For a sync process, my SQL Server database should record a list items that have changed - table name and primary key.
The DB already has a table and stored procedure to do this:
EXEC #ErrCode = dbo.SyncQueueItem "tableName", 1234;
I'd like to add triggers to a table to call this stored procedure on INSERT, UPDATE, DELETE. How do I get the key? What's the simplest thing that could possibly work?
CREATE TABLE new_employees
(
id_num INT IDENTITY(1,1),
fname VARCHAR(20),
minit CHAR(1),
lname VARCHAR(30)
);
GO
IF OBJECT_ID ('dbo.sync_new_employees','TR') IS NOT NULL
DROP TRIGGER sync_new_employees;
GO
CREATE TRIGGER sync_new_employees
ON new_employees
AFTER INSERT, UPDATE, DELETE
AS
DECLARE #Key Int;
DECLARE #ErrCode Int;
-- How to get the key???
SELECT #Key = 12345;
EXEC #ErrCode = dbo.SyncQueueItem "new_employees", #key;
GO
The way to access the records changed by the operation is by using the Inserted and Deleted pseudo-tables that are provided to you by SQL Server.
Inserted contains any inserted records, or any updated records with their new values.
Deleted contains any deleted records, or any updated records with their old values.
More Info
When writing a trigger, to be safe, one should always code for the case when multiple records are acted upon. Unfortunately if you need to call a SP that means a loop - which isn't ideal.
The following code shows how this could be done for your example, and includes a method of detecting whether the operation is an Insert/Update/Delete.
declare #Key int, #ErrCode int, #Action varchar(6);
declare #Keys table (id int, [Action] varchar(6));
insert into #Keys (id, [Action])
select coalesce(I.id, D.id_num)
, case when I.id is not null and D.id is not null then 'Update' when I.id is not null then 'Insert' else 'Delete' end
from Inserted I
full join Deleted D on I.id_num = D.id_num;
while exists (select 1 from #Keys) begin
select top 1 #Key = id, #Action = [Action] from #Keys;
exec #ErrCode = dbo.SyncQueueItem 'new_employees', #key;
delete from #Keys where id = #Key;
end
Further: In addition to solving your specified problem its worth noting a couple of points regarding the bigger picture.
As #Damien_The_Unbeliever points out there are built in mechanisms to accomplish change tracking which will perform much better.
If you still wish to handle your own change tracking, it would perform better if you could arrange it such that you handle the entire recordset in one go as opposed to carrying out a row-by-row operation. There are 2 ways to accomplish this a) Move your change tracking code inside the trigger and don't use a SP. b) Use a "User Defined Table Type" to pass the record-set of changes to the SP.
You should use the Magic Table to get the data.
Usually, inserted and deleted tables are called Magic Tables in the context of a trigger. There are Inserted and Deleted magic tables in SQL Server. These tables are automatically created and managed by SQL Server internally to hold recently inserted, deleted and updated values during DML operations (Insert, Update and Delete) on a database table.
Inserted magic table
The Inserted table holds the recently inserted values, in other words, new data values. Hence recently added records are inserted into the Inserted table.
Deleted magic table
The Deleted table holds the recently deleted or updated values, in other words, old data values. Hence the old updated and deleted records are inserted into the Deleted table.
**You can use the inserted and deleted magic table to get the value of id_num **
SELECT top 1 #Key = id_num from inserted
Note: This code sample will only work for a single record for insert scenario. For Bulk insert/update scenarios you need to fetch records from inserted and deleted table stored in the temp table or variable and then loop through it to pass to your procedure or you can pass a table variable to your procedure and handle the multiple records there.
A DML trigger should operate set data else only one row will be processed. It can be something like this. And of course use magic tables inserted and deleted.
CREATE TRIGGER dbo.tr_employees
ON dbo.employees --the table from Northwind database
AFTER INSERT,DELETE,UPDATE
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
declare #tbl table (id int identity(1,1),delId int,insId int)
--Use "magic tables" inserted and deleted
insert #tbl(delId, insId)
select d.EmployeeID, i.EmployeeID
from inserted i --empty when "delete"
full join deleted d --empty when "insert"
on i.EmployeeID=d.EmployeeID
declare #id int,#key int,#action char
select top 1 #id=id, #key=isnull(delId, insId),
#action=case
when delId is null then 'I'
when insId is null then 'D'
else 'U' end --just in case you need the operation executed
from #tbl
--do something for each row
while #id is not null --instead of cursor
begin
--do the main action
--exec dbo.sync 'employees', #key, #action
--remove processed row
delete #tbl where id=#id
--refill #variables
select top 1 #id=id, #key=isnull(delId, insId),
#action=case
when delId is null then 'I'
when insId is null then 'D'
else 'U' end --just in case you need the operation executed
from #tbl
end
END
Not the best solution, but just a direct answer on the question:
SELECT #Key = COALESCE(deleted.id_num,inserted.id_num);
Also not the best way (if not the worst) (do not try this at home), but at least it will help with multiple values:
DECLARE #Key INT;
DECLARE triggerCursor CURSOR LOCAL FAST_FORWARD READ_ONLY
FOR SELECT COALESCE(i.id_num,d.id_num) AS [id_num]
FROM inserted i
FULL JOIN deleted d ON d.id_num = i.id_num
WHERE (
COALESCE(i.fname,'')<>COALESCE(d.fname,'')
OR COALESCE(i.minit,'')<>COALESCE(d.minit,'')
OR COALESCE(i.lname,'')<>COALESCE(d.lname,'')
)
;
OPEN triggerCursor;
FETCH NEXT FROM triggerCursor INTO #Key;
WHILE ##FETCH_STATUS = 0
BEGIN
EXEC #ErrCode = dbo.SyncQueueItem 'new_employees', #key;
FETCH NEXT FROM triggerCursor INTO #Key;
END
CLOSE triggerCursor;
DEALLOCATE triggerCursor;
Better way to use trigger based "value-change-tracker":
INSERT INTO [YourTableHistoryName] (id_num, fname, minit, lname, WhenHappened)
SELECT COALESCE(i.id_num,d.id_num) AS [id_num]
,i.fname,i.minit,i.lname,CURRENT_TIMESTAMP AS [WhenHeppened]
FROM inserted i
FULL JOIN deleted d ON d.id_num = i.id_num
WHERE ( COALESCE(i.fname,'')<>COALESCE(d.fname,'')
OR COALESCE(i.minit,'')<>COALESCE(d.minit,'')
OR COALESCE(i.lname,'')<>COALESCE(d.lname,'')
)
;
The best (in my opinion) way to track changes is to use Temporal tables (SQL Server 2016+)
inserted/deleted in triggers will generate as many rows as touched and calling a stored proc per key would require a cursor or similar approach per row.
You should check timestamp/rowversion in SQL Server. You could add that to the all tables in question (not null, auto increment, unique within database for each table/row etc).
You could add a unique index on that column to all tables you added the column.
##DBTS is the current timestamp, you can store today's ##DBTS and tomorrow you will scan all tables from that to current ##DBTS. timestamp/rowversion will be incremented for all updates and inserts but for deletes it won't track, for deletes you can have a delete only trigger and insert keys into a different table.
Change data capture or change tracking could do this easier, but if there is heavy volumes on the server or large number of data loads, partition switches scanning the transaction log becomes a bottleneck and in some cases you will have to remove change data capture to save the transaction log from growing indefinetely.

SQL Trigger not updating field

When i select a value in one table, the same value must populate the other table.
For example:
Please see image attached:
The problem i am having is when i update the first field stage (left hand side), to say Stage2, the uStage field stays the same.. It should update to Stage2 as well..
The Ustage table has the same stage values as the Stage table so I know it needs to select the corresponding Value however i have tried all sorts to get this to work but it doesnt want to update:
Here is my Trigger code:
USE [SKY]
GO
/****** Object: Trigger [dbo].[SetIT] Script Date: 2018/10/04 19:52:06 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER TRIGGER [dbo].[SetIT] ON [dbo].[AMGR_Opportunity_Tbl]
AFTER INSERT
AS
DECLARE #RecordId int
DECLARE #Stage varchar(750)
Declare #ID int
begin
--check to see if we have any records in the inserted set
IF EXISTS( SELECT * FROM inserted ) BEGIN
--set up the cursor that we use to iterate over the recordset
DECLARE I CURSOR LOCAL FAST_FORWARD FOR
SELECT Record_Id FROM Inserted;
OPEN I
FETCH NEXT FROM I INTO #RecordId;
WHILE ##FETCH_STATUS = 0 BEGIN
SELECT #Stage = STAGE FROM STAGE WHERE ID = #ID;
--insert the UDF value
INSERT INTO O_Ustage(Client_id,Contact_Number,O_Ustage)
SELECT Opp_Id, 0, #Stage
FROM inserted WHERE Record_Id = #RecordId AND Opp_Type = 0;
--update the current stage
UPDATE stage set stage=#stage WHERE
ID=#ID;
FETCH NEXT FROM I INTO #RecordId;
END
CLOSE I
DEALLOCATE I
END
END
GO
Please assist in helping me figure out how to update the USTAGE field.
Below is the data that is in the Stage table
If i change the field 'Stage' below, then 'Ustage' also needs to change.. At the moment it is not doing it:
The opportunity table has 50 columns and stage table 2 columns.
When I change the stage on the opportunity table to Stage2 then the Ustage value must also change to Stage2.. Both values need to correspond
Your trigger code has a lot of problems, but here's one that will definitely cause it not to work:
SELECT #Stage = STAGE FROM STAGE WHERE ID = #ID;
Prior to that line of code, you never populate #ID, so it's value will be NULL. Therefore you will get NULL for #Stage, which is what you later insert into the O_UStage column.
Maybe you meant to use #RecordId on that line instead of #ID?

How to "INSERT or SELECT" in SQL Server 2012 using fewest operations?

I've been searching around for a question similar to this for awhile now, and I haven't found anything, so if this has been asked before, this will at least serve as a good pointer for those ignorant of the proper nomenclature.
I want to INSERT INTO a table if a row doesn't already exist, based on a unique key. It it does exist, then I want to get the primary key Id of that row.
Imagine a table that holds email addresses:
EmailAddressId(PK) | EmailAddress(UK)
I want to INSERT into that table a new Email Address, but there is a unique constraint on EmailAddress. Thus, if the new Email Address is the same as an existing, the INSERT will fail. In that case, I want to select the existing EmailAddressId from the database for the EmailAddress.
I want to do this in the fewest number of operations, assuming that collisions will be a rare case.
Thus, I setup a TRY...CATCH block within a Stored Procedure as follows:
ALTER PROCEDURE [dbo].[EmailAddressWrite]
#EmailAddress nvarchar[256]
BEGIN
SET NOCOUNT ON;
BEGIN TRANSACTION
DECLARE #EmailAddressId INT
BEGIN TRY
INSERT INTO EmailAddress VALUES (#EmailAddress)
SET #EmailAddressId = (SELECT SCOPE_IDENTITY())
END TRY
BEGIN CATCH
SET #EmailAddressId = (SELECT EmailAddressId FROM EmailAddress WHERE EmailAddress = #EmailAddress)
END CATCH
--Do some more stuff with the Id now.
COMMIT TRANSACTION
RETURN #EmailAddressId
END
The code above functions, and produces the required result, but the Internet makes me think that using TRY...CATCH in this fashion might be slow...thus I'm unsure if this is an optimal solution.
I've only found one other solution which is to SELECT first, and INSERT second. This would result in 2 operations almost all of the time, as I am anticipating very few duplicate email addresses (at least for a month or more).
Is this the optimal solution to achieve 1 operation on INSERT and 2 operations on INSERT fail?
What other solutions can achieve 1 operation on INSERT and 2
operations on INSERT fail?
If I've misused any terminology, please correct it.
DECLARE #id INT
DECLARE #newid TABLE
(
emailAddressId INT NOT NULL PRIMARY KEY
)
;
WITH t AS
(
SELECT *
FROM emailAddress WITH (ROWLOCK, HOLDLOCK)
WHERE emailAddress = #emailAddress
)
MERGE
INTO t
USING (
SELECT #emailAddress
) s (emailAddress)
ON 1 = 1
WHEN NOT MATCHED BY TARGET THEN
INSERT (emailAddress)
VALUES (emailAddress)
WHEN MATCHED THEN
UPDATE
SET #id = 1
OUTPUT INSERTED.emailAddressId
INTO #newid
;

Updating the table in a trigger written in sql server

I am trying to create a trigger which is triggered when an insert, delete happens in a table 'Abschaetzung_has_Varianten' and updates a table called 'Flag'. I need to select an ID from the same table to update the Flag table. Is the syntax wrong in writing the SELECT of the ID? I don't seem to get the #abschID from the select. Could anyone help me in this. Thank you.
CREATE TRIGGER trig_update_flag on [Abschaetzung_has_Varianten]
after insert, delete
As
Begin
DECLARE #x INT;
DECLARE #abschID INT;
DECLARE #value INT;
SELECT #value = 1;
SELECT #abschID = (SELECT TOP 1 Abschaetzung_ID FROM Abschaetzung_has_Varianten ORDER BY Abschaetzung_ID DESC);
SELECT #x = Count(*) FROM Flag WHERE AbschaetzID = #abschID
If #x > 0
Begin
UPDATE Flag Set [Flag] = #value WHERE AbschaetzID = #abschID;
end
end
Your code should be more like this:
CREATE TRIGGER trig_update_flag on [Abschaetzung_has_Varianten]
after insert, delete
as
begin
UPDATE Flag Set [Flag] = 1
WHERE AbschaetzID IN (SELECT DISTINCT Abschaetzung_ID FROM INSERTED)
UPDATE Flag Set [Flag] = 1
WHERE AbschaetzID IN (SELECT DISTINCT Abschaetzung_ID FROM DELETED)
end
INSERTED is a special trigger pseudo table that contains all of the updated or inserted records.
DELETED is a special trigger pseudo table that contains all of the deleted records.
This table may contain many records for one invocation of the trigger.
The code above is not the most efficient and may not suit your exact requirements but hopefully you get the idea.

After Update Triggers and batch updates

I have the following trigger to avoid updating a certain column.
ALTER TRIGGER [dbo].[MyTrigger]
ON [dbo].[MyTable]
AFTER UPDATE
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
IF UPDATE(SomeID)
BEGIN
DECLARE #id INT,
#newSomeID INT,
#currentSomeID INT
SELECT #id = ID, #newSomeID = SomeID
FROM inserted
SELECT #currentSomeID = SomeID
FROM deleted
WHERE ID = #id
IF (#newSomeID <> #currentSomeID)
BEGIN
RAISERROR ('cannot change SomeID (source = [MyTrigger])', 16, 1)
ROLLBACK TRAN
END
RETURN
END
END
Since i'm selecting from inserted and deleted, will this work if someone updates the table using a where clause that encapsulates multiple rows? In other words is it possible for the inserted and deleted table to contain more than one row within the scope of my trigger?
Thanks...
why not use an instead of update trigger and just join to INSERTED and push in all the columns except the one you don't want to update? your approach does not take in account that multiple rows can be affected by an single UPDATE statement.
try something like this:
ALTER TRIGGER [dbo].[MyTrigger]
ON [dbo].[MyTable]
INSTEAD OF UPDATE
AS
BEGIN
UPDATE m
SET col1=INSERTED.col1
,col2=INSERTED.col2
,col4=INSERTED.col4
FROM [dbo].[MyTable] m
INNER JOIN INSERTED i ON m.PK=i.PK
END
you could also try something like this:
ALTER TRIGGER [dbo].[MyTrigger]
ON [dbo].[MyTable]
AFTER UPDATE
AS
BEGIN
IF EXISTS(SELECT 1 FROM INSERTED i INNER JOIN DELETED d ON i.PK=d.PK WHERE i.SomeID!=d.SomeID OR (i.SomeID IS NULL AND d.SomeID IS NOT NULL) OR (d.SomeID IS NULL AND i.SomeID IS NOT NULL))
BEGIN
RAISERROR ('cannot change SomeID (source = [MyTrigger])', 16, 1)
ROLLBACK TRAN
RETURN
END
END
This will work for multiple row updates. Also, if the "SomeID" is NOT NULL you can remove the two OR conditions in the IF EXISTS
You need to define a cursor in trigger and get all affected records in cursor and then process it.

Resources