Although this is completed successfully on completion, it is not having the desired update.
CREATE TRIGGER Trigger1
On dbo.[table1]
FOR UPDATE
AS
Declare #Id int;
SELECT #Id = Issue_Id FROM dbo.[table1]
INSERT INTO dbo.[storage]
SELECT Id, Title, project, Problem
FROM dbo.[table2]
WHERE Id = #Id
Is there something I am doing wrong or that I can't use variables within the scope of a trigger?
Many thanks
To support multirow updates
CREATE TRIGGER Trigger1 On dbo.[table1] FOR UPDATE
AS
SET NOCOUNT ON
INSERT INTO dbo.[storage]
SELECT t.Id, t.Title, t.project, t.Problem
FROM dbo.[table2] t
JOIN INSERTED I ON t.ID = I.ID
GO
If table2 is actually table1 (which makes more sense: how is table1 related to storage and table2?)...
CREATE TRIGGER Trigger1 On dbo.[table1] FOR UPDATE
AS
SET NOCOUNT ON
INSERT INTO dbo.[storage]
SELECT Id, Title, project, Problem
FROM INSERTED
GO
To handle multple updates and the inserted table in one go:
CREATE TRIGGER Trigger1
On dbo.[table1]
FOR UPDATE
AS
INSERT INTO dbo.[storage]
SELECT Id, Title, project, Problem
FROM dbo.[table2] t2
JOIN Inserted i ON i.Issue_ID = t2.Id
Please go through the below suggestion.
Instead of the below line
SELECT #Id = Issue_Id FROM dbo.[table1]
It had to be following.
SELECT Issue_Id FROM Inserted
Following is the updated one.
CREATE TRIGGER Trigger1
On dbo.[table1]
FOR UPDATE
AS
SET NOCOUNT ON
Declare #Id int;
With CTE as
(
SELECT Issue_Id FROM Inserted I
Inner Join [table1] T on T.Issue_Id = I.Issue_Id
)
INSERT INTO dbo.[storage]
SELECT Id, Title, project, Problem
FROM dbo.[table2]
Inner Join CTE c on c.Issue_Id = Id
For more information
In SQL server the records which are being inserted / modified or deleted occupies themselves in two temporary tables available in a DML trigger. These tables are INSERTED and DELETED. The INSERTED table has inserted or updated records. The DELETED table has the old state of the records being updated or deleted.
The others have correctly answered that you should be using inserted and a join, to build a proper trigger. But:
Based on your comments to other's answers - you should never attempt to access any resource outside of your own database from a trigger, let along from another server.
Try to decouple the trigger activity from the cross server activity - say have your trigger add a row to a queue table (or use real service broker queues), and have an independent component be responsible for servicing these requests.
Otherwise, if there are any e.g. network issues, not only does your trigger break, but it forces a rollback for the original update also - it makes your local database unusable.
This also means that the independent component can cope with timeouts, and perform appropriate retries, etc.
below line should be removed
SELECT #Id = Issue_Id FROM dbo.[table1]
It should be following.
SELECT Issue_Id FROM Inserted
Related
I am trying to insert values into another table with an after update trigger in the SQL server.
I have a table where I am storing my employees (employee_id, name, exam_results, exam_date) and I need to make a trigger that allows me to insert employee_id, exam_results, and exam_date into a table called exams_backlog whenever exam_results or exam_date is updated in table employees.
I don't know if I am able to check those fields to make the insert.
This is what I have so far:
CREATE TRIGGER updateExamBacklogTrigger
ON employees
AFTER UPDATE
AS
BEGIN
*****HERE IS WHERE I NEED TO CHECK IF ONE OF THOSE TWO FIELDS HAS BEEN UPDATED*****
INSERT INTO exams_backlog SELECT e.employee_id, e.exam_results, e.exam_date FROM employees e
SET NOCOUNT ON;
END
GO
Thanks in advance.
I have found the solution I've been looking for:
As I wanted to check if exam_results or exam_date had been updated I have used
IF(UPDATE(exam_results) OR UPDATE(exam_date ))
And then in order to only insert data for those employees that had been updated I used an
INNER JOIN inserted i ON i.employee_id = e.employee_id
So the complete trigger results as this:
CREATE TRIGGER updateExamBacklogTrigger
ON employees
AFTER UPDATE
AS
BEGIN
IF(UPDATE(exam_results) OR UPDATE(exam_date ))
INSERT INTO exams_backlog SELECT e.employee_id, e.exam_results, e.exam_date FROM employees e INNER JOIN inserted i ON i.employee_id = e.employee_id
SET NOCOUNT ON;
END
GO
Hej,
Update
Thank you guys for your answers and hints. I understand that my first attempt was wrong - so maybe the better approach is to describe my problem in words rather than trying a shitty trigger.
Table A is a list of all clients. For earch client exists multiple orders (next to other not needed information in that table):
CLIENT
ORDER
OPTIONAL
A
1
NO
A
2
YES
A
3
NO
B
16818
YES
B
342
YES
I need to insert all OPTIONAL=NO orders into table B in an automatic bulk process. It is possible that an order is changed from OPTIONAL=NO to OPTIONAL=YES and therefore i need the solutions for not only inserts but updates.
Thank you very much!
Best practice for triggers:
Don't create them unless you have no other option.
It's usually best to separate insert and update triggers, sometimes they can be combined.
NOCOUNT stops spurious messages going back to the client, XACT_ABORT means any error automatically rolls back the transaction.
Check if there are any relevant rows, if not bail out early.
Also check if a column is present in the update statement using the UPDATE() function if relevant (this doesn't mean the row has actually changed)
In an UPDATE trigger, make sure you compare inserted and deleted rows for actual changes.
Most important: be aware that inserted and deleted may contain multiple rows.
CREATE TRIGGER Trg_TableA_OPTIONAL_ins
ON TableA
AFTER INSERT
AS
SET NOCOUNT, XACT_ABORT ON;
IF (NOT EXISTS (SELECT 1 FROM inserted WHERE OPTIONAL = 'NO'))
RETURN;
INSERT TableB (CLIENT, [ORDER])
SELECT CLIENT, [ORDER]
FROM inserted
WHERE OPTIONAL = 'NO';
GO
CREATE TRIGGER Trg_TableA_OPTIONAL_upd
ON TableA
AFTER UPDATE
AS
SET NOCOUNT, XACT_ABORT ON;
IF (NOT EXISTS (SELECT 1 FROM inserted))
RETURN;
INSERT TableB (CLIENT, [ORDER])
SELECT CLIENT, [ORDER]
FROM (
SELECT CLIENT, [ORDER], OPTIONAL
FROM inserted
WHERE OPTIONAL = 'NO'
EXCEPT
SELECT CLIENT, [ORDER], OPTIONAL
FROM deleted
) i
EXCEPT
SELECT CLIENT, [ORDER]
FROM TableB;
DELETE FROM b
FROM TableB b
JOIN (
SELECT CLIENT, [ORDER], OPTIONAL
FROM inserted
WHERE OPTIONAL = 'YES'
EXCEPT
SELECT CLIENT, [ORDER], OPTIONAL
FROM deleted
) i
ON TableB.CLIENT = Source.CLIENT AND TABLEB.[ORDER] = Source.[ORDER];
GO
Update: Just to eliminate one possible suggestion, I saved the first query as a view and substituted it in the trigger. No change in behavior has been observed. I have updated the code below to reflect the new arrangement.
I have just started working with SQL Server triggers and have had some success. The current bit that I'm working on has me confused.
I am working with two tables. One is an order header and the other contains the line items. The trigger is watching for a new row to be inserted in the line item table and, if the item added meets certain criteria, update a value in the order header table. When I break my query down and run it separately, I get what I expect. A list of orders that contain the items I am watching for.
--QUERY WORKS AS EXPECTED
SELECT ord_no, cmt_cd_1
FROM OrderHeader
WHERE ord_no IN
(SELECT DISTINCT OrderHeader.ord_no
FROM OrderLine INNER JOIN OrderHeader
ON OrderLine.ord_no = OrderHeader.ord_no
WHERE (OrderLine.item_no LIKE 'A%') OR
(OrderLine.item_no LIKE 'B%') OR
(OrderLine.item_no LIKE 'C30%'))
However, when I try to use this in a trigger, it doesn't work.
--TRIGGER DOES NOT UPDATE ANY RECORDS
CREATE TRIGGER add_cmt_cd
ON [OrderLine]
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON;
UPDATE OrderHeader
SET cmt_cd_1 = 'O'
FROM OrderHeader
INNER JOIN inserted ON OrderHeader.ord_no = inserted.ord_no
WHERE (OrderHeader.ord_no IN (SELECT OrderListView.ord_no
FROM OrderListView))
END
I have verified that the header record is created first and does exist before the line item records are created. I have another trigger on this table that works fine, though the value it updates is in the same table as the trigger.
--TRIGGER WORKS AS DESIGNED
CREATE TRIGGER add_line_cmt
ON [OrderLine]
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON;
UPDATE OrderLine
SET cmt_1 = 'Comment 1',
cmt_2 = 'Comment 2'
FROM OrderLine
INNER JOIN inserted ON inserted.ord_no = OrderLine.ord_no
WHERE (OrderLine.item_no IN (SELECT CAST(item_no AS CHAR) AS item
FROM ItemTable
WHERE (group = 3)))
END
I've been banging my head against the desk for 3 hours and I know it's something simple I've missed. I'm hoping some other sets of eyes will spot it before I go nuts.
The trigger below select ID's from one table (employeeInOut), sums int's in a column in that table matching all ID's, and is supposed to insert these in another table (monthlyHours). I can't figure out if this is a syntax problem (nothing shows up in intellisense), and all it says is trigger executed successfully - nothing is inserted.
Trigger ->
GO
CREATE TRIGGER empTotalsHoursWorked
ON employeeInOut
FOR INSERT, DELETE, UPDATE
AS
BEGIN
INSERT INTO monthlyHours(employeeID, monthlyHours)
SELECT (SELECT employeeID FROM employeeInOut),
SUM(dailyHours) AS monthlyHours
FROM employeeInOut
WHERE employeeInOut.employeeID=(SELECT employeeID FROM monthlyHours)
END
GO
I have re-worked this trigger many times and this is the one with no errors, however nothing is inserted, and results seem to be nothing. Any advice, answers please appreciated.
Going with a couple of assumptions here one being that monthlyHours table contains employeeID and monthlyhours.
With that being said I think you are going to need multiple triggers depending on the action. Below is an example based on insert into the employeeInOut table
GO
CREATE TRIGGER empTotalsHoursWorked
ON employeeInOut
AFTER INSERT
AS
BEGIN
DECLARE #employeeID INT
DECLARE #monthlyHours INT
SELECT #employeeID = INSERTED.employeeID
FROM INSERTED
SELECT #monthlyHours = SUM(dailyHours)
FROM employeeInOut
WHERE employeeInOut.employeeID = #employeeID
INSERT INTO monthlyHours(employeeID,monthlyHours)
values (#employeeID, #monthlyHours)
END
GO
This will insert a new row of course. You may want to modify this to update the row if the row already exists in the monthlyHours table for that employee.
I would really advise against a trigger for a simple running total like this, your best option would be to create a view. Something like:
CREATE VIEW dbo.MonthlyHours
AS
SELECT EmployeeID,
monthlyHours = SUM(dailyHours)
FROM dbo.employeeInOut
GROUP BY EmployeeID;
GO
Then you can access it in the same way as your table:
SELECT *
FROM dbo.MonthlyHours;
If you are particularly worried about performance, then you can always index the view:
CREATE VIEW dbo.MonthlyHours
WITH SCHEMABINDING
AS
SELECT EmployeeID,
monthlyHours = SUM(dailyHours),
RecordCount = COUNT_BIG(*)
FROM dbo.employeeInOut
GROUP BY EmployeeID;
GO
CREATE UNIQUE CLUSTERED INDEX UQ_MonthlyHours__EmployeeID ON dbo.MonthlyHours(EmployeeID);
Now whenever you add or remove records from employeeInOut SQL Server will automatically update the clustered index for the view, you just need to use the WITH (NOEXPAND) query hint to ensure that you aren't running the query behind the view:
SELECT *
FROM dbo.MonthlyHours WITH (NOEXPAND);
Finally, based on the fact the table is called monthly hours, I am guessing it should be by month, as such I assume you also have a date field in employeeInOut, in which case your view might be more like:
CREATE VIEW dbo.MonthlyHours
WITH SCHEMABINDING
AS
SELECT EmployeeID,
FirstDayOfMonth = DATEADD(MONTH, DATEDIFF(MONTH, 0, [YourDateField]), 0),
monthlyHours = SUM(dailyHours),
RecordCount = COUNT_BIG(*)
FROM dbo.employeeInOut
GROUP BY EmployeeID, DATEADD(MONTH, DATEDIFF(MONTH, 0, [YourDateField]), 0);
GO
CREATE UNIQUE CLUSTERED INDEX UQ_MonthlyHours__EmployeeID_FirstDayOfMonth
ON dbo.MonthlyHours(EmployeeID, FirstDayOfMonth);
And you can use the view in the same way described above.
ADDENDUM
For what it is worth, for your trigger to work properly you need to consider all cases:
Inserting a record where that employee already exists in MonthlyHours (Update existing).
Inserting a record where that employee does not exist in MonthlyHours (insert new).
Updating a record (update existing)
Deleting a record (update existing, or delete)
To handle all of these cases you can use MERGE:
CREATE TRIGGER empTotalsHoursWorked
ON employeeInOut
FOR INSERT, DELETE, UPDATE
AS
BEGIN
WITH ChangesToMake AS
( SELECT EmployeeID, SUM(dailyHours) AS MonthlyHours
FROM ( SELECT EmployeeID, dailyHours
FROM Inserted
UNION ALL
SELECT EmployeeID, -dailyHours
FROM deleted
) AS t
GROUP BY EmployeeID
)
MERGE INTO monthlyHours AS m
USING ChangesToMake AS c
ON c.EmployeeID = m.EmployeeID
WHEN MATCHED THEN UPDATE
SET MonthlyHours = c.MonthlyHours
WHEN NOT MATCHED BY TARGET THEN
INSERT (EmployeeID, MonthlyHours)
VALUES (c.EmployeeID, c.MonthlyHours)
WHEN NOT MATCHED BY SOURCE THEN
DELETE;
END
GO
I have a table where I created an INSTEAD OF trigger to enforce some business rules.
The issue is that when I insert data into this table, SCOPE_IDENTITY() returns a NULL value, rather than the actual inserted identity.
Insert + Scope code
INSERT INTO [dbo].[Payment]([DateFrom], [DateTo], [CustomerId], [AdminId])
VALUES ('2009-01-20', '2009-01-31', 6, 1)
SELECT SCOPE_IDENTITY()
Trigger:
CREATE TRIGGER [dbo].[TR_Payments_Insert]
ON [dbo].[Payment]
INSTEAD OF INSERT
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
IF NOT EXISTS(SELECT 1 FROM dbo.Payment p
INNER JOIN Inserted i ON p.CustomerId = i.CustomerId
WHERE (i.DateFrom >= p.DateFrom AND i.DateFrom <= p.DateTo) OR (i.DateTo >= p.DateFrom AND i.DateTo <= p.DateTo)
) AND NOT EXISTS (SELECT 1 FROM Inserted p
INNER JOIN Inserted i ON p.CustomerId = i.CustomerId
WHERE (i.DateFrom <> p.DateFrom AND i.DateTo <> p.DateTo) AND
((i.DateFrom >= p.DateFrom AND i.DateFrom <= p.DateTo) OR (i.DateTo >= p.DateFrom AND i.DateTo <= p.DateTo))
)
BEGIN
INSERT INTO dbo.Payment (DateFrom, DateTo, CustomerId, AdminId)
SELECT DateFrom, DateTo, CustomerId, AdminId
FROM Inserted
END
ELSE
BEGIN
ROLLBACK TRANSACTION
END
END
The code worked before the creation of this trigger. I am using LINQ to SQL in C#. I don't see a way of changing SCOPE_IDENTITY to ##IDENTITY. How do I make this work?
Use ##identity instead of scope_identity().
While scope_identity() returns the last created id in the current scope, ##identity returns the last created id in the current session.
The scope_identity() function is normally recommended over the ##identity field, as you usually don't want triggers to interfer with the id, but in this case you do.
Since you're on SQL 2008, I would highly recommend using the OUTPUT clause instead of one of the custom identity functions. SCOPE_IDENTITY currently has some issues with parallel queries that cause me to recommend against it entirely. ##Identity does not, but it's still not as explicit, and as flexible, as OUTPUT. Plus OUTPUT handles multi-row inserts. Have a look at the BOL article which has some great examples.
I was having serious reservations about using ##identity, because it can return the wrong answer.
But there is a workaround to force ##identity to have the scope_identity() value.
Just for completeness, first I'll list a couple of other workarounds for this problem I've seen on the web:
Make the trigger return a rowset. Then, in a wrapper SP that performs the insert, do INSERT Table1 EXEC sp_ExecuteSQL ... to yet another table. Then scope_identity() will work. This is messy because it requires dynamic SQL which is a pain. Also, be aware that dynamic SQL runs under the permissions of the user calling the SP rather than the permissions of the owner of the SP. If the original client could insert to the table, he should still have that permission, just know that you could run into problems if you deny permission to insert directly to the table.
If there is another candidate key, get the identity of the inserted row(s) using those keys. For example, if Name has a unique index on it, then you can insert, then select the (max for multiple rows) ID from the table you just inserted to using Name. While this may have concurrency problems if another session deletes the row you just inserted, it's no worse than in the original situation if someone deleted your row before the application could use it.
Now, here's how to definitively make your trigger safe for ##Identity to return the correct value, even if your SP or another trigger inserts to an identity-bearing table after the main insert.
Also, please put comments in your code about what you are doing and why so that future visitors to the trigger don't break things or waste time trying to figure it out.
CREATE TRIGGER TR_MyTable_I ON MyTable INSTEAD OF INSERT
AS
SET NOCOUNT ON
DECLARE #MyTableID int
INSERT MyTable (Name, SystemUser)
SELECT I.Name, System_User
FROM Inserted
SET #MyTableID = Scope_Identity()
INSERT AuditTable (SystemUser, Notes)
SELECT SystemUser, 'Added Name ' + I.Name
FROM Inserted
-- The following statement MUST be last in this trigger. It resets ##Identity
-- to be the same as the earlier Scope_Identity() value.
SELECT MyTableID INTO #Trash FROM MyTable WHERE MyTableID = #MyTableID
Normally, the extra insert to the audit table would break everything, because since it has an identity column, then ##Identity will return that value instead of the one from the insertion to MyTable. However, the final select creates a new ##Identity value that is the correct one, based on the Scope_Identity() that we saved from earlier. This also proofs it against any possible additional AFTER trigger on the MyTable table.
Update:
I just noticed that an INSTEAD OF trigger isn't necessary here. This does everything you were looking for:
CREATE TRIGGER dbo.TR_Payments_Insert ON dbo.Payment FOR INSERT
AS
SET NOCOUNT ON;
IF EXISTS (
SELECT *
FROM
Inserted I
INNER JOIN dbo.Payment P ON I.CustomerID = P.CustomerID
WHERE
I.DateFrom < P.DateTo
AND P.DateFrom < I.DateTo
) ROLLBACK TRAN;
This of course allows scope_identity() to keep working. The only drawback is that a rolled-back insert on an identity table does consume the identity values used (the identity value is still incremented by the number of rows in the insert attempt).
I've been staring at this for a few minutes and don't have absolute certainty right now, but I think this preserves the meaning of an inclusive start time and an exclusive end time. If the end time was inclusive (which would be odd to me) then the comparisons would need to use <= instead of <.
Main Problem : Trigger and Entity framework both work in diffrent scope.
The problem is, that if you generate new PK value in trigger, it is different scope. Thus this command returns zero rows and EF will throw exception.
The solution is to add the following SELECT statement at the end of your Trigger:
SELECT * FROM deleted UNION ALL
SELECT * FROM inserted;
in place of * you can mention all the column name including
SELECT IDENT_CURRENT(‘tablename’) AS <IdentityColumnname>
Like araqnid commented, the trigger seems to rollback the transaction when a condition is met. You can do that easier with an AFTER INSTERT trigger:
CREATE TRIGGER [dbo].[TR_Payments_Insert]
ON [dbo].[Payment]
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON;
IF <Condition>
BEGIN
ROLLBACK TRANSACTION
END
END
Then you can use SCOPE_IDENTITY() again, because the INSERT is no longer done in the trigger.
The condition itself seems to let two identical rows past, if they're in the same insert. With the AFTER INSERT trigger, you can rewrite the condition like:
IF EXISTS(
SELECT *
FROM dbo.Payment a
LEFT JOIN dbo.Payment b
ON a.Id <> b.Id
AND a.CustomerId = b.CustomerId
AND (a.DateFrom BETWEEN b.DateFrom AND b.DateTo
OR a.DateTo BETWEEN b.DateFrom AND b.DateTo)
WHERE b.Id is NOT NULL)
And it will catch duplicate rows, because now it can differentiate them based on Id. It also works if you delete a row and replace it with another row in the same statement.
Anyway, if you want my advice, move away from triggers altogether. As you can see even for this example they are very complex. Do the insert through a stored procedure. They are simpler and faster than triggers:
create procedure dbo.InsertPayment
#DateFrom datetime, #DateTo datetime, #CustomerId int, #AdminId int
as
BEGIN TRANSACTION
IF NOT EXISTS (
SELECT *
FROM dbo.Payment
WHERE CustomerId = #CustomerId
AND (#DateFrom BETWEEN DateFrom AND DateTo
OR #DateTo BETWEEN DateFrom AND DateTo))
BEGIN
INSERT into dbo.Payment
(DateFrom, DateTo, CustomerId, AdminId)
VALUES (#DateFrom, #DateTo, #CustomerId, #AdminId)
END
COMMIT TRANSACTION
A little late to the party, but I was looking into this issue myself. A workaround is to create a temp table in the calling procedure where the insert is being performed, insert the scope identity into that temp table from inside the instead of trigger, and then read the identity value out of the temp table once the insertion is complete.
In procedure:
CREATE table #temp ( id int )
... insert statement ...
select id from #temp
-- (you can add sorting and top 1 selection for extra safety)
drop table #temp
In instead of trigger:
-- this check covers you for any inserts that don't want an identity value returned (and therefore don't provide a temp table)
IF OBJECT_ID('tempdb..#temp') is not null
begin
insert into #temp(id)
values
(SCOPE_IDENTITY())
end
You probably want to call it something other than #temp for safety sake (something long and random enough that no one else would be using it: #temp1234235234563785635).