Much more complicated then this, but this is the basic
Person table (id, name, emailaddress)
Salesperson table (id, personID)
CustomerServiceRep table (id, personID)
Jeff is salesperson (id=4) and customerservicerep (id=5) with personID=1.
Simple
Trigger on SalesPerson Table
AFTER DELETE
AS
DECLARE #personID int = (SELECT personID FROM deleted);
IF #personID IS NOT NULL
BEGIN TRY
DELETE FROM Person
WHERE Person.id = #personID;
END TRY
BEGIN CATCH
END CATCH
DELETE FROM SalesPerson WHERE id=4;
Causes
Msg 3616, Level 16, State 1
An error was raised during trigger execution. The batch has been aborted and the user transaction, if any, has been rolled back.
I'm sure there's a much simpler way to not delete personID if it exists from some kind of constraint. Or catch the constraint. To go through every possible table that this could be in seems very repetitive and potentially more difficult when there are more tables/columns that may use this same table/constraint (foreign key).
You need an instead of delete trigger here rather than an after trigger.
CREATE Trigger tr_Delete_person
on Person
INSTEAD OF DELETE
AS
BEGIN
SET NOCOUNT ON;
-- Delete any child records
Delete FROM SalesPerson
WHERE EXISTS (SELECT 1 FROM deleted
WHERE personID = SalesPerson.personID)
Delete FROM CustomerServiceRep
WHERE EXISTS (SELECT 1 FROM deleted
WHERE personID = CustomerServiceRep .personID)
-- Finally delete from the person table
DELETE FROM Person
WHERE EXISTS (SELECT 1 FROM deleted
WHERE personID = Person .personID)
END
You also have a fundamental flaw in your trigger in that you seem to expect that the trigger will be fired once per row - this is NOT the case in SQL Server. Instead, the trigger fires once per statement, and the pseudo table Deleted might contain multiple rows.
Given that that table might contain multiple rows - which one do you expect will be selected here??
DECLARE #personID int = (SELECT personID FROM deleted);
It's undefined - you'll get the value from one, arbitrary row in Deleted, and all others are ignored - typically not what you want!
You need to rewrite your entire trigger with the knowledge the Deleted WILL contain multiple rows! You need to work with set-based operations - don't expect just a single row in Deleted !
Related
I hope someone can help me. This is my first post ever, so I hope I explained it well.
I am using Microsoft SQL Management Studio.
I am trying to make a trigger that is not letting anyone change the personal number of a person if that person didn't return a computer in a database that I made.
So I am making a trigger for table Person where I have ID_Person primary key, Name, Surname and a Personal_numb.
Also, I made a table called Renting where I have columns: ID_Renting primary key, ID_Person foreign key, ID_Computer foreign key and Date_Rented and Date_Returned columns..
Here is how my code look a like:
create trigger NO
On Person
after update
as
begin
if update(Personal_numb)
(select ID_Person from Renting
where Renting.Date_Returned is null)
begin
Raiserror('Person needs to return the computer before you try to change their personal number!', 16,1)
end
end
However, I know that I am missing something between if update(Personal_numb) and select but what is it?
You could try something like this
drop trigger if exists trg_Person_upd;
go
create trigger trg_Person_upd on Person
after update
as
set nocount on;
if exists(select * from inserted);
begin
if exists (select *
from Renting r
join inserted i on r.ID_Person=i.ID_Person
join deleted d on r.ID_Person=d.ID_Person
where i.Personal_numb<>d.Personal_numb
and r.Date_Returned is null))
throw 50000, 'Person needs to return the computer before you try to change their personal number!', 1;
end
Something like this.
You use inserted to join to renting and checks for Date_Rreturned using EXISTS()
Note : I changed the trigger name. Naming it NO is rather unusual
create trigger TR_Person
On Person
after update
as
begin
if update(Personal_numb)
and exists
(
select *
from inserted i
inner join Renting r on i.ID = r.ID_Person
where r.Date_Returned is null
)
begin
Raiserror('Person needs to return the computer before you try to change their personal number!', 16,1)
end
end
go
I can detect duplicate records, but when I'm inserting new data it will detect it as a duplicate record even if doesn't already exist.
Here is my code:
ALTER TRIGGER [dbo].[SDPRawInventory_Dup_Trigger]
ON [dbo].[SDPRawInventory]
AFTER INSERT
AS
DECLARE #Year float,
#Month float,
#SDPGroup nvarchar(255);
SELECT
#Year = i.InvYear, #Month = i.InvMonth, #SDPGroup = i.SDPGroup
FROM inserted i;
IF (SELECT COUNT(*) FROM SDPRawInventory A
WHERE A.InvYear = #Year
AND A.InvMonth = #Month
AND A.SDPGroup = #SDPGroup) >= 1
BEGIN
RAISERROR ('Duplicate data', 16, 1)
ROLLBACK;
END
ELSE
BEGIN
INSERT INTO SDPRawInventory
SELECT * FROM inserted;
END
This is the table
And to clarify there is no primary key nor unique identifier.
If you are unable to put a constraint in place, then you need to handle the fact that Inserted may have multiple records. And because its an after insert trigger, you don't need to do anything if no duplicates are found because the records are already inserted.
ALTER TRIGGER [dbo].[SDPRawInventory_Dup_Trigger]
ON [dbo].[SDPRawInventory]
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON;
IF EXISTS (
SELECT 1
FROM dbo.SDPRawInventory S
INNER JOIN Inserted I ON
-- Test for a duplicate
S.InvYear = I.InvYear
AND S.InvMonth = I.InvMonth
AND S.SDPGroup = I.SDPGroup
-- But ensure the duplicate is a *different* record - assumes a unique ID
AND S.ID <> I.ID
)
BEGIN
THROW 51000, 'Duplicate data.', 1;
END;
END;
Note the simplified and modern error handling.
EDIT: And if you have no unique key, and no permission to add one, then you need an instead of trigger to only insert non-duplicates e.g.
ALTER TRIGGER [dbo].[SDPRawInventory_Dup_Trigger]
ON [dbo].[SDPRawInventory]
INSTEAD OF INSERT
AS
BEGIN
SET NOCOUNT ON;
-- Reject the entire insert if a single duplicate exists
-- Note if multiple records are inserted, some of which are duplicates and some of which aren't, they all get rejected
IF EXISTS (
SELECT 1
FROM dbo.SDPRawInventory S
INNER JOIN Inserted I ON
-- Test for a duplicate
A.InvYear = I.InvYear
AND A.InvMonth = I.InvMonth
AND A.SDPGroup = I.#SDPGroup
)
-- Test that Inserted itself doesn't contain duplicates
OR EXISTS (SELECT 1 FROM Inserted GROUP BY InvYear, InvMonth, SDPGroup HAVING COUNT(*) > 1)
BEGIN
THROW 51000, 'Duplicate data.', 1;
END;
INSERT INTO dbo.SDPRawInventory (SDP_SKU_DESC, WholeQty, InvYear, InvMonth, SDPGroup, invUOM, LooseQty)
SELECT SDP_SKU_DESC, WholeQty, InvYear, InvMonth, SDPGroup, invUOM, LooseQty
FROM Inserted I
WHERE NOT EXISTS (
SELECT 1
FROM dbo.SDPRawInventory S
-- Test for a duplicate
WHERE S.InvYear = I.InvYear
AND S.InvMonth = I.InvMonth
AND S.SDPGroup = I.SDPGroup
);
END;
Note: This doesn't do anything to handle existing duplicates.
This trigger is executed after the new records were inserted, so it will at least find the original records in the SELECT COUNT statement. Changing >= 1 into >= 2 can only partially fix this when inserting is guaranteed to occur one record as a time. Moreover, it will still fail when there were already multiple duplicated of the newly inserted record in the database before the insert.
You need to exclude the latest inserted records from the COUNT. But a better idea would probably be to add a UNIQUE constraint for preventing duplicates, so no trigger would be necessary.
If adding a constraint is not possible yet, you should initiate a clean-up process to eliminate the existing duplicates beforehand. Everything else is looks pretty flawed to me, since it is unlikely the current approach will ever bring the table into a state with no duplicates.
You are creating the infinite loop. You just have to remove the insertion part from your trigger.
ELSE
BEGIN
INSERT INTO SDPRawInventory
SELECT * FROM inserted;
END
This should not be in the trigger as trigger is called as part of insertion. you should not write actual insertion in to table in trigger.
I am attempting to do some batch deletions using Azure's SQL Database, I run a statement as follows:
delete from DbRoles where AccountID = 41;
One would expect that all rows where AccountID is 41, however it only deletes that last matching row from the table. I have found this to be true whether using a regular "where" statement as well as using subqueries like
where Id in (select Id from DbRoles where AccountID = 41)
(And Id in (41, 42, 43, ... etc) also fails) Does anyone have any ideas why this might be?
----- EDIT -----
More info to answer some questions in the comments:
The Roles table does indeed have a trigger on delete (thanks for the tip #Steve Ford) We have a Closure table connected to the Roles table, and it gets updated via triggers when actions occur in the roles table. One of which is the delete trigger, which goes as follows:
CREATE TRIGGER [tr_dbRole_delete]
ON [dbo].[DbRoles]
Instead of DELETE
AS
BEGIN
declare #roleID int;
select #roleID = Id from deleted
--delete self closure
delete from DbRoleClosures
where childID = #roleID and parentID = #roleID
--delete role
delete from DbRoles
where Id = #roleID
END
I think the "Instead" statement might be my culprit, can anyone verify?
Your trigger is only deleting one row because of this statement:
select #roleID = Id from deleted
When you delete multiple rows #roleID will be set to one of the Id's depending upon which order SQL Server queried your deleted table.
Trigger should be this:
CREATE TRIGGER [tr_dbRole_delete]
ON [dbo].[DbRoles]
Instead of DELETE
AS
BEGIN
--delete self closure
delete from DbRoleClosures
where childID = parentId AND
childID in (SELECT Id from deleted)
--delete role
delete from DbRoles
where Id in (SELECT Id from deleted)
END
I've discovered a new problem: I have a two tables, Classes and Students. Students references to Classes by [ClassID] column. Classes have column named [Count], which storing count of referencing students and I'm trying update it via AFTER INSERT,DELETE trigger on Students table.
I wrote a simple CALC_COUNT procedure like that:
CREATE PROCEDURE [dbo].[CALC_COUNT]
#classid INT
AS
BEGIN
UPDATE classes SET [Count] = (SELECT COUNT(Id) FROM students WHERE [ClassID] = #classid);
END
RETURN 0
and use it inside trigger
CREATE TRIGGER [MONITOR_STUDENTS_SCHEMA_TRIGGER]
ON [dbo].[students]
AFTER DELETE, INSERT
AS
BEGIN
UPDATE [dbo].[classes]
SET studentsschemarev +=1 FROM inserted;
CALC_COUNT(SELECT [ClassID] FROM inserted UNION SELECT [ClassID] FROM deleted);
UPDATE [dbo].[stats] SET students_schema_rev += 1;
END
But it not works.
I think, I need a way to execute procedure for each row in SELECT statement of trigger, but I don't know how.
SQL Server 2012 LocalDB, compatibility mode with SQL Server 2008.
You don't need your stored proc.
Update your trigger to be
update classes
set
count = StudentCount,
schemarevcount += 1
from
classes
inner join
(select * from inserted union select * from deleted) students
on classes.classid=students.classid
inner join
(select classid, count(*) as StudentCount from students group by classid) counts
on students.classid = counts.classid
in place of the update and the call to calc_count
An english translation...
Update classes (set the revision and the count)
where the class is changed in the students table
where that class is in the set of counts of students per class
Well, the main reason your trigger is not fired, is because you are doing an UPDATE where your trigger is going of on a INSERT and DELETE.
Yes, an UPDATE is a DELETE and INSERT, but you have to put it in your footprint :
AFTER DELETE, INSERT, UPDATE
instead of
AFTER DELETE, INSERT
Next, in the trigger itself you only have the internal conceptual tables called inserted and deleted, which combined give you the update. The old part is in the deleted table, the new part is in the inserted table.
Why you want to execute record by record by the way? Doing it with a complete resultset is much faster!
I have a table that has id, name, age. (Table name : Employee)
The id is the primary key in the table. There is an Sproc which deletes the entry given the name.
So in my sproc for deletion, I first select the id based on the name. If the name is valid then I do a delete.
DECLARE #Id uniqueidentifier
BEGIN TRANSACTION
SELECT TOP 1 #Id=Id FROM Employee WHERE Name=#Name
IF ##ROWCOUNT = 0
BEGIN
ROLLBACK TRANSACTION
RETURN ERROR_NOT_FOUND
END
DELETE FROM EMPLOYEE WHERE Id = #Id
IF ##ROWCOUNT = 0
BEGIN
ROLLBACK TRANSACTION
RETURN ERROR_NOT_FOUND
END
COMMIT TRANSACTION
My question is whether I need to need a transaction in this case or not. I understand that we need a transaction when we want atomic operations (set of operations should pass/fail together).
Please comment for the above scenario whether a transaction is required.. and what are the pros / cons of with / without transaction.
As for the answer to your question - if there is no employee with this name neither delete nor select will not change anything in the database, thus rolling back is not necessary.
Now if id is unique and the name is not - selecting an employee by the name is pretty dangerous since you have no real control which employee (from ones with the same name) you are going to delete. Looks like this procedure should take id as a parameter, rather than selecting it by name.
In your example code both ROLLBACK statements aren't actually rolling anything back since in both cases you haven't changed anything in the database. In other words, no, they aren't necessary.