TSQL Trigger with INSTEAD OF - sql-server

So I am trying to build a simple trigger that will cause an error message to popup if a credit card date entered has passed. If the cards date is still good, I would like it to update or insert as usual. I am still very new to dealing with SQL Server and trying to wrap my head around the contexts of how things should be setup correctly. I have a feeling I am way off on setting this up correctly but any advice would be kindly appreciated.
The Table contains, (CreditCardID (PK), CardType, CardNumber, ExpMonth, ExpYear, ModifiedDate
USE AdventureWorks2012
GO
CREATE TRIGGER BadCreditCardDate
ON Sales.CreditCard
INSTEAD OF UPDATE, INSERT
AS
Begin
DECLARE #ExpMonth tinyint,
#ExpYear smallint
SELECT #ExpMonth=ExpMonth, #ExpYear=ExpYear
FROM INSERTED
IF
#ExpMonth < MONTH(GETDATE())
AND
#ExpYear < YEAR(GETDATE())
RAISERROR ('The Credit Card you have entered has expired.' ,10,1)
ELSE
DECLARE #CreditCardID INT
INSERT INTO CreditCard (CardType, CardNumber, ExpMonth, ExpYear, ModifiedDate)
Select CardType, CardNumber, ExpMonth, ExpYear, ModifiedDate FROM inserted
WHERE CreditCardID = #CreditCardID
END

First of all, your ELSE condition only apply to the next statement which is DECLARE #CreditCardID INT. You should use BEGIN/END around your entire ELSE block.
Second, the logic in this trigger will fail if you decide to update multiple rows since it is only considering one row updates. This may happen if you need to import customer data from another source.
Third. You should handle UPDATES and INSERTS separately. I would probably consider creating 2 triggers, one for update, another for insert, just to avoid costly determination of what happened and a lot of if and else logic.

Related

SQL server GetDate in trigger called sequentially has the same value

I have a trigger on a table for insert, delete, update that on the first line gets the current date with GetDate() method.
The trigger will compare the deleted and inserted table to determine what field has been changed and stores in another table the id, datetime and the field changed. This combination must be unique
A stored procedure does an insert and an update sequentially on the table. Sometimes I get a violation of primary key and I suspect that the GetDate() returns the same value.
How can I make the GetDate() return different values in the trigger.
EDIT
Here is the code of the trigger
CREATE TRIGGER dbo.TR
ON table
FOR DELETE, INSERT, UPDATE
AS
BEGIN
SET NoCount ON
DECLARE #dt Datetime
SELECT #dt = GetDate()
insert tableLog (id, date, field, old, new)
select I.id, #dt, 'field', D.field, I.field
from INSERTED I LEFT JOIN DELETED D ON I.id=D.id
where IsNull(I.field, -1) <> IsNull(D.field, -1)
END
and the code of the calls
...
insert into table ( anotherfield)
values (#anotherfield)
if ##rowcount=1 SET #ID=##Identity
...
update table
set field = #field
where Id = #ID
...
Sometimes the GetDate() between the 2 calls (insert and update) takes 7 milliseconds and sometimes it has the same value.
That's not exactly full solution but try using SYSDATETIME instead and of course make sure that target table can store up datetime2 up to microseconds.
Note that you can't force different datetime regardless of precision (unless you will start counting up to ticks) as stuff can just happen at the same time wihthin given precision.
If stretching up to microseconds won't solve the issue on practical level, I think you will have to either redesign this logging schema (perhaps add identity column on top of what you have) or add some dirty trick - like make this insert in try catch block and add like microsecond (nanosecond?) in a loop until you insert successfully. Definitely not s.t. I would recommend.
Look at this answer: SQL Server: intrigued by GETDATE()
If you are inserting multiple ROWS, they will all use the same value of GetDate(), so you can try wrapping it in a UDF to get unique values. But as I said, this is just a guess unless you post the code of your trigger so we can see what you are actually doing?
It sounds like you're trying to create an audit trail - but now you want to forge some of the entries?
I'd suggest instead adding a rowversion column to the table and including that in your uniqueness criteria - either instead of or as well as the datetime value that is being recorded.
In this way, even if two rows are inserted with identical date/time data, you can still tell the actual insertion order.

SQL Server timestamping trigger

My knowledge of SQL is pretty limited as I mostly focus in backend Ruby development. However, due to architectural changes and wanting to keep things well designed; I have decided to set up timestamping on the database level rather than on the backend level.
As it goes right now, all of my tables have two columns: CreatedAt and UpdatedAt, both with a default value of GETDATE().
However, I now need to set up a timestamping trigger for UpdatedAt, so that every time a row (or rows) are updated, the UpdatedAt column for those rows gets a brand new timestamp.
I am having trouble with the following trigger I wrote. I am getting an error:
Incorrect Syntax near '='
I am testing out my trigger on my Orders table first, and then I plan to move the functionality to all tables.
CREATE TRIGGER dbo.trgTimestampAfterUpdate
ON Dbo.Orders
AFTER UPDATE
AS
BEGIN
IF EXISTS (SELECT * FROM inserted)
BEGIN
SET UpdatedAt = GETDATE()
END
END
I know that I can access the inserted, and deleted virtual tables when using a trigger. My thought with this query was that I would use inserted in order to distinguish which rows have been updated. If anyone can help that would be great, and also if you wouldn't mind explaining to me what I messed up with my syntax or line of thinking would be greatly appreciated.
You can't really access the inserted tables quite like that. Having just Set UpdatedAt =... is an incomplete statement. Implicitly it makes sense to you but even in your trigger, you have to make complete SQL statements.
The way to do this is to JOIN to the INSERTED table (in the example below, I'm using a semi-join) You can then use the contents of the INSERTED table to perform another update.
CREATE TRIGGER [dbo].[trgTimestampAfterUpdate] ON dbo.orders
FOR UPDATE
AS
BEGIN
IF NOT(UPDATE(UpdatedAt)) --Avoid triggers firing triggers
BEGIN
UPDATE dbo.orders
SET UpdatedAt = GETDATE()
WHERE id IN ( SELECT id
FROM inserted )
END
END
Two REALLY important things to note in this code example. First Updating the table with the trigger on it will cause the trigger to fire again (creating a loop that will increase until you reach the max level of nested triggers on your system.) I put a check to make sure it terminates if you're only updating the updatedat column.
Second, never ever assume there is only one row in the inserted table. Something like the code below is a very common mistake
DECLARE #id INT
SELECT #id = id FROM INSERTED
UPDATE MyTable
SET UpdatedAT = GETDATE()
WHERE id = #id
--DON'T DO THIS!
This looks right, and is a common mistake, but it will only ever update 1 record and there could be any number of records in the INSERTED table.

SQL Server decrementing inventory

I am using SQL Server 2008. I have a table where orders with SKU are recorded, a table for inventory that has counts and a table where the relationship between the SKU sold and inventory items is recorded.
In the end, I got the report like this
Inventory CurrentQuantity OpenedOrder
SKU1 300 50
SKU2 100 10
Each order will be processed individually. How can I have the database automatically update the inventory tablet after each order is processed?
i.e
If the order has 2 SKU1 in it got processed, the the inventory table will automatically show 298.
Thanks
I would use a Stored Procedure, and perform the order insert and quantity update in one hit:
CREATE PROC dbo.ProcessOrder
#Item int,
#Quantity int
AS
BEGIN
--Update order table here
INSERT INTO dbo.Orders(ItemID,Quantity)
VALUES (#ItemID, #Quantity)
--Update Inventory here
UPDATE dbo.Inventory
SET CurrentQuantity = CurrentQuantity - Quantity
WHERE ItemID = #ItemID
END
I think what you are looking for is a trigger
Basically, set up a trigger that will update the appropriate columns using the inserted/updated data given. Without a full schema set, that is the best answer I can give at this time
I wouldn't be looking at a trigger myself for this.
My check out process
Start a transaction
Check stock level.
If OK, (optional validation / authorisation)
Add a check out record
Reduce the stock
Possibly add some record to invoice teh recipent etc.
Commit the transaction
While you could do it with triggers, I simply fail to see the point, a nice simple clear and all in one place SP_CheckOut stored procedure is where I'd be going.
I would normally advise to use a trigger but stock manipulation is that kind of operation that's usually done a lot of times, sometimes on batches and this is not the best scenario for triggers to be honest.
I think PKG's idea is very good, but you should never forget to add transaction control to it, otherwise you can endup with non-matching stocks:
CREATE PROC dbo.ProcessOrder
#Item int,
#Quantity int
AS
BEGIN
begin transaction my_tran
begin try
--Update order table here
INSERT INTO dbo.Orders(ItemID,Quantity)
VALUES (#ItemID, #Quantity)
--Update Inventory here
UPDATE dbo.Inventory
SET CurrentQuantity = CurrentQuantity - Quantity
WHERE ItemID = #ItemID
commit transaction
end try
begin catch
rollback transaction
--raise error if necessary
end catch
END
you can use the trigger,also use the procedure,and the specific steps on the top,use the procedure need to open the atuo exec feature in mastaer DB.

Linq to SQL with INSTEAD OF Trigger and an Identity Column

I need to use the clock on my SQL Server to write a time to one of my tables, so I thought I'd just use GETDATE(). The problem is that I'm getting an error because of my INSTEAD OF trigger. Is there a way to set one column to GETDATE() when another column is an identity column?
This is the Linq-to-SQL:
internal void LogProcessPoint(WorkflowCreated workflowCreated, int processCode)
{
ProcessLoggingRecord processLoggingRecord = new ProcessLoggingRecord()
{
ProcessCode = processCode,
SubId = workflowCreated.SubId,
EventTime = DateTime.Now // I don't care what this is. SQL Server will use GETDATE() instead.
};
this.Database.Add<ProcessLoggingRecord>(processLoggingRecord);
}
This is the table. EventTime is what I want to have as GETDATE(). I don't want the column to be null.
And here is the trigger:
ALTER TRIGGER [Master].[ProcessLoggingEventTimeTrigger]
ON [Master].[ProcessLogging]
INSTEAD OF INSERT
AS
BEGIN
SET NOCOUNT ON;
SET IDENTITY_INSERT [Master].[ProcessLogging] ON;
INSERT INTO ProcessLogging (ProcessLoggingId, ProcessCode, SubId, EventTime, LastModifiedUser)
SELECT ProcessLoggingId, ProcessCode, SubId, GETDATE(), LastModifiedUser FROM inserted
SET IDENTITY_INSERT [Master].[ProcessLogging] OFF;
END
Without getting into all of the variations I've tried, this last attempt produces this error:
InvalidOperationException
Member AutoSync failure. For members to be AutoSynced after insert, the type must either have an auto-generated identity, or a key that is not modified by the database after insert.
I could remove EventTime from my entity, but I don't want to do that. If it was gone though, then it would be NULL during the INSERT and GETDATE() would be used.
Is there a way that I can simply use GETDATE() on the EventTime column for INSERTs?
Note: I do not want to use C#'s DateTime.Now for two reasons:
1. One of these inserts is generated by SQL Server itself (from another stored procedure)
2. Times can be different on different machines, and I'd like to know exactly how fast my processes are happening.
Bob,
It seems you are attempting to solve two different problems here. One of which has to do with a L2S error with an Instead Of trigger and another with using the date on the SQL Server box for your column. I think you might have problems with Instead of Triggers and L2S. You might want to try an approach that uses an After trigger, like this. I think this will solve both your problems.
ALTER TRIGGER [Master].[ProcessLoggingEventTimeTrigger]
ON [Master].[ProcessLogging]
AFTER INSERT
AS
BEGIN
UPDATE [Master].[ProcessLogging] SET EventTime = GETDATE() WHERE ProcessLoggingId = (SELECT ProcessLoggingId FROM inserted)
END
Don't use a trigger, use a defualt:
create table X
(id int identity primary key,
value varchar(20),
eventdate datetime default(getdate()))
insert into x(value) values('Try')
insert into x(value) values('this')
select * from X
It's much better.
Have you tried using a default value of (getdate()) for the EventTime colum?
You wouldn't then need to set the value in the trigger, it would be set automatically.
A default value is used when you don't explicitly supply a value, e.g.
INSERT INTO ProcessLogging (ProcessLoggingId, ProcessCode, SubId, LastModifiedUser)
SELECT ProcessLoggingId, ProcessCode, SubId, LastModifiedUser FROM inserted
Bob,
I see it is better to don't use triggers in SQL server; it have a lot of disadvantage and not recommended for database performance enhancements. Please check SQL Authority blog for more information about the Triggers problems.
You can achieve what you want without Triggers using the following steps:
Change Eventime column to allow null
Set Eventtime column Default Value to GetDate(). So it always will have a the current insertion value.
Don't set Eventtime value to DateTime.Now from your LinqToSQL code, so it will take the default value in the SQL Server.

SQL Query Exclusive Read/Write to Table

SQL Server 2008: I've got a situation here in which I wish to read from a table and write a row under certain conditions. The problem is that I don't want another request coming it at exactly the same time and doing the same thing. I'll try to explain here:
Table Name: RequestQueue
Columns:
RequestID, StartDate, EndDate, RequestResult
Sample Data
1, 12/4/10 1:00pm, 12/4/10 1:02pm, Success
2, 12/4/10 1:04pm, 12/4/10 1:05pm, Success
3, 12/4/10 1:00pm, NULL, NULL
When a page loads in my app, I want it to look at this table and if there is a request still pending like (ID #3) it will not do anything. Otherwise, if there are no requests pending, it creates a new row with the ID and StartDate filled in.
The issue is that we could get into a situation where the page is loaded twice at almost exactly the same time. If they happen to both read from the table before the new row is produced, then I could get two new rows in there. I want to have some sort of query that reads from the table and if there are no requests pending, inserts the new row with the StartDate filled in. I want that query to run all the way before another page can even read from this table so I don't get the "double row" effect.
I might need "locking" or something, I googled that but haven't found something for my exact situation. I'm sure this could be a simple stored procedure I just need a push in the right direction here.
Thanks,
Robert
Assuming that you just want to block all concurrent access to the table you could just do.
BEGIN TRAN
DECLARE #StartDate datetime,
#EndDate datetime
SELECT TOP 1
#StartDate = StartDate,
#EndDate = EndDate
FROM RequestQueue WITH(TABLOCK,XLOCK)
ORDER BY RequestID DESC
IF #EndDate IS NULL
SELECT #StartDate AS 'StartDate'
ELSE
INSERT INTO RequestQueue (StartDate)
OUTPUT INSERTED.* /* Or use SCOPE_IDENTITY() instead*/
VALUES (GETDATE())
COMMIT
Alternatively you could just serialise access to the SELECT/INSERT code inside the specific procedure without taking an exclusive table lock by using sp_getapplock

Resources