I have 2 datatriggers, one is supposed to fire after update and the other one after insert. This is how the beginning of the update trigger looks like:
USE [Database]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER TRIGGER [dbo].[UpdateTrigger]
ON [dbo].[Table1]
AFTER UPDATE
AS
BEGIN
(...)
The after insert trigger looks exactly the same, apart from the fact that there's AFTER INSERT instead of AFTER UPDATE. When I update the Table1, only update trigger is fired. However, when I insert, both triggers are fired. Why? How can I solve it?
When trigger fires there are 2 virtual tables: DELETED and INSERTED. When you insert records you have the records only in INSERTED table, however when you update the record you have old records in DELETED table and new ones in the INSERTED tables. So, basically when you do UPDATE, you are doing DELETE and INSERT. You can easily solve that by identifying if there are records in DELETED table. If there are then it was UPDATE, if there is no then it was INSERT.
I was having the same experience as JerryBox where the separate update trigger would fire when the insert trigger fired.
What I wanted was on inserting a new record an insert trigger would insert the datetime stamp when the record was created and insert the username the record was created by. Conversely, if record was updated add/update last modified datetime and last modified by username.
In my case, when a new record was inserted into the table, the insert trigger would fire and add values to fields in the SAME TABLE (i.e. CreatedBy and CreatedDateTime fields). These additions were ‘seen’ by the update trigger as an update to an existing record so ModifiedBy and ModifiedDateTime fields would also be inserted.
In other words, inserting a new record would cause both insert and update triggers to fire.
Reading the comments on the posting by Dmitrij Kultasev, I came up with the following solution:
Combine both INSERT and UPDATE triggers into a single TRIGGER xxx
ON xxx AFTER INSERT, UPDATE ...
Check for old records in the DELETED table
If records exist in DELETED table then update actions are performed
If no records exist in DELETED table then insert actions are
performed
Combining both triggers into a single trigger has outcome that update of records by an AFTER INSERT trigger does not fire the AFTER UPDATE trigger.
USE [Database]
GO
CREATE TABLE [dbo].[Table1](
[id] [int] IDENTITY(1,1) PRIMARY KEY,
[Data] [nvarchar](255) NULL,
[CreatedBy] [nvarchar](255) NULL,
[CreatedDateTime] [datetime] NULL,
[ModifiedBy] [nvarchar](255) NULL,
[ModifiedDateTime] [datetime] NULL)
GO
CREATE TRIGGER [dbo].[UpdateTrigger]
ON [dbo].[Table1]
AFTER INSERT, UPDATE
AS
BEGIN
IF EXISTS (SELECT 1 FROM deleted)
UPDATE [dbo].[Table1]
SET [ModifiedBy] = SYSTEM_USER,
[ModifiedDateTime] = SYSDATETIME()
FROM inserted i
WHERE i.id = [dbo].[Table1].id
ELSE
UPDATE [dbo].[Table1]
SET [CreatedBy] = SYSTEM_USER,
[CreatedDateTime] = SYSDATETIME()
FROM inserted i
WHERE i.id = [dbo].[Table1].id
END
GO
Edits: Formatting, Improved explanation in response to feedback from andronicus
Related
I have seen some articles mention the possibility of a Trigger on a View, triggering on either insert, updates or deletes to one of the base tables from which the View is created.
However I am not able to get a simple example to work.
CREATE TABLE [Test].[Data] (
Id INT PRIMARY KEY IDENTITY (1,1),
Data VARCHAR(255) NOT NULL,
);
GO
CREATE VIEW [Test].[View] AS SELECT * FROM [Test].[Data];
GO
CREATE TABLE [Test].[Queue] (
Id INT PRIMARY KEY IDENTITY (1,1),
DataId INT NOT NULL,
Action VARCHAR(255) NOT NULL,
Timestamp DATETIME NOT NULL,
);
GO
CREATE TRIGGER InsertTrigger ON [Test].[View] INSTEAD OF INSERT AS
BEGIN
DECLARE #DataId INT;
DECLARE #Timestamp DATETIME;
SET #DataId = (SELECT Id FROM INSERTED);
SET #Timestamp = GETDATE();
INSERT INTO [Test].[Queue] (DataId, Action, Timestamp) VALUES (#DataId, 'Insert', #Timestamp)
END
GO
ENABLE TRIGGER InsertTrigger ON [Test].[View];
GO
INSERT INTO [Test].[Data] (Data) VALUES ('Testdata');
The trigger is not firing, is the above not possible or is there something wrong with my Sql?
Edit: Although answered I would like to clarify the question. The idea was to get the trigger on the View to fire, when there was an Insert to the base table and not the View itself.
A trigger on a view will only work on inserts into that view, not on any inserts into tables to which the view references.
In your script you're not inserting into that view, you're inserting into a table.
In addition to not testing this correctly, your view is wrong. You are not considering that inserted represents multiple rows, not one.
So:
CREATE TRIGGER InsertTrigger ON [Test].[View] INSTEAD OF INSERT AS
BEGIN
INSERT INTO [Test].[Queue] (DataId, Action, Timestamp)
SELECT i.Id, 'Insert', GETDATE()
FROM Inserted;
END;
GO
INSERT INTO [Test].[View] (Data)
VALUES ('Testdata');
I've created a trigger which is to block inserted records with a date already existing in a table.
CREATE TRIGGER [dbo].[SpecialOffers_Insert]
ON [dbo].[SpecialOffers]
FOR INSERT,UPDATE
AS
SET NOCOUNT ON
IF EXISTS (SELECT * FROM inserted WHERE SPO_DateFrom IN (SELECT SPO_DateFrom FROM dbo.SpecialOffers))
BEGIN
RAISERROR('Error. ', 16, 1)
ROLLBACK TRAN
SET NOCOUNT OFF
RETURN
END
SET NOCOUNT OFF
It is added to a table:
CREATE TABLE [dbo].[SpecialOffers](
[SPO_SpoId] [int] IDENTITY(1,1) NOT NULL,
[SPO_DateFrom] [datetime] NOT NULL,
[SPO_DateTo] [datetime] NOT NULL)
The table is empty but when trying to insert such record:
INSERT INTO dbo.SpecialOffers (SPO_DateFrom, SPO_DateTo) VALUES ('2015-01-15','2015-01-15')
I got the Error from the trigger. How should I modify the trigger not to get the error?
If the goal is to block inserted records with date already existing in a table, you don't need a trigger - just create a unique constraint on the date field:
ALTER TABLE [dbo].[SpecialOffers]
ADD CONSTRAINT SpecialOffersUQ UNIQUE (SPO_DateFrom)
If you wanted a trigger to prevent overlaps, why didn't you say so:
CREATE TABLE [dbo].[SpecialOffers](
[SPO_SpoId] [int] IDENTITY(1,1) NOT NULL,
[SPO_DateFrom] [datetime] NOT NULL,
[SPO_DateTo] [datetime] NOT NULL,
constraint CK_SO_NoTimeTravel CHECK (SPO_DateFrom <= SPO_DateTo)
)
GO
CREATE TRIGGER NoOverlaps
on dbo.SpecialOffers
after insert,update
as
set nocount on
if exists (
select *
from dbo.SpecialOffers so1
inner join
dbo.SpecialOffers so2
on
so1.SPO_DateFrom < so2.SPO_DateTo and
so2.SPO_DateFrom < so1.SPO_DateTo and
so1.SPO_SpoId != so2.SPO_SpoId
inner join
inserted i
on
so1.SPO_SpoId = i.SPO_SpoId
)
begin
RAISERROR('No overlaps',16,1)
ROLLBACK
end
Examples:
--Works
INSERT INTO SpecialOffers (SPO_DateFrom,SPO_DateTo)
values ('20010101','20011231')
GO
--Fails (Trigger)
INSERT INTO SpecialOffers (SPO_DateFrom,SPO_DateTo)
values ('20010101','20011231')
GO
--Fails (Constraint)
INSERT INTO SpecialOffers (SPO_DateFrom,SPO_DateTo)
values ('20011231','20010101')
GO
--Fails (Trigger)
INSERT INTO SpecialOffers (SPO_DateFrom,SPO_DateTo)
values ('20020101','20021231'),
('20020701','20030630')
I also added a check constraint so that I didn't have to deal with nonsense data in the trigger.
You might have to change swap some of the <s for <=s or vice-versa, depending on what definition of intervals you want to use (i.e. are DateFrom and DateTo meant to be inclusive or exclusive endpoints for the interval they're describing?)
Since the trigger runs in the transaction context of the SQL statement that fired it, after this INSERT, there will be a row in your table dbo.SpecialOffers with the SPO_DateFrom values you've just inserted and the SELECT from the table will succeed ...
Therefore, the trigger will assume that there's already been a value - and it throws the error (as designed).
You could rewrite the trigger to not look at the newly inserted rows, but anything else - but as others have pointed out, a UNIQUE constraint does that much more simply
You should check if the rows you found are actually NOT the ones you have just inserted. Change the line
IF EXISTS (
SELECT * FROM inserted
WHERE SPO_DateFrom IN (
SELECT SPO_DateFrom
FROM dbo.SpecialOffers)
)
To
IF EXISTS (
SELECT * FROM inserted a
WHERE SPO_DateFrom IN (
SELECT SPO_DateFrom
FROM dbo.SpecialOffers b
WHERE a.SPO_SpoId <> b.SPO_SpoId)
)
In SQL Server 2008 R2, I am looking to create a trigger that imitates the behavior of an Oracle BEFORE INSERT trigger, where any insert that comes in has the trigger update the UPDATE_TS and CREATE_TS to the current timestamp right before the persist.
To issue I am seeing right now is the error:
An explicit value for the identity column in table 'MY_TABLE' can only be specified when a column list is used and IDENTITY_INSERT is ON
I am not sure if it is a good idea to turn SET IDENTITY INSERT table ON and then
SET IDENTITY INSERT table OFF within the trigger. Maybe that is a possible solution.
Please advise on best practice.
Example Table is called MY_TABLE:
CREATE TABLE [myschema].[MY_TABLE](
[MY_TABLE_ID] [bigint] IDENTITY(1,1) NOT NULL,
[FIELD_TO_UPDATE] [varchar](255) NOT NULL,
[CREATE_TS] [datetime] NULL,
[UPDATE_TS] [datetime] NULL),
PRIMARY KEY (MY_TABLE_ID))
Trigger:
CREATE TRIGGER my_table_create_ts_trigger
ON [mydb].myschema.MY_TABLE
INSTEAD OF INSERT
AS
BEGIN
INSERT INTO MY_TABLE([MY_TABLE_ID], [FIELD_TO_UPDATE], [CREATE_TS], [UPDATE_TS])
SELECT i.MY_TABLE_ID, i.FIELD_TO_UPDATE, GETDATE(), GETDATE()
FROM INSERTED as i
END
Not sure why you are using dynamic SQL, it's not really needed. Also, no need to do an UPDATE afterwards, you can just do:
CREATE TRIGGER my_table_create_ts_trigger
ON [mydb].myschema.MY_TABLE
INSTEAD OF INSERT
AS
BEGIN
INSERT INTO MY_TABLE(<list of every non identity column here>)
SELECT <list of every non identity column and the date here>, GETDATE()
FROM INSERTED
END
Also, you should list the columns explicitely in the INSERT and the SELECT.
I must be a glutton for punishment, but I have a better suggestion. Drop the trigger.
Change your table to:
CREATE TABLE [myschema].[MY_TABLE](
[MY_TABLE_ID] [bigint] IDENTITY(1,1) NOT NULL,
[FIELD_TO_UPDATE] [varchar](255) NOT NULL,
-- change this column to have a default:
[CREATE_TS] [datetime] NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- and this column too, I guess:
[UPDATE_TS] [datetime] NOT NULL DEFAULT CURRENT_TIMESTAMP),
PRIMARY KEY (MY_TABLE_ID))
Why would you allow those columns to be NULL? Why do you want to use an elaborate trigger to replace something that is much simpler to implement with a default constraint?
You don't need the trigger, and I don't understand what benefit it brings or why you want to replicate a BEFORE trigger. An INSTEAD OF trigger is similar, but not exactly the same thing.
I have a table that contains two not null columns Created and Updated.
I wrote corresponding triggers
ALTER TRIGGER [dbo].[tr_category_inserted] ON [dbo].[Category]
AFTER INSERT
AS
BEGIN
UPDATE Category
SET Created = GETDATE(), Updated = GETDATE()
FROM inserted
WHERE Category.ID = inserted.ID;
END
and
ALTER TRIGGER [dbo].[tr_category_updated] ON [dbo].[Category]
AFTER UPDATE
AS
BEGIN
UPDATE Category
SET Updated = GETDATE()
FROM inserted
inner join [dbo].[Category] c on c.ID = inserted.ID
END
but if I am inserting a new row I get an error
Cannot insert the value NULL into column 'Created', table
'Category'; column does not allow nulls. INSERT fails.
Insert command:
INSERT INTO [Category]([Name], [ShowInMenu], [Deleted])
VALUES ('category1', 0, 0)
How can I write such triggers without a setting to these columns to allow null?
Modify your table like this:
ALTER TABLE yourTable MODIFY COLUMN updated timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;
ALTER TABLE yourTable MODIFY COLUMN created timestamp DEFAULT 0;
Set the default for the created to column to 0. Unfortunately MySQL does not allow two timestamp columns with default CURRENT_TIMESTAMP in one table. To overcome this you just have to insert a NULL value into created column and you will have both columns to the current timestamp.
INSERT INTO yourTable (col1, created) VALUES ('whatever', NULL);
Or you set the default to a valid timestamp like
ALTER TABLE yourTable MODIFY COLUMN created timestamp DEFAULT '1970-01-01 00:00:00';
and modify your trigger like this:
ALTER TRIGGER [dbo].[tr_category_inserted] ON [dbo].[Category]
AFTER INSERT
AS
BEGIN
UPDATE Category
SET Created = GETDATE()
/* FROM inserted */ /*don't know where you got that from, do you port from SQL Server?*/
WHERE Category.ID = NEW.ID;
END
Error occurring because trigger works after insertion only, and you may not be inserting the column values Created and Updated at the time of insert.
So for eliminating the error, you can insert/populate the columns Created and Updated along with insert.
OR
You can add default value property of column. Please check the links for details
Add column, with default value, to existing table in SQL Server
Specify Default Values for Columns
Possible use INSTEAD OF trigger
CREATE TRIGGER [dbo].[tr_category_inserted] ON [dbo].[Category]
INSTEAD OF INSERT
AS
BEGIN
INSERT Category(Name, ShowInMenu, Deleted, Created, Updated)
SELECT Name, ShowInMenu, Deleted, GETDATE(), GETDATE()
FROM inserted i
END
I typically allow the last updated column to be null since I want to know if its never been altered. If you must keep both fields nullable I would add a default value to those columns like this:
ALTER TABLE [dbo].[Category] ADD DEFAULT (getdate()) FOR [Created]
GO
ALTER TABLE [dbo].[Category] ADD DEFAULT (getdate()) FOR [LastUpdated]
GO
Then you won't need the trigger for the insert.
If you really want to use a trigger anyway you will have to specify INSTEAD OF rather than AFTER and include the insert statement.
I have a table A with an Identity Column which is the primary key.
The primary key is at the same time a foreign key that points towards another table B.
I am trying to build an insert trigger that inserts into Table B the identity column that is about to be created in table A and another custom value for example '1'.
I tried using ##Identity but I keep getting a foreign key conflict. Thanks for your help.
create TRIGGER dbo.tr ON dbo.TableA FOR INSERT
AS
SET NOCOUNT ON
begin
insert into TableB
select ##identity, 1;
end
alexolb answered the question himself in the comments above. Another alternative is to use the IDENT_CURRENT function instead of selecting from the table. The drawback of this approach is that it always starts your number one higher than the seed, but that is easily remedied by setting the seed one unit lower. I think it feels better to use a function than a subquery.
For example:
CREATE TABLE [tbl_TiggeredTable](
[id] [int] identity(0,1) NOT NULL,
[other] [varchar](max)
)
CREATE TRIGGER [trgMyTrigger]
ON [tbl_TriggeredTable]
INSTEAD OF INSERT,UPDATE,DELETE
SET identity_insert tbl_TriggeredTable ON
INSERT INTO tbl_TriggeredTable (
[id],
[other]
)
SELECT
-- The identity column will have a zero in the insert table when
-- it has not been populated yet, so we need to figure it out manually
case i.[id]
when 0 then IDENT_CURRENT('tbl_TriggeredTable') + IDENT_INCR('tbl_TriggeredTable')
ELSE i.[id]
END,
i.[other],
FROM inserted i
SET identity_insert tbl_TriggeredTable OFF
END