SQL Server Trigger switching Insert,Delete,Update - sql-server

Hello is possible to switch between DML commands/operations (Insert,Delete,Update) on Trigger Body?, I try to snippet some T-SQL for understand me better :
CREATE TRIGGER DML_ON_TABLEA
ON TABLEA
AFTER INSERT,DELETE,UPDATE
AS
BEGIN
SET NOCOUNT ON;
CASE
WHEN (INSERT) THEN
-- INSERT ON AUX TABLEB
WHEN (DELETE) THEN
-- DELETE ON AUX TABLEB
ELSE --OR WHEN (UPDATE) THEN
-- UPDATE ON AUX TABLEB
END
END
GO
Thanks,

I will show you a simple way to check this in SQL Server 2000 or 2005 (you forgot to mention which version you are using), but in general I agree with Remus that you should break these up into separate triggers:
DECLARE #i INT, #d INT;
SELECT #i = COUNT(*) FROM inserted;
SELECT #d = COUNT(*) FROM deleted;
IF #i + #d > 0
BEGIN
IF #i > 0 AND #d = 0
BEGIN
-- logic for insert
END
IF #i > 0 AND #d > 0
BEGIN
-- logic for update
END
IF #i = 0 AND #d > 0
BEGIN
-- logic for delete
END
END
Note that this may not be perfectly forward-compatible due to the complexity MERGE introduces in SQL Server 2008. See this Connect item for more information:
MERGE can cause a trigger to fire multiple times
So if you are planning to use SQL Server 2008 and MERGE in the future, then this is even more reason to split the trigger up into a trigger for each type of DML operation.
(And if you want more reasons to avoid MERGE, read this and this.)

You can use the inserted and deleted tables to see what changes were made to the table.
For an UPDATE, the deleted table contains the old version of the row, and inserted the new version.
DELETE and INSERT use their own table as you would expect.

You can have three separate triggers, one for INSERT one for UPDATE one for DELETE. Since each trigger is different, there is no need for switch logic.

I think the general way to do this is to create a trigger for each action, like so:
CREATE TRIGGER INSERT_ON_TABLEA
ON TABLEA
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON;
-- INSERT ON AUX TABLEB
END
GO
CREATE TRIGGER DELETE_ON_TABLEA
ON TABLEA
AFTER DELETE
AS
BEGIN
SET NOCOUNT ON;
-- DELETE ON AUX TABLEB
END
GO

You can use one trigger for all commands/operations by use union join;
CREATE TRIGGER DML_ON_TABLEA
ON TABLEA
AFTER INSERT,DELETE,UPDATE
AS
BEGIN
SET NOCOUNT ON;
--logic for insert
insert into Backup_table (columns_name) select columns_name from inserted i
--logic for delete
UNION ALL
insert into Backup_table (columns_name) select columns_name from deleted d
END
GO
--note update command like inserted command but have another command deleted

Related

Using a if condition in an insert SQL Server

I have the following statement in my code
INSERT INTO #TProductSales (ProductID, StockQTY, ETA1)
VALUES (#ProductID, #StockQTY, #ETA1)
I want to do something like:
IF #ProductID exists THEN
UPDATE #TProductSales
ELSE
INSERT INTO #TProductSales
Is there a way I can do this?
The pattern is (without error handling):
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN TRANSACTION;
UPDATE #TProductSales SET StockQty = #StockQty, ETA1 = #ETA1
WHERE ProductID = #ProductID;
IF ##ROWCOUNT = 0
BEGIN
INSERT #TProductSales(ProductID, StockQTY, ETA1)
VALUES(#ProductID, #StockQTY, #ETA1);
END
COMMIT TRANSACTION;
You don't need to perform an additional read of the #temp table here. You're already doing that by trying the update. To protect from race conditions, you do the same as you'd protect any block of two or more statements that you want to isolate: you'd wrap it in a transaction with an appropriate isolation level (likely serializable here, though that all only makes sense when we're not talking about a #temp table, since that is by definition serialized).
You're not any further ahead by adding an IF EXISTS check (and you would need to add locking hints to make that safe / serializable anyway), but you could be further behind, depending on how many times you update existing rows vs. insert new. That could add up to a lot of extra I/O.
People will probably tell you to use MERGE (which is actually multiple operations behind the scenes, and also needs to be protected with serializable), I urge you not to. I and others lay out why here:
Use Caution with SQL Server's MERGE Statement
So, you want to use MERGE, eh?
For a multi-row pattern (like a TVP), I would handle this quite the same way, but there isn't a practical way to avoid the second read like you can with the single-row case. And no, MERGE doesn't avoid it either.
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN TRANSACTION;
UPDATE t SET t.col = tvp.col
FROM dbo.TargetTable AS t
INNER JOIN #TVP AS tvp
ON t.ProductID = tvp.ProductID;
INSERT dbo.TargetTable(ProductID, othercols)
SELECT ProductID, othercols
FROM #TVP AS tvp
WHERE NOT EXISTS
(
SELECT 1 FROM dbo.TargetTable
WHERE ProductID = tvp.ProductID
);
COMMIT TRANSACTION;
Well, I guess there is a way to do it, but I haven't tested this thoroughly:
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN TRANSACTION;
DECLARE #exist TABLE(ProductID int PRIMARY KEY);
UPDATE t SET t.col = tvp.col
OUTPUT deleted.ProductID INTO #exist
FROM dbo.TargetTable AS t
INNER JOIN #tvp AS tvp
ON t.ProductID = tvp.ProductID;
INSERT dbo.TargetTable(ProductID, othercols)
SELECT ProductID, othercols
FROM #tvp AS t
WHERE NOT EXISTS
(
SELECT 1 FROM #exist
WHERE ProductID = t.ProductID
);
COMMIT TRANSACTION;
In either case, you perform the update first, otherwise you'll update all the rows you just inserted, which would be wasteful.
I personally like to make a table variable or temp table to store the values and then do my update/insert, but I'm normally doing mass insert/updates. That is the nice thing about this pattern is that it works for multiple records without redundancy in the inserts/updates.
DECLARE #Tbl TABLE (
StockQty INT,
ETA1 DATETIME,
ProductID INT
)
INSERT INTO #Tbl (StockQty,ETA1,ProductID)
SELECT #StockQty AS StockQty ,#ETA1 AS ETA1,#ProductID AS ProductID
UPDATE tps
SET StockQty = tmp.StockQty
, tmp.ETA1 = tmp.ETA1
FROM #TProductSales tps
INNER JOIN #Tbl tmp ON tmp.ProductID=tps.ProductID
INSERT INTO #TProductSales(StockQty,ETA1,ProductID)
SELECT
tmp.StockQty,tmp.ETA1,tmp.ProductID
FROM #Tbl tmp
LEFT JOIN #TProductSales tps ON tps.ProductID=tmp.ProductID
WHERE tps.ProductID IS NULL
You could use something like:
IF EXISTS( SELECT NULL FROM #TProductSales WHERE ProductID = #ProductID)
UPDATE #TProductSales SET StockQTY = #StockQTY, ETA1 = #ETA1 WHERE ProductID = #ProductID
ELSE
INSERT INTO #TProductSales(ProductID,StockQTY,ETA1) VALUES(#ProductID,#StockQTY,#ETA1)

SQL Server for delete trigger to delete records in other table with procedure

I've got a table like table1 (table1_id, col1, some_ID) and another table2 (table2_id, col1, table1_id)
I have a procedure
PROC deleteTable2(#id int)
AS
BEGIN
DELETE table2
WHERE table1_id = #id
END
I'm trying to create a trigger for delete for table1 table, so when I'm trying to delete from table1, the trigger will delete all records in table2.
I wrote the trigger like this:
CREATE TRIGGER deleteTable1 on table1
FOR DELETE
AS
BEGIN
SET NOCOUNT ON
DECLARE #id int = 0
SELECT #id = del.table1_ID from deleted as del
EXECUTE dbo.deleteTable2 #id
It works if there is only one record for some_ID, but if there are more records, it doesn't work. For example:
delete table1
where some_ID='some value'
This SQL query will delete all records in table1 and only first in table2.
It makes sense, so I made a trigger with CURSOR, something like
CREATE TRIGGER deleteTable1 ON table1
FOR DELETE
AS
BEGIN
DECLARE #id int = 0, #CURSOR cursor
SET #CURSOR = CURSOR scroll FOR
SELECT deleted.table1_id
FROM deleted
OPEN #CURSOR
FETCH NEXT FROM #CURSOR into #id
WHILE ##FETCH_STATUS = 0
BEGIN
EXECUTE dbo.deleteTable2 #id
FETCH NEXT FROM #CURSOR into #id
END
CLOSE #CURSOR
END
But it doesn't work at all... Maybe I just missed something or I don't get some nuances. Thank you.
UPDATE
In fact, This 2 tables are in one db, but I've got 3rd table table3 (table3_id, col1, table2_id). And this table3 is in other db, and they connected by linked server. So when I call stored procedure as I'm trying, this stored procedure calls stored procedure (with cursor) to delete records in table3. That the exact reason why I'm trying to use stored procedure to delete records in table2.
Avoid using all these unnecessary cursors and stored procedure. Simply follow a set based approach and do the following inside your trigger.
CREATE TRIGGER deleteTable1 on table1
FOR DELETE
AS
BEGIN
SET NOCOUNT ON
Delete FROM Table2
WHERE Exists (SELECT 1
FROM deleted del
WHERE del.table1_ID = table2.Table1_ID)
END
UPDATE
Since you have mentioned you would like to delete records from a third table too which is some where on a linked server.
I would suggest you to use the same approach and just add another delete statement in inside the procedure for that third table on the linked server.
CREATE TRIGGER deleteTable1 on table1
FOR DELETE
AS
BEGIN
SET NOCOUNT ON
Delete FROM Table2
WHERE Exists (SELECT 1
FROM deleted del
WHERE del.table1_ID = table2.Table1_ID)
DELETE FROM t3
FROM [LinkedServerName].[DBName].[SchemaName].Table3 t3
WHERE Exists (SELECT 1
FROM deleted del
WHERE del.table1_ID = t3.Table1_ID)
END
CREATE TRIGGER deleteTable1 on table1
FOR DELETE
AS
BEGIN
SET NOCOUNT ON
DELETE FROM table2 WHERE table1_id IN (SELECT table1_id FROM deleted)
END

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.

Instead of trigger in SQL Server loses SCOPE_IDENTITY?

I have a table where I created an INSTEAD OF trigger to enforce some business rules.
The issue is that when I insert data into this table, SCOPE_IDENTITY() returns a NULL value, rather than the actual inserted identity.
Insert + Scope code
INSERT INTO [dbo].[Payment]([DateFrom], [DateTo], [CustomerId], [AdminId])
VALUES ('2009-01-20', '2009-01-31', 6, 1)
SELECT SCOPE_IDENTITY()
Trigger:
CREATE TRIGGER [dbo].[TR_Payments_Insert]
ON [dbo].[Payment]
INSTEAD OF INSERT
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
IF NOT EXISTS(SELECT 1 FROM dbo.Payment p
INNER JOIN Inserted i ON p.CustomerId = i.CustomerId
WHERE (i.DateFrom >= p.DateFrom AND i.DateFrom <= p.DateTo) OR (i.DateTo >= p.DateFrom AND i.DateTo <= p.DateTo)
) AND NOT EXISTS (SELECT 1 FROM Inserted p
INNER JOIN Inserted i ON p.CustomerId = i.CustomerId
WHERE (i.DateFrom <> p.DateFrom AND i.DateTo <> p.DateTo) AND
((i.DateFrom >= p.DateFrom AND i.DateFrom <= p.DateTo) OR (i.DateTo >= p.DateFrom AND i.DateTo <= p.DateTo))
)
BEGIN
INSERT INTO dbo.Payment (DateFrom, DateTo, CustomerId, AdminId)
SELECT DateFrom, DateTo, CustomerId, AdminId
FROM Inserted
END
ELSE
BEGIN
ROLLBACK TRANSACTION
END
END
The code worked before the creation of this trigger. I am using LINQ to SQL in C#. I don't see a way of changing SCOPE_IDENTITY to ##IDENTITY. How do I make this work?
Use ##identity instead of scope_identity().
While scope_identity() returns the last created id in the current scope, ##identity returns the last created id in the current session.
The scope_identity() function is normally recommended over the ##identity field, as you usually don't want triggers to interfer with the id, but in this case you do.
Since you're on SQL 2008, I would highly recommend using the OUTPUT clause instead of one of the custom identity functions. SCOPE_IDENTITY currently has some issues with parallel queries that cause me to recommend against it entirely. ##Identity does not, but it's still not as explicit, and as flexible, as OUTPUT. Plus OUTPUT handles multi-row inserts. Have a look at the BOL article which has some great examples.
I was having serious reservations about using ##identity, because it can return the wrong answer.
But there is a workaround to force ##identity to have the scope_identity() value.
Just for completeness, first I'll list a couple of other workarounds for this problem I've seen on the web:
Make the trigger return a rowset. Then, in a wrapper SP that performs the insert, do INSERT Table1 EXEC sp_ExecuteSQL ... to yet another table. Then scope_identity() will work. This is messy because it requires dynamic SQL which is a pain. Also, be aware that dynamic SQL runs under the permissions of the user calling the SP rather than the permissions of the owner of the SP. If the original client could insert to the table, he should still have that permission, just know that you could run into problems if you deny permission to insert directly to the table.
If there is another candidate key, get the identity of the inserted row(s) using those keys. For example, if Name has a unique index on it, then you can insert, then select the (max for multiple rows) ID from the table you just inserted to using Name. While this may have concurrency problems if another session deletes the row you just inserted, it's no worse than in the original situation if someone deleted your row before the application could use it.
Now, here's how to definitively make your trigger safe for ##Identity to return the correct value, even if your SP or another trigger inserts to an identity-bearing table after the main insert.
Also, please put comments in your code about what you are doing and why so that future visitors to the trigger don't break things or waste time trying to figure it out.
CREATE TRIGGER TR_MyTable_I ON MyTable INSTEAD OF INSERT
AS
SET NOCOUNT ON
DECLARE #MyTableID int
INSERT MyTable (Name, SystemUser)
SELECT I.Name, System_User
FROM Inserted
SET #MyTableID = Scope_Identity()
INSERT AuditTable (SystemUser, Notes)
SELECT SystemUser, 'Added Name ' + I.Name
FROM Inserted
-- The following statement MUST be last in this trigger. It resets ##Identity
-- to be the same as the earlier Scope_Identity() value.
SELECT MyTableID INTO #Trash FROM MyTable WHERE MyTableID = #MyTableID
Normally, the extra insert to the audit table would break everything, because since it has an identity column, then ##Identity will return that value instead of the one from the insertion to MyTable. However, the final select creates a new ##Identity value that is the correct one, based on the Scope_Identity() that we saved from earlier. This also proofs it against any possible additional AFTER trigger on the MyTable table.
Update:
I just noticed that an INSTEAD OF trigger isn't necessary here. This does everything you were looking for:
CREATE TRIGGER dbo.TR_Payments_Insert ON dbo.Payment FOR INSERT
AS
SET NOCOUNT ON;
IF EXISTS (
SELECT *
FROM
Inserted I
INNER JOIN dbo.Payment P ON I.CustomerID = P.CustomerID
WHERE
I.DateFrom < P.DateTo
AND P.DateFrom < I.DateTo
) ROLLBACK TRAN;
This of course allows scope_identity() to keep working. The only drawback is that a rolled-back insert on an identity table does consume the identity values used (the identity value is still incremented by the number of rows in the insert attempt).
I've been staring at this for a few minutes and don't have absolute certainty right now, but I think this preserves the meaning of an inclusive start time and an exclusive end time. If the end time was inclusive (which would be odd to me) then the comparisons would need to use <= instead of <.
Main Problem : Trigger and Entity framework both work in diffrent scope.
The problem is, that if you generate new PK value in trigger, it is different scope. Thus this command returns zero rows and EF will throw exception.
The solution is to add the following SELECT statement at the end of your Trigger:
SELECT * FROM deleted UNION ALL
SELECT * FROM inserted;
in place of * you can mention all the column name including
SELECT IDENT_CURRENT(‘tablename’) AS <IdentityColumnname>
Like araqnid commented, the trigger seems to rollback the transaction when a condition is met. You can do that easier with an AFTER INSTERT trigger:
CREATE TRIGGER [dbo].[TR_Payments_Insert]
ON [dbo].[Payment]
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON;
IF <Condition>
BEGIN
ROLLBACK TRANSACTION
END
END
Then you can use SCOPE_IDENTITY() again, because the INSERT is no longer done in the trigger.
The condition itself seems to let two identical rows past, if they're in the same insert. With the AFTER INSERT trigger, you can rewrite the condition like:
IF EXISTS(
SELECT *
FROM dbo.Payment a
LEFT JOIN dbo.Payment b
ON a.Id <> b.Id
AND a.CustomerId = b.CustomerId
AND (a.DateFrom BETWEEN b.DateFrom AND b.DateTo
OR a.DateTo BETWEEN b.DateFrom AND b.DateTo)
WHERE b.Id is NOT NULL)
And it will catch duplicate rows, because now it can differentiate them based on Id. It also works if you delete a row and replace it with another row in the same statement.
Anyway, if you want my advice, move away from triggers altogether. As you can see even for this example they are very complex. Do the insert through a stored procedure. They are simpler and faster than triggers:
create procedure dbo.InsertPayment
#DateFrom datetime, #DateTo datetime, #CustomerId int, #AdminId int
as
BEGIN TRANSACTION
IF NOT EXISTS (
SELECT *
FROM dbo.Payment
WHERE CustomerId = #CustomerId
AND (#DateFrom BETWEEN DateFrom AND DateTo
OR #DateTo BETWEEN DateFrom AND DateTo))
BEGIN
INSERT into dbo.Payment
(DateFrom, DateTo, CustomerId, AdminId)
VALUES (#DateFrom, #DateTo, #CustomerId, #AdminId)
END
COMMIT TRANSACTION
A little late to the party, but I was looking into this issue myself. A workaround is to create a temp table in the calling procedure where the insert is being performed, insert the scope identity into that temp table from inside the instead of trigger, and then read the identity value out of the temp table once the insertion is complete.
In procedure:
CREATE table #temp ( id int )
... insert statement ...
select id from #temp
-- (you can add sorting and top 1 selection for extra safety)
drop table #temp
In instead of trigger:
-- this check covers you for any inserts that don't want an identity value returned (and therefore don't provide a temp table)
IF OBJECT_ID('tempdb..#temp') is not null
begin
insert into #temp(id)
values
(SCOPE_IDENTITY())
end
You probably want to call it something other than #temp for safety sake (something long and random enough that no one else would be using it: #temp1234235234563785635).

Resources