SQL Server : After Insert Trigger for update another Table - sql

I wrote this trigger to insert data into another table after data insert into the CHECKINOUT table.
But it doesn't insert data into the Att_process table. No errors is showing up in SQL Server. Can you help me to figure this out problem?
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TRIGGER [dbo].[trgAfterInsert] ON [dbo].[CHECKINOUT]
AFTER INSERT
AS
DECLARE #uid INT;
DECLARE #checkin DATETIME;
SELECT #uid = i.USERID
FROM [CHECKINOUT] i;
SELECT #checkin = i.CHECKTIME
FROM [CHECKINOUT] i;
IF(DATEPART(HOUR, #checkin) < 12)
BEGIN
INSERT INTO Att_process (USERID, checkin_time)
VALUES (#uid, #checkin);
END;
ELSE
BEGIN
INSERT INTO Att_process (USERID, checkout_time)
VALUES (#uid, #checkin);
END;

Two main problems:
you're not looking at the Inserted pseudo table which contains the newly inserted rows
you're assuming the trigger is called once per row - this is not the case, the trigger is called once per statement and the Inserted pseudo table will contain multiple rows - and you need to deal with that
So try this code instead:
CREATE TRIGGER [dbo].[trgAfterInsert] ON [dbo].[CHECKINOUT]
AFTER INSERT
AS
-- grab all the rows from the "Inserted" pseudo table
-- and insert into the "checkin_time" column, if the value
-- of the HOUR is less than 12
INSERT INTO Att_process (USERID, checkin_time)
SELECT
i.USERID, i.CHECKTIME
FROM
Inserted i
WHERE
DATEPART(HOUR, i.CHECKTIME) < 12
-- grab all other rows (HOUR is greater than or equal to 12)
-- and insert into the "checkout_time" column
INSERT INTO Att_process (USERID, checkout_time)
SELECT
i.USERID, i.CHECKTIME
FROM
Inserted i
WHERE
DATEPART(HOUR, i.CHECKTIME) >= 12

As many SQL developers did, you had experienced the same error with SQL Server triggers. You have missed the case when multiple insert statements are executed on the table.
Triggers are executed only once for the SQL statement, not for each row.
So the code in the trigger should be able to cover all tasks for all rows affected by the SQL statement.
Here is a sample how you can alter your trigger code.
ALTER TRIGGER [dbo].[trgAfterInsert] ON [dbo].[CHECKINOUT]
AFTER INSERT
AS
INSERT INTO Att_process (USERID, checkin_time, checkout_time)
select
USERID,
case when DATEPART(HOUR, CHECKTIME) < 12 then CHECKTIME else null end,
case when DATEPART(HOUR, CHECKTIME) < 12 then null else CHECKTIME end
FROM inserted
For obtaining stable results, please use the Inserted and Deleted internal tables which are available within trigger codes.
You can further check sample SQL Trigger to log changes for how Deleted and Inserted tables are used.
I hope that helps you,
One last note, if you can use SQL Output clause to insert data into two tables at the same time.
But of course triggers work on table base, so where ever the insert statement is executed triggers work. If you use Output clause, you should guarantee that the only SQL statement which inserts data into that table will be it to maintain consistency between two tables.

Try this, since you are not using 'inserted' magic table to extract last inserted data.
DECLARE #uid INT;
DECLARE #checkin DATETIME;
SELECT #uid = USERID FROM inserted
SELECT #checkin = CHECKTIME FROM inserted

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.

SQL Trigger after update on a single column in different table

i'm creating a SQL trigger which I need to insert a record into a different table after updating it on the first table.
I got the following tables.
Queue table:
Bin table
Till now i've the following SQL query which doesn't compile.
CREATE TRIGGER dbo.MoveBinToQueue
ON dbo.Bins
AFTER UPDATE
AS
IF UPDATE(Status_StatusId)
BEGIN
INSERT INTO dbo.Queues
(
Prio,
Time
Bin_BinId,
Goal_GoalId
)
SELECT
20,
GETDATE(),
inserted.Status_StatusId,
inserted.Goal_GoalId
)
END
You need a FROM clause:
CREATE TRIGGER dbo.MoveBinToQueue
ON dbo.Bins
AFTER UPDATE
AS
BEGIN
IF UPDATE(Status_StatusId)
BEGIN
INSERT INTO dbo.Queues (Prio, Time, Bin_BinId, Goal_GoalId )
SELECT 20, GETDATE(), inserted.Status_StatusId, inserted.Goal_GoalId
FROM inserted;
END
END;

How to get a inserted id in other table from inside a trigger?

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.

SQL After Delete Trigger has Null values in the Deleted Pseudotable?

Im trying to make this trigger work when trying to delete a record. The way it is suposed to work is, when someone tries to delete a record it rollbacks and inserts an audit record to TbAudit table which by the way, all columns have a NOT NULL constraint. However, turns out it wont do it, because for some reason I dont understand when I try to delete a record it will display the message and rollback BUT all my variables within the select statement are getting NULL values even though Im pulling them directly from the "deleted" table. Please help.
USE BdPlan
GO
CREATE TRIGGER TrAudit
ON Plan.TPlan
AFTER DELETE
AS
BEGIN
DECLARE #IdPlan = int,
#IdEmployee int,
#Month int,
#Year int
ROLLBACK
PRINT 'CANT DELETE RECORDS'
-- All variables are getting NULL
SELECT #IdPlan = D.IdPlan,
#IdEmployee = D.IdEmployee ,
#Month = D.Month,
#Year = D.Year
FROM deleted AS D
INSERT INTO BdAudit.dbo.TbAudit
VALUES
(
#IdPlan,
#IdEmployee,
#Month,
#Year,
SUSER_NAME(),
GETDATE()
)
END
I believe there may be problems with this approach:
you are trying to access the DELETED pseudotable after the transaction has been rolled back - it will have zero rows after the rollback (see below)
your trigger only attempts to deal with a single row deletion - it should be able to handle multi row deletes
It is also noted that inserting directly into the Audit table from the Deleted pseudotable before ROLLBACK will of course roll the audit data back as well.
From here it is apparent you can cache the data to be audited in a #Temporary table variable, then do the ROLLBACK (which doesn't undo the #Temp table), and then do the Audit insertion:
ALTER trigger d_foo ON FOO AFTER DELETE
AS BEGIN
DECLARE #Temp AS TABLE
(
ID INT,
-- Obviously add all your fields go here
);
INSERT INTO #Temp(ID)
SELECT ID FROM DELETED;
ROLLBACK TRAN;
insert into fooaudit(id)
select id from #Temp;
END;
Simplified SqlFiddle here with multiple row deletion.
To confirm, the DELETED pseudotable contains zero rows after a ROLLBACK in a trigger, as this modified Fiddle demonstrates.

SQL Insert Trigger - Update other record

I'm working on a small database in SQL Server 2008 to track employee changes. I'm having trouble with an Insert Trigger at the moment. What I want to happen, is that when a "Record" is inserted into the Record table, it finds the previous open record (i.e. one without an end date) for that Employee (EmpID), if there is one, and updates it with an EndDate - which will be calculated as the day before the inserted StartDate. Here is what I have tried, to no success:
CREATE TRIGGER [dbo].[trgInsertRecord] ON [dbo].[Record]
FOR INSERT
AS
declare #EmpID int;
declare #StartDate date;
declare #EndDate date;
select #EmpID=i.EmpID from inserted i;
select #StartDate=i.RealStart from inserted i;
set #EndDate=DATEADD(DAY,-1,#StartDate)
UPDATE Record
SET RealEnd=#EndDate
WHERE EmpID=#EmpID AND RealEnd=NULL;
Can somebody please help me understand my mistake?
Assuming only a single row exists for a given EmpID and NULL RealEnd, you can join to the inserted table for the update operation like the example below. The best practice is to code triggers as to handle multiple rows updated by a single statement so you should avoid local scalar subqueries in triggers.
CREATE TRIGGER [dbo].[trgInsertRecord] ON [dbo].[Record]
FOR INSERT
AS
UPDATE r
SET RealEnd = DATEADD(DAY, -1, i.RealStart)
FROM Record AS r
JOIN inserted AS i ON
r.EmpID=i.EmpID
AND RealEnd IS NULL;