INSERT TRIGGERS and the UPDATE() function - sql-server

I have created an INSTEAD OF INSERT trigger on a view in my database. I want to know which columns are included in the column list of the INSERT statement on the view.
If you read the MSDN documentation for triggers the UPDATE() and COLUMNS_UPDATED() functions should satisfy this requirement. However, during my testing I found that regardless of what columns are in the INSERT column list the UPDATE() and COLUMNS_UPDATED() functions always return all columns from the view.
CREATE VIEW dbo.MyView (BatchId, [Status], OrderNumber, WhenClosed) AS
SELECT bth.BatchId, bth.[Status], bth.OrderNumber,
Private.ufxAdjustDateTime(bth.WhenClosed, bth.WhenClosedUtcOffset)
FROM Private.Batch AS bth
GO
CREATE TRIGGER dbo.[MyView-Insert] ON dbo.MyView INSTEAD OF INSERT AS
BEGIN
SET NOCOUNT ON
DECLARE #batchIdIsSet BIT
SELECT #batchIdIsSet = 0
IF UPDATE(BatchId)
SELECT #batchIdIsSet = 1
INSERT INTO Private.Batch
(BatchId, [Status], OrderNumber)
SELECT CASE #batchIdSet
WHEN 1 THEN ins.BatchId
ELSE NEWID()
END, ins.[Status], ins.OrderNumber
FROM inserted AS ins
END
The reason I want to do this is that I need to modify an existing table and I have loads of legacy code that relies on it. So what I've done is created a new table, changed the old table to a view and created triggers, on the view, to allow INSERT, UPDATE and DELETE statements.
Now, the old table had defaults for certain columns, if the insert into the view uses the default I want to use a default in my insert into the new table. To do this I have to be able to figure out which columns had values [explicitly] supplied for the INSERT.
Checking to see if the column has NULL is not enough because the INSERT statement can explicitly set the field value to NULL and this is perfectly acceptable.
Hmmm, I hope this is clear.
Kep.

On an INSERT statement, every column is affected. It either gets NULL, or the value you're specifying.
Checking for NULL would be the best option, but as you can't do that, I'm thinking you might be a bit stuck. Can you work out scenarios which might need to handle NULL explicitly?

For INSERT, everything is a change because it's a new row.
If you had an AFTER trigger, you could test to see if the inserted value is the default value. If the default is NULL (eg nullable and no default), then how can you distinguish if NULL is explicitly inserted in any trigger?
In a BEFORE trigger, I don't know if you can trap the default. Of course, if the default is NEWID() this still won't help you.
On the face of it, this can't be done in a trigger.

Related

Update OUTPUT results when used on a view with an INSTEAD OF INSERT trigger

I have a view that combines two tables. When I do an insert into the view, the trigger gets the primary key/clustered index value using NEXT VALUE FOR MySequence.
The trigger then inserts into the two tables using the generated key and the values in the Inserted table. It all works just fine.
The problem comes in when an application that uses the view does an insert with an output clause. Something like this:
DECLARE #InsertedIds table (MyViewId int, ValueForTheFirstTable int)
INSERT INTO MyView (ValueForTheFirstTable, ValueForTheSecondTable )
OUTPUT Inserted.MyViewId, Inserted.ValueForTheFirstTable into #InsertedIds
VALUES ('FirstTableValue', 'SecondTableValue')
ValueForTheFirstTable does return the correct value. But the MyViewId (the key) is null. (And I really expect it to be anything else because it does not know the key that I generated.)
My question is, how can I modify my INSTEAD OF INSERT trigger to make it so that the OUTPUT clause will contain my generated Sequence number value?
I know value, I just need a way to feed it back to SQL Server so it can provide it to any OUTPUT clauses.

SQL Server add auto serial id to additional field during insert

I have an insert statement in a stored procedure who's primary key is a serial id. I want to be able to populate an additional field in the same table during the same insert statement with the serial id used for the primary key. Is this possible?
Unfortunately this is a solution already in place... I just have to implement it.
Regards
I can't imagine a reason why you would want a copy of the key in another column. But in order to do it, I think you'll need to follow your update with a statement to get the value of the identity key, and then an update to put that value in the other column. Since you're already in a stored procedure, it's probably ok to have a few extra statements, instead of doing it in the very same one.
DECLARE #ID INT;
INSERT INTO TABLE_THINGY (Name, Address) VALUES ('Joe Blow', '123 Main St');
SET #ID = SCOPE_IDENTITY();
UPDATE TABLE_THINGY SET IdCopy = #Id WHERE ID = #ID
If it's important that this be done every single time, you might want to create a Trigger to do it; beware, however, that many people hate triggers because of the obfuscation and difficulty in debugging, among other reasons.
http://blog.sqlauthority.com/2007/03/25/sql-server-identity-vs-scope_identity-vs-ident_current-retrieve-last-inserted-identity-of-record/
I agree, it is odd that you would replicate the key within the same table but with that said you could use a trigger, thus making it have no impact to current insert statements.
The below trigger is "After Insert" so technically it happens milliseconds after the insert if you truly wanted it to happen at the same time you would use a FOR INSERT instead and just replicate the logic used to create the serial id field into the new field.
CREATE TRIGGER triggerName ON dbo.tableName
AFTER INSERT
AS
BEGIN
update dbo.tableName set newField = inserted.SerialId where serialId = inserted.SerialId
END
GO
You could have a computed column that just returns the id column.
CREATE TABLE dbo.Products
(
ProductID int IDENTITY (1,1) NOT NULL
, OtherProductID AS ProductID
);
Having said that, data should only live in one place and to duplicate it in the same table is just a wrong design.
No, you cannot use the same insert statement for identity Id and copy that auto generated Id to the same row.
Multi-Statement using OUTPUT inserted or Trigger is your best bet.

In trigger, get values from insert

When I try to create this trigger, I get this exception:
The name "SITE_ID" is not permitted in this context. Valid expressions are constants, constant expressions, and (in some contexts) variables. Column names are not permitted.
CREATE TRIGGER tr_VISITACTIVITY_insert ON VISITACTIVITY
INSTEAD OF INSERT AS
BEGIN
INSERT INTO [VISITACTIVITY]
([O_ID]
,[SITE_ID]
,[VISIT_DATE]
,[STAFF]
,[VISIT_TYPE_ID]
,[COMMENTS]
,[DELETED])
VALUES
(-1
,SITE_ID
,VISIT_DATE
,STAFF
,VISIT_TYPE_ID
,VISIT_COMMENTS
,0
)
GO
END
GO
The insert that the programmer would issue in the application is something like this:
INSERT INTO [VISITACTIVITY]
([SITE_ID]
,[VISIT_DATE]
,[STAFF_COMMENT]
,[VISIT_TYPE_ID]
,[COMMENTS]
)
VALUES
(15
,'2011-08-08'
,'Eric, John'
,6
,'invasives growing along the border of the preserve'
)
So, I need the values that the programmer provided in the insert to create the insert in the trigger.
How to I reference these values, then, in the trigger?
Thanks.
You need to look at the meta-table INSERTED, which is basically a table that contains all the data being inserted, so you'd reference (for example) SITE_ID as inserted.SITE_ID.
What you also need to be careful of as well is doing single-row inserts from within the trigger (as you seem to be doing). If multiple rows are inserted in a batch, then INSERTED will contain multiple rows. So what you need to do is to use INSERT INTO table (...) SELECT ..., for example:
INSERT INTO [VISITACTIVITY]
([SITE_ID]
,[VISIT_DATE]
,[STAFF_COMMENT]
,[VISIT_TYPE_ID]
,[COMMENTS]
)
SELECT VISITACTIVITY, SITE_ID, VISIT_DATE, STAFF_COMMENT, VISIT_TYPE_ID, COMMENTS
FROM inserted
By the way, reading your question, it looks like you're just using the trigger to populate default values in a couple of columns. Have you considered just using a DEFAULT constraint on the table instead? For example, if you did the following:
ALTER TABLE VisitActivity ADD CONSTRAINT DF_VisitActivity_Deleted DEFAULT (0) FOR DELETED
ALTER TABLE VisitActivity ADD CONSTRAINT DF_VisitActivity_OID DEFAULT (-1) FOR O_ID
...then this would make your example insert behave in the same way without the trigger. It depends whether you always want to override the values in Deleted or O_ID or not. If you always want to override, use a trigger - or better yet, stick a CHECK on the column to force it to a fixed value (will also highlight where your code is trying to change it as SQL Server will fail the insert due to referential integrity). If you want to be able to override it, then use DEFAULT constraints.
(Of course, you may well have simplified the problem for the sake of the question, but I'm just highlighting how you can do this without resorting to a trigger).

Stuck at creating a trigger

performing some MSSQL exercises, and I am trying to create a trigger. However, the solution I have, comes across as theoretically correct to me but it is not working.
The aim is to create a trigger for a table that has only two columns. One column is the primary key and is Identity and does not allow null values. The other column is one that ALLOWS NULL values. However, it permits NULL values ONLY FOR A SINGLE ROW in the entire table. Basically a trigger should fire for an insert/update operation on this table which attempts to insert/update the column to a NULL value when there is already an existing NULL value for the column in the table.
This condition I capture in my trigger code as follows:
After Insert, Update
AS
set ANSI_WARNINGS OFF
If ( (select count(NoDupName) from TestUniqueNulls where NoDupName is null) > 1 )
BEGIN
Print 'There already is a row that contains a NULL value, transaction aborted';
ROLLBACK TRAN
END
However, the transaction executes itself nonetheless. I am not sure why this is happening and the trigger is not firing itself.
So anybody to enlighten my misgivings here?
I also have used set ANSI_WARNINGS OFF at the start of the trigger.
count(col) only counts non null values so count(NoDupName) ... where NoDupName is null will always be zero. You would need to check count(*) instead.
I realise this is just a practice exercise but an indexed view might be a better mechanism for this.
CREATE VIEW dbo.NoMoreThanOneNull
WITH SCHEMABINDING
AS
SELECT NoDupName
FROM dbo.TestUniqueNulls
WHERE NoDupName IS NULL
GO
CREATE UNIQUE CLUSTERED INDEX ix ON dbo.NoMoreThanOneNull(NoDupName)
Yeah that's a gotcha. The expression inside parens of the COUNT has to evaluate to not null, otherwise it will not be counted. So it is safer to use *, or 1 or any not nullable column in the expression. The most commonly encountered expression is '*', although you can come across '1' as well. There is no difference between these expressions in terms of performance. However if you use expression that can evaluate to null (like nullable column), your counts and other aggregations can be completely off.
create table nulltest(a int null)
go
insert nulltest(a) values (1), (null), (2)
go
select * from nulltest
select COUNT(*) from nulltest
select COUNT(1) from nulltest
select COUNT(a) from nulltest
go
drop table nulltest

Why FOR Trigger does not run befor the action?

I am trying to write a trigger on a table to avoid insertion of two Names which are not flagged as IsDeleted. But the first part of selection contains the inserted one and so the condition is always true. I though that using FOR keyword causes the trigger to run before the INSERTION but in this case the inserted row is already in the table. Am I wrong or this is how all FOR trigger work?
ALTER TRIGGER TriggerName
ON MyTable
FOR INSERT, UPDATE
AS
BEGIN
If exist (select [Name] From MyTable WHERE IsDeleted = 0 AND [Name] in (SELECT [Name] FROM INSERTED)
BEGIN
RAISERROR ('ERROR Description', 16, 1);
Rollback;
END
END
FOR runs after the data is changed, INSTEAD OF is what I think you are after.
EDIT: As stated by others, INSTEAD OF runs instead of the data you are changing, therefore you need to insert the data if it is valid, rather than stopping the insert if it is invalid.
Read this question for a much more detailed explanation of the types of Triggers.
SQL Server "AFTER INSERT" trigger doesn't see the just-inserted row
FOR is the same as AFTER. if you want to "simulate" BEFORE trigger, use INSTEAD OF, caveat, it's not exactly what you would expect on proper BEFORE trigger, i.e. if you fail to provide the necessary INSTEAD action, your inserted/updated data could be lost/ignored.
MSSQL doesn't have BEFORE trigger.
For SQL Server, FOR runs AFTER the SQL which triggered it.
From:
http://msdn.microsoft.com/en-us/library/ms189799.aspx
FOR | AFTER
AFTER specifies that the DML trigger
is fired only when all operations
specified in the triggering SQL
statement have executed successfully.
All referential cascade actions and
constraint checks also must succeed
before this trigger fires.
AFTER is the default when FOR is the
only keyword specified.
AFTER triggers
cannot be defined on views.
I've actually ran into a similar problem lately, and found a cool way to handle it. I had a table which could have several rows for one id, but only ONE of them could be marked as primary.
In SQL Server 2008, you'll be able to make a partial unique index something like this:
create unique index IX on MyTable(name) where isDeleted = 0;
However, you can accomplish it with a little more work in SQL Server 2005. The trick is to make a view showing only the rows which aren't deleted, and then create a unique clustered index on it:
create view MyTableNotDeleted_vw
with schema_binding /* Must be schema bound to create an indexed view */
as
select name
from dbo.MyTable /* Have to use dbo. for schema bound views */
where isDeleted = 0;
GO
create unique clustered index IX on MyTableNotDeleted_vw ( name );
This will effectively create a unique constraint only affecting rows that haven't yet been deleted, and will probably perform better than a custom trigger!

Resources