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.
Related
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.
I have 3 tables tbl_Users, tbl_Protocol and tbl_ProtocolDetails and inside of my trigger on Users, I have to inserted into Protocol and then insert into ProtocolDetails, but I don't know how work the inserted scope.
Something like that:
CREATE TRIGGER tg_Users ON tbl_Users
AFTER INSERT, UPDATE AS
BEGIN
DECLARE #UserId = Int
DECLARE #ProtocolId = Int
DECLARE #UserDetail = NVARCHAR(255)
SELECT
#UserId = user_id,
#UserDetail = user_detail + '#' + user_explanation
FROM INSERTED
INSERT INTO tbl_Protocol (user_id, inserted_date)
VALUES (#UserId, GetDate())
-- Return Inserted Id from tbl_Protocol into #ProtocolDetail then
INSERT INTO tbl_ProtocolDetails (protocol_id, protocol_details)
VALUES (#ProtocolId, #UserDetail)
END
Your trigger has a MAJOR flaw in that you seems to expect to always have just a single row in the Inserted table - that is not the case, since the trigger will be called once per statement (not once for each row), so if you insert 20 rows at once, the trigger is called only once, and the Inserted pseudo table contains 20 rows.
Therefore, code like this:
Select #UserId = user_id,
#UserDetail = user_detail + '#' + user_explanation
From INSERTED;
will fail, since you'll retrieve only one (arbitrary) row from the Inserted table, and you'll ignore all other rows that might be in Inserted.
You need to take that into account when programming your trigger! You have to do this in a proper, set-based fashion - not row-by-agonizing-row stlye!
Try this code:
CREATE TRIGGER tg_Users ON tbl_Users
AFTER INSERT, UPDATE AS
BEGIN
-- declare an internal table variable to hold the inserted "ProtocolId" values
DECLARE #IdTable TABLE (UserId INT, ProtocolId INT);
-- insert into the "tbl_Protocol" table from the "Inserted" pseudo table
-- keep track of the inserted new ID values in the #IdTable
INSERT INTO tbl_Protocol (user_id, inserted_date)
OUTPUT Inserted.user_id, Inserted.ProtocolId INTO #IdTable(UserId, ProtocolId)
SELECT user_id, SYSDATETIME()
FROM Inserted;
-- insert into the "tbl_ProtocolDetails" table from both the #IdTable,
-- as well as the "Inserted" pseudo table, to get all the necessary values
INSERT INTO tbl_ProtocolDetails (protocol_id, protocol_details)
SELECT
t.ProtocolId,
i.user_detail + '#' + i.user_explanation
FROM
#IdTable t
INNER JOIN
Inserted i ON i.user_id = t.UserId
END
There is nothing in this trigger that would handle a multiple insert/update statement. You will need to either use one scenario that will handle multiple records or check how many records were effected with a IF ##ROWCOUNT = 1 else statement. In your example, I would just use something like
insert into tbl_Protocol(user_id, inserted_date)
select user_id, user_detail + '#' + user_explanation
From INSERTED;
As for your detail table, I see Marc corrected his answer to include the multiple lines and has a simple solution or you can create a second trigger on the tbl_Protocol. Another solution I have used in the past is a temp table for processing when I have very complicated triggers.
I understand the idea of triggers but a bit confused on how to use it to impose constraints. For example I have two tables: a student table and a book_order table that shows what book a student orders.
I want to create a trigger that will check that a student with a book order cannot be deleted from the student table.
Not sure why you would ever do this unless you wanted to write to a log or something when it happened but.....
create TRIGGER Del_Student
ON dbo.Student
INSTEAD OF DELETE
AS
BEGIN
Declare #cnt int
Select #cnt = count(*) from deleted d
Inner Join BookOrders o on d.studentId = o.studentId
if (#cnt > 0)
BEGIN
RAISERROR ('Deletions not allowed from this table when bookorders exist for student)', 16, 1)
END
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 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.