sql trigger to stop duplicates across row - sql

I have a table with multiple records:
User_Name ( e.g. 'TOM')
Question_ID (eg 'q002')
Answer (e.g. 'D')
i want to create a trigger so that no one can submit an answer to the same question twice.
It has to be a trigger only.
CREATE TRIGGER trigger_Check_Duplicates
ON submit_Answer
FOR INSERT
AS
IF SELECT???
PRINT 'duplicate'
raiserror('cant submit answer to same question twice')
ROLLBACK
End

Create trigger
CREATE TRIGGER dbo.uniqueUserQuestion
ON dbo.submit_Answer
INSTEAD OF INSERT
AS
BEGIN
SET NOCOUNT ON
IF EXISTS
(
SELECT 1
FROM dbo.submit_Answer T
INNER JOIN INSERTED I
ON T.user_name = I.user_name
AND T.question_id = I.question_id
)
BEGIN
-- Do dupe handling here
PRINT 'duplicate'
raiserror('cant submit answer to same question twice')
return
END
-- actually add it in
INSERT INTO
dbo.submit_Answer
SELECT
*
FROM
INSERTED I
END
GO

MySql does not support INSTEAD OF triggers, which is what you'd need to use here. In SQL Server, you'd use an INSTEAD OF INSERT trigger that will fire before the insert occurs, where you can write a check for the duplicate. However, if you can avoid a trigger, why not use a Stored Routine and just check for the duplicate before inserting?
This is, of course, if you really, really cannot use a constraint.
Edit: Updating answer for MSSQL.
Here's an example right from MSDN:
CREATE TRIGGER IO_Trig_INS_Employee ON Employee
INSTEAD OF INSERT
AS
BEGIN
SET NOCOUNT ON
-- Check for duplicate Person. If there is no duplicate, do an insert.
IF (NOT EXISTS (SELECT P.SSN
FROM Person P, inserted I
WHERE P.SSN = I.SSN))
INSERT INTO Person
SELECT SSN,Name,Address,Birthdate
FROM inserted
ELSE
-- Log an attempt to insert duplicate Person row in PersonDuplicates table.
INSERT INTO PersonDuplicates
SELECT SSN,Name,Address,Birthdate,SUSER_SNAME(),GETDATE()
FROM inserted
-- Check for duplicate Employee. If no there is duplicate, do an INSERT.
IF (NOT EXISTS (SELECT E.SSN
FROM EmployeeTable E, inserted
WHERE E.SSN = inserted.SSN))
INSERT INTO EmployeeTable
SELECT EmployeeID,SSN, Department, Salary
FROM inserted
ELSE
--If there is a duplicate, change to UPDATE so that there will not
--be a duplicate key violation error.
UPDATE EmployeeTable
SET EmployeeID = I.EmployeeID,
Department = I.Department,
Salary = I.Salary
FROM EmployeeTable E, inserted I
WHERE E.SSN = I.SSN
END
You'll obviously need to modify/simplify for your situation, but the basic context is there.

Related

Insert trigger doesnt do what i want it to do

i made a trigger which should avoid inserting a record in the rental 'uitlening' table if the person has an overdue payment (Boete). Unfortunately it doesnt work and i cant find the reason why. 'Boete' is an attribute of another table than rental. Can someone help me?
CREATE TRIGGER [dbo].[Trigger_uitlening]
ON [dbo].[Uitlening]
FOR INSERT
AS
BEGIN
DECLARE #Boete decimal(10, 2);
SET #Boete = (SELECT Boete FROM Lid WHERE LidNr = (SELECT LidNr FROM inserted));
IF #Boete = 0
BEGIN
INSERT INTO Uitlening
SELECT *
FROM inserted;
END;
END;
It sounds like what you actually need is a cross-table constraint.
You can either do this by throwing an error in the trigger:
CREATE TRIGGER [dbo].[Trigger_uitlening]
ON [Rental]
AFTER INSERT
AS
SET NOCOUNT ON;
IF EXISTS (SELECT 1
FROM inserted i
INNER JOIN dbo.Person p ON i.[personID] = p.[personID]
WHERE p.[PaymentDue] <= 0
)
THROW 50001, 'PaymentDue is less than 0', 1;
A better solution is to utilize a trick with an indexed view. This is based on an article by spaghettidba.
We first create a dummy table of two rows
CREATE TABLE dbo.DummyTwoRows (dummy bit not null);
INSERT DummyTwoRows (dummy) VALUES(0),(1);
Then we create the following view:
CREATE VIEW dbo.vwPaymentLessThanZero
WITH SCHEMBINDING -- needs schema-binding
AS
SELECT 1 AS DummyOne
FROM dbo.Rental r
JOIN dbo.Person p ON p.personID = r.personID
CROSS JOIN dbo.DummyTwoRows dtr
WHERE p.PaymentDue <= 0;
This view should in theory always have no rows in it. To enforce that, we create an index on it:
CREATE UNIQUE CLUSTERED INDEX CX_vwPaymentLessThanZero
ON dbo.vwPaymentLessThanZero (DummyOne);
Now if you try to add a row that qualifies for the view, it will fail with a unique violation, because the cross-join is doubling up the rows.
Note that in practice the view index takes up no space because there are never any rows in it.
Assuming you just want to insert records into [Rental] of those users, who have [PaymentDue] <= 0. As you mentioned in your last comment:
no record in rental can be inserted if the person has a PaymentDue
thats greater than zero
And other records should be silently discarded as you didn't give a clear answer to #Larnu's question:
should that row be silently discarded, or should an error be thrown?
If above assumptions are true, your trigger would look like:
CREATE TRIGGER [dbo].[Trigger_uitlening]
ON [Rental]
INSTEAD OF INSERT
AS
BEGIN
INSERT INTO [Rental] ( [DATE], [personID], [productID])
SELECT i.[DATE], i.[personID], i.[productID]
FROM INSERTED i
INNER JOIN Person p ON i.[personID] = p.[personID]
WHERE p.[PaymentDue] <= 0
END;
Attention! When you create a trigger by FOR INSERT or AFTER INSERT then don't write insert into table select * from inserted, because DB will insert data automatically, you can do only ROLLBACK this process. But, when creating a trigger by INSTEAD OF INSERT then you must write insert into table select * from inserted, else inserting not be doing.

Prevent duplicate data in using After Insert Trigger

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.

SQL trigger for update

I was just trying to figure out how to do a basic trigger when I updated a row
Heres the setup
CREATE TABLE marriage(
personid int
married varchar(20)
);
INSERT INTO marriage
values (1, unmarried);
What im trying to do is create a sql trigger that will make it so that when I update a person can only go from married to divorced but not unmarried to divorced.
If anyone can help me with structuring this that would be great
This is what I was looking for if someone was looking for something similar
alter trigger
trigtest3
on married
for update
as
begin
declare #old varchar(20)
declare #new varchar(20)
select #old = married from deleted
select #new = married from inserted
if(#old like 'Unmarried' AND #new like 'Divorced')
rollback
end
SQL Server doesn't provide per-row triggers unfortunately, but only triggers for a complete command. And one single update command can update several rows, so you must look whether at least one affected row has undergone a forbidden change. You do this by joining the deleted and inserted pseudo tables on a column or a combination of columns that uniquely identify a record (i.e. the primary key).
create trigger trg_upd_married on marriage for update as
begin
declare #error_count int
select #error_count = count(*)
from deleted d
join inserted i on i.id = d.id
where d.married = 'Unmarried'
where i.married = 'Divorced'
if #error_count > 0
begin
raiserror('Unmarried persons cannot get divorced.', 16, 121)
rollback transaction
end
end;
The above trigger may still have errors. I am not fluent with TSQL (and just notice that I find its triggers quite clumsy - at least compared to Oracle's triggers I am used to).
You need to use instead of triggers as you need to prevent update. For update triggers are run after the insert happens. Use the following code -
create trigger abc on marriage
for instead of update
as
begin
Begin transaction
if exists(select 1 from deleted as a
inner join inserted as b
on a.personid = b.personid
where a.married = 'unmarried' and b.married = 'Divorced')
begin
raiserror('Status can not be changed from unmarried to Divorced',16,1)
Rollback transaction
end
else
begin
update a
set a.married = b.married
from marriage as a
inner join inserted as b
on a.personid = b.personid
Commit transaction
end
end
Let me know if this helps

SQL Server : delete row if no constraint error

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 !

MSSQL Trigger if id exist

I have problem with MSSQL trigger. I want to create trigger which will be check if id exist in other table and if it will be exist it will not insert a record.
Example
We have Person, Person could be a Student or Teacher, but it can't be the Student and Teacher in the same time. So I need to check before insert and update if Teacher exist with the same id like Student that I trying to insert. I am not sure is this clear?
I try to found answer, I found some triggers but not what I want. Could you help me with this?
I attache UML diagram with my problem:
UML Diagram
With help of Muhammed Ali I wrote this:
CREATE Trigger tr_TriggerName
ON Student
INSTEAD OF INSERT
AS
BEGIN
SET NOCOUNT ON;
IF NOT EXISTS
(SELECT Teacher.id
FROM Teacher
WHERE Teacher.id =
(SELECT inserted.id FROM inserted))
BEGIN
/* This isnert is wrong I don't know how to write it?????*/
INSERT INTO Student VALUES (inserted.id, inserted.field1, inserted.field2);
END
ELSE
RAISERROR ('There is already a Teacher with the same Student id', 0, 0);
RETURN
END
This is exactly what I want, but I don't know how to write this insert.
I wrote earlier that Person could be a Student or Teacher, but it can't be a Student and Teacher in the same time. So if I want to insert Student to my table I must check table which Teatchers. If Teacher exist with the same Persson id (like Student that I try to insert) I must show error message. If (or else..:)) Teacher not exist I need to insert him, but I don't konow how to write this insert in my trigger.
Try this for an alternative:
CREATE Trigger tr_TriggerName
ON Student
INSTEAD OF INSERT
AS
BEGIN
declare #count int
select #count=count(*) from inserted
insert into Student (id,field1,field2)
select id,field1,field2 from inserted
where id not in (select id from teacher)
if ##ROWCOUNT<#count
raiserror('Some Teachers were rejected due to conflicts with Student ids', 0, 0);
END
EDIT: I modified it a bit, for the error message to cover partial failure of multiple row insert. In order to display an error for each rejected id, a cursor should be employed.
I made it this way, without rewriting INSERT logic, so the same trigger can be used on INSERT and on UPDATE.
CREATE TRIGGER tr_StudentNotTeacher
ON Student
AFTER INSERT, UPDATE
AS
IF EXISTS ( SELECT * FROM Teacher T
JOIN inserted i ON i.id=T.id
WHERE T.id IS NOT NULL )
BEGIN
RAISERROR('Person cannot be Student and Teacher at the same time', 16, 1);
ROLLBACK TRANSACTION;
RETURN
END;
Need the same (analogous) trigger on Teacher table.