I want to create a trigger to check what is being deleted against business rules and then cancel the deletion if needed. Any ideas?
The solution used the Instead of Delete trigger. The Rollback tran stopped the delete. I was afraid that I would have a cascade issue when I did the delete but that didn't seem to happen. Maybe a trigger cannot trigger itself.
Use an INSTEAD OF DELETE (see MSDN) trigger and decide within the trigger what you really want to do.
The solution used the Instead of Delete trigger. The Rollback tran stopped the delete. I was afraid that I would have a cascade issue when I did the delete but that did'nt seem to happen. Maybe a trigger cannot trigger itself. Anyhow, thanks all for your help.
ALTER TRIGGER [dbo].[tr_ValidateDeleteForAssignedCalls]
on [dbo].[CAL]
INSTEAD OF DELETE
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
DECLARE #RecType VARCHAR(1)
DECLARE #UserID VARCHAR(8)
DECLARE #CreateBy VARCHAR(8)
DECLARE #RecID VARCHAR(20)
SELECT #RecType =(SELECT RecType FROM DELETED)
SELECT #UserID =(SELECT UserID FROM DELETED)
SELECT #CreateBy =(SELECT CreateBy FROM DELETED)
SELECT #RecID =(SELECT RecID FROM DELETED)
-- Check to see if the type is a Call and the item was created by a different user
IF #RECTYPE = 'C' and not (#USERID=#CREATEBY)
BEGIN
RAISERROR ('Cannot delete call.', 16, 1)
ROLLBACK TRAN
RETURN
END
-- Go ahead and do the update or some other business rules here
ELSE
Delete from CAL where RecID = #RecID
END
The trigger can roll back the current transaction, which will have the effect of cancelling the deletion. As the poster above also states, you can also use an instead of trigger.
According to MSDN documentation about INSTEAD OF DELETE triggers:
The deleted table sent to a DELETE
trigger contains an image of the rows
as they existed before the DELETE
statement was issued.
If I understand it correctly the DELETE is actually being executed. What am I missing?
Anyway, I don't understand why do you want to delete the records and if the business rules are not passed then undelete those records. I would have swear it should be easier to test if you pass the business rules before deleting the records.
And I would have said use a transaction, I haven't heard before about INSTEAD OF triggers.
Related
Would there be any benefit of not using SCOPE_IDENTITY() and switching to ##IDENTITY? For the area I'm talking about is part of an install script that sets up a database for our customers. It's inserting a record in one table and using the identifier key from that table and inserting it into a foreign key into another. We are doing this twice.
We seem to have a rare condition in which the 2nd time this happens, we are inserting the id from the first insert into the 2nd table for both passes, causing issues with the data. There is a chance that something else altogether is causing this, but my lead seemed to zeroed in on the SCOPE_IDENTITY() as possibly being the culprit.
Declare #TheId int
Insert into dbo.TableName (Name) Values ('xxxx')
Select #TheId = SCOPE_IDENTITY()
-- some code here that uses #TheId
-- ...
Insert into dbo.TableName (Name) Values ('yyyy')
Select #TheId = SCOPE_IDENTITY()
-- some code here that uses #TheId
-- at this point, we may have the condition that SCOPE_IDENTITY() still has the value before that 2nd insert...
The only way scope_identity() could have the prior id value in this context is if the INSERT statement does not create any rows. In that situation, ##IDENTITY isn't gonna fix anything. In fact, ##IDENTITY is less specific, and therefore could only hope to make things worse.
What you can do is use a different variable for the second insert. Or, you could set #TheId back to NULL before the second insert runs. In this way, you'll be able to tell if something went wrong. ##rowcount is also useful for this.
I did see this in the comments:
"The second insert did not fail as the record was found in the database."
I put it to you perhaps the record was already in the database, before the code ran. Moreover, if there is a constraint on the table this could be the reason why the insert fails.
Within the scope of the proc or script the #TheId created by the first insert is not same object as the #TheId created by the second insert. While it's possible to reuse variables it's not a good practice imo when it comes to multiple DML statements within a code block. In this script I add TRY/CATCH and SET XACT_ABORT ON to ensure a complete rollback of all DML statements within the block.
Something like this
set nocount on;
set xact_abort on;
begin transaction
begin try
Insert into dbo.TableName (Name) Values ('xxxx');
if ##rowcount=1
begin
Declare #Id1 int = SCOPE_IDENTITY();
-- some code here that uses #Id1
-- ...
end
else
throw 50000, 'The first insert failed', 1;
Insert into dbo.TableName (Name) Values ('yyyy');
if ##rowcount=1
begin
Declare #Id2 int = SCOPE_IDENTITY();
-- some code here that uses #Id2
-- ...
end
else
throw 50000, 'The second insert failed', 1;
commit transaction
end try
begin catch
/* put error handling here */
rollback transaction
end catch
Thanks everyone for the help. We will likely go with creating a new variable for the 2nd insert.
I want to make INSTEAD OF trigger like this:
CREATE TRIGGER [dbo].[DeleteCompany]
ON [dbo].[Company]
INSTEAD OF DELETE
AS
DECLARE #CompanyID int
SELECT #CompanyID = deleted.CompanyID FROM deleted
BEGIN TRAN
DELETE FROM Project WHERE CompanyID = #CompanyID
DELETE FROM CompanyPerson WHERE CompanyID = #CompanyID
UPDATE PersonCompany SET CompanyID = null WHERE CompanyID = #CompanyID
DELETE [Company]
FROM DELETED D
INNER JOIN [Company] T ON T.CompanyID = D.CompanyID
COMMIT TRAN
So, I can be sure, that these actions is one atomic action. But it make sense or TRIGGER always execute inside transaction?
Also, what happens if company will be deleted inside another TRIGGER like this:
CREATE TRIGGER [dbo].[DeleteSecurityLevel]
ON [dbo].[SecurityLevel]
INSTEAD OF DELETE
AS
DECLARE #SecurityLevelID int
SELECT #SecurityLevelID = deleted.SecurityLevelID FROM deleted
BEGIN TRAN
DELETE FROM Company WHERE SecurityLevelId = #SecurityLevelID
DELETE FROM CompanyRole WHERE SecurityLevelId = #SecurityLevelID
....
DELETE SecurityLevel
FROM DELETED D
INNER JOIN SecurityLevel T ON T.SecurityLevelID = D.SecurityLevelID
COMMIT TRAN
so, trigger DeleteSecurityLevel is deleting Company and call DeleteCompany trigger. It would be in one transaction if each trigger has BEGIN/COMMIT TRAM ? if each trigger does not have it?
PS. I can't set "CASCADE DELETE" because DB has some relationships like it:
so, try to set CASCADE DELETE will throw error like it:
Introducing FOREIGN KEY constraint 'FK_Persons_Areas' on table
'Persons' may cause cycles or multiple cascade paths. Specify ON
DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY
constraints. Could not create constraint or index. See previous
errors.
All DML statements are executed within a transaction. The DML within the trigger will use the transaction context of the statement that fired the trigger so all modifications, inside the trigger and out, are a single atomic operation.
You don't need explicit transactions inside the trigger, they share the same transaction workspace and the batch inside the trigger and the operation invoking it either commit together or rollback together
A trigger always executes in the context of a transaction - every DML statement operates within a transaction. This is normally hidden from view by the fact that Implicit Transactions are set to commit automatically in SQL Server.
If you issue a rollback from within a trigger, this will (as always with rollback) rollback all transactions, whether nested or not.
In general, you wouldn't commit within a trigger, unless (as in your commented out example) you're opening a nested transaction explicitly.
If there are other aspects to your question, I'm unable to work out what they are from your posted example. Although I'm always a fan of people posting actual SQL when asking SQL questions, sometimes a little commentary, or a bullet-point list of actual questions can help.
basically i am looking for logic to generate insert or update statement in string variable from trigger. suppose when people just update 'N' of fields like update statement....then my trigger will fire and from that trigger i want build what update statement was issued by user and store in string variable
the same way i want to build insert statement from trigger too but i am not getting logic. so if anyone has any idea or sample code then please share with me to achieve my goal.
ALTER TRIGGER WSContent AFTER INSERT, UPDATE, DELETE
AS
SET NOCOUNT ON;
DECLARE #Action VARCHAR(10)
DECLARE #PKValue INT
DECLARE #TableName VARCHAR(50)
SET #TableName='website_content'
IF EXISTS(SELECT * FROM INSERTED)
BEGIN
IF EXISTS(SELECT * FROM DELETED)
BEGIN
SET #Action ='U';
SELECT #PKValue=ContentNumber from DELETED
END
ELSE
BEGIN
SET #Action ='I';
SELECT #PKValue=ContentNumber from INSERTED
END
END
ELSE
BEGIN
SET #Action = 'D';
SELECT #PKValue=ContentNumber from DELETED
END;
INSERT INTO [ContentChangeLog]([PkValue],[TableName],[Action],ActionDate)
VALUES(#PKValue,#TableName,#Action,GetDate())
SET NOCOUNT OFF;
Like many people, you misunderstand how a trigger works. When you insert, update or delete multiple records, the trigger is called once and the tables deleted/inserted can contain multiple records, not 1 for each record effected. You need to rewrite this assuming you have multiple records in those tables.
INSERT INTO [ContentChangeLog]([PkValue],[TableName],[Action],ActionDate)
SELECT ContentNumber, #TableName, 'I', GETDATE()
FROM INSERTED i
WHERE not exists(SELECT TOP 1 1 FROM DELETED WHERE ContentNumber = i.ContentNumber)
Here is an example of the Inserted records only, you will want to do something similar for your updates and deletes.
Here is my trigger
Create TRIGGER [dbo].[tri_before_update]
ON [dbo].[test]
instead of update
AS
BEGIN
SET NOCOUNT ON;
if update (test_a)
begin
*.. my update & insert query*
end
END
create TRIGGER [dbo].[tri_before_update_price]
ON [dbo].[co_ticket]
instead of update
AS
BEGIN
SET NOCOUNT ON;
if update (t_price)
begin
insert into old_price_log (t_id,insert_time,process_id,old_t_price)
select i.t_id,getdate(),2,t_price
from Inserted i,co_ticket t where i.t_id = t.t_id
update t set t_price = i.t_price
from co_ticket t, inserted i
where t.t_id = i.t_id
end
else
begin
-- if update other then (t_price) then the update comand not execute.
-- example when i update t_cancel_flag or t_quantity and etc. end
END
This trigger execute perfectly when i update on column "test_a". HOWEVER, when i update other than column "test_a" it won't be execute. I know i can put "else" command, but i got a lot of column. sometimes will update two other column , sometimes three or four column. I don't wish to update all column everytime. Is it possible ELSE "then execute original query"?
I tried a lot different way but still can't work. :( Please HELP!
create TRIGGER [dbo].[tri_on_update_price]
ON [dbo].[co_ticket]
AS
BEGIN
SET NOCOUNT ON;
if update (t_price)
begin
insert into old_price_log (t_id,insert_time,process_id,old_t_price)
select d.t_id, getutcdate(),2,d.price
from deleted d
END
end
An ordinary after trigger will do just what you want: insert a log of the price change, if the price was updated. No need for INSTEAD OF. You need to look into the deleted pseudo-table to get the old price. Never store local times in a database.
I want to prevent a specific record from being deleted. This trigger works fine for that specific record. However, other records still remain when they're being deleted. Why?
ALTER TRIGGER [Globalization].[CountriesTracker]
ON [Globalization].[Countries]
INSTEAD OF DELETE
AS
BEGIN
SET NOCOUNT ON;
IF ((Select COUNT(*) from [Deleted]
Where [Deleted].[CountryId] = '36bd1536-fb56-4ec4-957e-1b3afde16c56') = 1)
BEGIN
RAISERROR('You can not delete this specific record!', 0, 0)
ROLLBACK TRANSACTION
RETURN
END
END
How can I ensure that rows not matching the above condition are being deleted as expected?
You have an INSTEAD OF trigger so you need an actual DELETE in it.
I'd also consider simply filtering the protected row out because:
Do you need an error throwing? Or silently ignore?
What about multi row deletes that contain the protected row: abort the whole, or delete the rest?
Something like:
ALTER TRIGGER [Globalization].[CountriesTracker] ON [Globalization].[Countries]
INSTEAD OF DELETE
AS
SET NOCOUNT ON;
DELETE
CT
FROM
[Globalization].[Countries] C
JOIN
DELETED D ON C.CountryId = D.CountryId
WHERE
[Deleted].[CountryId] <> '36bd1536-fb56-4ec4-957e-1b3afde16c56'
GO
Because this is INSTEAD OF you still need to perform the delete operation for the default case.