In my SQL table (2008), I have a timestamp column called Timestamp.
In my .Net project, I have a POCO class with a property
public byte[] Timestamp {get; set;}
In my configuration code, I have the following:
Property(p => p.Timestamp).IsRowVersion();
Now, if I open two edit screens make a change in one and save it (save is accepted), then make a change in the second and save it (save is rejected with DbUpdateConcurrencyException).
One thing that I find odd is that SQL Profiler shows an update request being sent to the SQL database even when I receive the concurrency exception. The update is not committed on the database but it is sent. Is that normal? I kind of expected EF to check that ahead of time and not even send the request.
Lastly, if I enable the trigger on this table:
ALTER TRIGGER [dbo].[trg_person_log_changes]
ON [dbo].[Person]
AFTER INSERT,DELETE,UPDATE
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
DECLARE #auditBody XML
DECLARE #actionType char(1)
DECLARE #username nvarchar(100)
if not exists(select * from deleted)
Begin
SET #actionType = 'I'
SET #username = (select Case
When lastchangedby is null or lastchangedby = '' then Suser_name()
Else lastchangedby
End
from inserted)
End
else if not exists(select * from inserted)
Begin
SET #actionType = 'D'
SET #username = Suser_name()
End
else
Begin
SET #actionType = 'U'
SET #username = (select distinct Case
When lastchangedby is null or lastchangedby = '' then Suser_name()
Else lastchangedby
End
from inserted)
End
If #actionType = 'I'
Set #auditBody = (select 'Person' as "#tableName", 'True' as "#synch",
(select * from inserted for xml path('DataItem'), type, binary base64)
for xml path('Root'))
Else
SET #auditBody = (select 'Person' as "#tableName", 'True' as "#synch",
(select * from deleted for xml path('DataItem'), type, binary base64)
for xml path('Root'))
insert into [dbo].[AuditLog]
([Action]
,[ActionDate]
,[ActionUser]
,[AuditData]
)
values (
#actionType
,getdate()
,#username
,#auditBody)
END
Now when I try to save the second edit, I no longer get a DbUpdateConcurrencyException, I get an error stating that ActionUser can't be null. Once again, I think my trigger is being executed because EF is allowing the update to go through even though there is a conflict.
Any ideas on what I may be doing wrong?
NOTE
This is an MVC project. In my Edit POST controller, I'm receiving a DTO object that contains all the form properties (one of which is Timestamp -- a hidden field on my edit form). I'm loading the edited person from the datacontext and mapping the edited properties from the DTO object into the Domain object returned from the datacontext.
The only way to do optimistic concurrency right is doing the version check and being 100% sure that nothing changes until the actual update is done. The most convenient way to do this is incorporate the version check in the UPDATE statement, so that's what EF does. So it does send an update statement, which looks like
UPDATE table SET columnA = value
WHERE rowversion = xxxx
However, when the statement does not find the row with the rowversion it had in memory it returns a different number of affected rows than expected and the exception is throw. And the transaction is rolled back. So yes, you see a statement in SQL Profiler, but it is never committed.
The trigger runs as part of the update statement, i.e. before EF is reported back about the update command. Apparently, #username doesn't get a value in the process, so this throws a SQL exception that spoils the update party before any concurrency conflict is noticed.
Related
I am trying to write an update trigger on a table which would cause it to run an additional update statement only if a certain column has been changed, so far the trigger runs the update no matter what, hoping maybe someone can see what I am doing wrong here.
Here is the trigger.
ALTER TRIGGER [dbo].[StatusChangedUpdateTrigger]
ON [dbo].[Trans_Order]
AFTER UPDATE
AS
DECLARE #OldOrderStatusId INT, #NewStatusOrderId INT, #ERRNUM INT;
BEGIN
SET #OldOrderStatusId = (SELECT OrderStatusId FROM deleted);
SET #NewStatusOrderId = (SELECT OrderStatusId FROM inserted);
IF (#OldOrderStatusId != #NewStatusOrderId)
SET NOCOUNT ON;
UPDATE Trans_Order
SET StatusChanged = 1
WHERE Id = (SELECT ID FROM inserted)
END
For some reason this is running no matter what, I can never set StatusChanged to 0 as it will automatically flip it back to 1 even if the OrderStatusId hasn't changed. So my update statement is running no matter what, so I am guessing I am doing something wrong in the if statement.
Hmmmm . . . Your logic seems strange. I would expect:
UPDATE t
SET StatusChanged = 1
FROM Trans_Order t JOIN
Inserted i
ON t.id = i.id JOIN
Deleted d
ON t.id = d.id
WHERE i.OrderStatusId <> d.OrderStatusId;
You might need to take NULL values into account -- although your code does not.
Note that your code is just a bug waiting to happen, because it assumes that inserted and deleted have only one row.
The specific problem with your code is that it is really:
IF (#OldOrderStatusId != #NewStatusOrderId)
BEGIN
SET NOCOUNT ON;
END;
UPDATE Trans_Order
SET StatusChanged = 1
WHERE Id = (SELECT ID FROM inserted);
Your indentation has confused the logic. However, you should still use the set-based version so the trigger does not fail.
The correct way to approach your trigger is as follows:
create or alter trigger [dbo].[StatusChangedUpdateTrigger] on [dbo].[Trans_Order]
after update
as
set nocount on
if ##RowCount=0 return
if Update(OrderStatusId)
begin
update t
set statusChanged=1
from inserted i join deleted d on d.id=i.id and d.OrderStatusId != i.OrderStatusId
join Trans_Order t on t.id=i.id
end
Always test ##rowcount and return if no rows updated.
Always put set options before DML
As you are only looking to update if a specific column is updated you can test specifically for that and if the update statement that's run doesn't touch that column the trigger will not run.
This will correctly account for multiple rows being updated and only update those where the new value is different to the old value.
Is it possible to make a column that can only be NULL from the beginning, but once it has been set to a non-null value then it can never be reset to NULL?
I'm guessing it might be possible with an update constraint using both the inserted and deleted table, but that seems somewhat complicated given the goal of the task.
Bonus question: Since it doesn't seem easy to do this, is this a sign of bad design (on the part of us / SQL Server)?
Since you're allowing the filed to be NULL when the ROW is Inserted, you'll have to use a Trigger for Update
CREATE TRIGGER trigger1 ON table1
FOR UPDATE
AS
BEGIN
SET NOCOUNT ON
DECLARE #field1 VARCAHR(MAX)
select #field1 = field1 FROM INSERTED
IF #field1 is NULL
BEGIN
RAISERROR('A NULL Values are not allowed', 15, 1)
ROLLBACK
END
END
"Allowing null value in a column initially, but once set to non null value, it must not allow null value for the same row"
This is clearly a business logic. It must be handled in controller part of MVC. Trying to handle it in database layer will break the MVC principle, and thus is a bad design.
If you still want to handle it in DBMS, then, as suggested in other answers, you can use triggers.
The trigger may look like this. You join INSERTED on DELETED to check if column was NOT NULL and became NULL:
CREATE TRIGGER trCheckForNulls ON SomeTable
AFTER UPDATE
AS
BEGIN
IF UPDATE(SomeColumn)
BEGIN
IF EXISTS(
SELECT * FROM INSERTED i
JOIN DELETED d ON i.PK = d.PK
WHERE d.SomeColumn IS NOT NULL AND i.SomeColumn IS NULL)
BEGIN
RAISERROR('SomeError', 16, 1)
ROLLBACK
END
END
END
Question:
In our SQL-Server 2005 database, we have a table T_Groups.
T_Groups has, amongst other things, the fields ID (PK) and Name.
Now some idiot in our company used the name as key in a mapping table...
Which means now one may not alter a group name, because if one does, the mapping is gone...
Now, until this is resolved, I need to add a restriction to T_Groups, so one can't update the group's name.
Note that insert should still be possible, and an update that doesn't change the groupname should also be possible.
Also note that the user of the application & the developers have both dbo and sysadmin rights, so REVOKE/DENY won't work.
How can I do this with a trigger ?
CREATE TRIGGER dbo.yournametrigger ON T_Groups
FOR UPDATE
AS
BEGIN
IF UPDATE(name)
BEGIN
ROLLBACK
RAISERROR('Changes column name not allowed', 16, 1);
END
ELSE
BEGIN
--possible update that doesn't change the groupname
END
END
CREATE TRIGGER tg_name_me
ON tbl_name
INSTEAD OF UPDATE
AS
IF EXISTS (
SELECT *
FROM INSERTED I
JOIN DELETED D ON D.PK = I.PK AND ISNULL(D.name,I.name+'.') <> ISNULL(I.name,D.name+'.')
)
RAISERROR('Changes to the name in table tbl_name are NOT allowed', 16,1);
GO
Depending on your application framework for accessing the database, a cheaper way to check for changes is Alexander's answer. Some frameworks will generate SQL update statements that include all columns even if they have not changed, such as
UPDATE TBL
SET name = 'abc', -- unchanged
col2 = null, -- changed
... etc all columns
The UPDATE() function merely checks whether the column is present in the statement, not whether its value has changed. This particular statement will raise an error using UPDATE() but won't if tested using the more elaborate trigger as shown above.
This is an example of preserving some original values with an update trigger.
It works by setting the values for orig_author and orig_date to the values from the deleted pseudotable each time. It still performs the work and uses cycles.
CREATE TRIGGER [dbo].[tru_my_table] ON [dbo].[be_my_table]
AFTER UPDATE
AS
UPDATE [dbo].[be_my_table]
SET
orig_author = deleted.orig_author
orig_date = deleted.orig_date,
last_mod_by = SUSER_SNAME(),
last_mod_dt = getdate()
from deleted
WHERE deleted.my_table_id IN (SELECT DISTINCT my_table_id FROM Inserted)
ALTER TABLE [dbo].[be_my_table] ENABLE TRIGGER [tru_my_table]
GO
This example will lock any updates on SABENTIS_LOCATION.fk_sabentis_location through a trigger, and will output a detailed message indicating what objects are affected
ALTER TRIGGER dbo.SABENTIS_LOCATION_update_fk_sabentis_location ON SABENTIS_LOCATION
FOR UPDATE
AS
DECLARE #affected nvarchar(max)
SELECT #affected=STRING_AGG(convert(nvarchar(50), a.id), ', ')
FROM inserted a
JOIN deleted b ON a.id = b.id
WHERE a.fk_sabentis_location != b.fk_sabentis_location
IF #affected != ''
BEGIN
ROLLBACK TRAN
DECLARE #message nvarchar(max) = CONCAT('Update values on column fk_sabentis_location locked by custom trigger. Could not update entities: ', #affected);
RAISERROR(#message, 16, 1)
END
Some examples seem to be using:
IF UPDATE(name)
But this seems to evaluate to TRUE if the field is part of the update statement, even if the value itself has NOT CHANGED leading to false positives.
Just want to get some views/possible leads on an issue I have.
I have a stored procedure that updates/deletes a record from a table in my database, the table it deletes from is a live table, that temporary holds the data, and also updates records on a archive table. (for reporting etc..) it works normally and havent had an issues.
However recently I had worked on a windows service to monitor our system (running 24/7), which uses a HTTP call to initiate a program, and once this program has finished it then runs the mention stored procedure to delete out redundant data. Basically the service just runs the program quickly to make sure its functioning correctly.
I have noticed recently that the data isnt always being deleted. Looking through logs I see no errors being reported. And Even see the record in the database has been updated correctly. But just doesnt get deleted.
This unfortunately has a knock on effect with the monitoring service, as this continously runs, and sends out alerts because the data cant be duplicated in the live table, hence why it needs to delete out the data.
Currently I have in place a procedure to clear out any old data. (3 hours).
Result has the value - Rejected.
Below is the stored procedure:
DECLARE #PostponeUntil DATETIME;
DECLARE #Attempts INT;
DECLARE #InitialTarget VARCHAR(8);
DECLARE #MaxAttempts INT;
DECLARE #APIDate DATETIME;
--UPDATE tCallbacks SET Result = #Result WHERE CallbackID = #CallbackID AND UPPER(Result) = 'PENDING';
UPDATE tCallbacks SET Result = #Result WHERE ID = (SELECT TOP 1 ID FROM tCallbacks WHERE CallbackID = #CallbackID ORDER BY ID DESC)
SELECT #InitialTarget = C.InitialTarget, #Attempts = LCB.Attempts, #MaxAttempts = C.CallAttempts
FROM tConfigurations C WITH (NOLOCK)
LEFT JOIN tLiveCallbacks LCB ON LCB.ID = #CallbackID
WHERE C.ID = LCB.ConfigurationID;
IF ((UPPER(#Result) <> 'SUCCESSFUL') AND (UPPER(#Result) <> 'MAXATTEMPTS') AND (UPPER(#Result) <> 'DESTBAR') AND (UPPER(#Result) <> 'REJECTED')) BEGIN
--INSERT A NEW RECORD FOR RTNR/BUSY/UNSUCCESSFUL/REJECT
--Create Callback Archive Record
SELECT #APIDate = CallbackRequestDate FROM tCallbacks WHERE Attempts = 0 AND CallbackID = #CallbackID;
BEGIN TRANSACTION
INSERT INTO tCallbacks (CallbackID, ConfigurationID, InitialTarget, Agent, AgentPresentedCLI, Callee, CalleePresentedCLI, CallbackRequestDate, Attempts, Result, CBRType, ExternalID, ASR, SessionID)
SELECT ID, ConfigurationID, #InitialTarget, Agent, AgentPresentedCLI, Callee, CalleePresentedCLI, #APIDate, #Attempts + 1, 'PENDING', CBRType, ExternalID, ASR, SessionID
FROM tLiveCallbacks
WHERE ID = #CallbackID;
UPDATE LCB
SET PostponeUntil = DATEADD(second, C.CallRetryPeriod, GETDATE()),
Pending = 0,
Attempts = #Attempts + 1
FROM tLiveCallbacks LCB
LEFT JOIN tConfigurations C ON C.ID = LCB.ConfigurationID
WHERE LCB.ID = #CallbackID;
COMMIT TRANSACTION
END
ELSE BEGIN
-- Update the Callbacks archive, when Successful or Max Attempts or DestBar.
IF EXISTS (SELECT ID FROM tLiveCallbacks WHERE ID = #CallbackID) BEGIN
BEGIN TRANSACTION
UPDATE tCallbacks
SET Attempts = #Attempts
WHERE ID IN (SELECT TOP (1) ID
FROM tCallbacks
WHERE CallbackID = #CallbackID
ORDER BY Attempts DESC);
-- The live callback should no longer be active now. As its either been answered or reach the max attempts.
DELETE FROM tLiveCallbacks WHERE ID = #CallbackID;
COMMIT
END
END
You need to fix your transaction processing. What is happening is that one statement is failing but since you don't have a try-catch block all changes are not getting rolled back only the statement that failed.
You should never have a begin tran without a try catch block and a rollback on error. I personally also prefer in something like this to put the errors and associated data into a table variable (which will not rollback) and then insert then to an exception table after the rollback. This way the data retains integrity and you can look up what the problem was.
I've got a table where I need to auto-assign an ID 99% of the time (the other 1% rules out using an identity column it seems). So I've got a stored procedure to get next ID along the following lines:
select #nextid = lastid+1 from last_auto_id
check next available id in the table...
update last_auto_id set lastid = #nextid
Where the check has to check if users have manually used the IDs and find the next unused ID.
It works fine when I call it serially, returning 1, 2, 3 ... What I need to do is provide some locking where multiple processes call this at the same time. Ideally, I just need it to exclusively lock the last_auto_id table around this code so that a second call must wait for the first to update the table before it can run it's select.
In Postgres, I can do something like 'LOCK TABLE last_auto_id;' to explicitly lock the table. Any ideas how to accomplish it in SQL Server?
Thanks in advance!
Following update increments your lastid by one and assigns this value to your local variable in a single transaction.
Edit
thanks to Dave and Mitch for pointing out isolation level problems with the original solution.
UPDATE last_auto_id WITH (READCOMMITTEDLOCK)
SET #nextid = lastid = lastid + 1
You guys have between you answered my question. I'm putting in my own reply to collate the working solution I've got into one post. The key seems to have been the transaction approach, with locking hints on the last_auto_id table. Setting the transaction isolation to serializable seemed to create deadlock problems.
Here's what I've got (edited to show the full code so hopefully I can get some further answers...):
DECLARE #Pointer AS INT
BEGIN TRANSACTION
-- Check what the next ID to use should be
SELECT #NextId = LastId + 1 FROM Last_Auto_Id WITH (TABLOCKX) WHERE Name = 'CustomerNo'
-- Now check if this next ID already exists in the database
IF EXISTS (SELECT CustomerNo FROM Customer
WHERE ISNUMERIC(CustomerNo) = 1 AND CustomerNo = #NextId)
BEGIN
-- The next ID already exists - we need to find the next lowest free ID
CREATE TABLE #idtbl ( IdNo int )
-- Into temp table, grab all numeric IDs higher than the current next ID
INSERT INTO #idtbl
SELECT CAST(CustomerNo AS INT) FROM Customer
WHERE ISNUMERIC(CustomerNo) = 1 AND CustomerNo >= #NextId
ORDER BY CAST(CustomerNo AS INT)
-- Join the table with itself, based on the right hand side of the join
-- being equal to the ID on the left hand side + 1. We're looking for
-- the lowest record where the right hand side is NULL (i.e. the ID is
-- unused)
SELECT #Pointer = MIN( t1.IdNo ) + 1 FROM #idtbl t1
LEFT OUTER JOIN #idtbl t2 ON t1.IdNo + 1 = t2.IdNo
WHERE t2.IdNo IS NULL
END
UPDATE Last_Auto_Id SET LastId = #NextId WHERE Name = 'CustomerNo'
COMMIT TRANSACTION
SELECT #NextId
This takes out an exclusive table lock at the start of the transaction, which then successfully queues up any further requests until after this request has updated the table and committed it's transaction.
I've written a bit of C code to hammer it with concurrent requests from half a dozen sessions and it's working perfectly.
However, I do have one worry which is the term locking 'hints' - does anyone know if SQLServer treats this as a definite instruction or just a hint (i.e. maybe it won't always obey it??)
How is this solution? No TABLE LOCK is required and works perfectly!!!
DECLARE #NextId INT
UPDATE Last_Auto_Id
SET #NextId = LastId = LastId + 1
WHERE Name = 'CustomerNo'
SELECT #NextId
Update statement always uses a lock to protect its update.
You might wanna consider deadlocks. This usually happens when multiple users use the stored procedure simultaneously. In order to avoid deadlock and make sure every query from the user will succeed you will need to do some handling during update failures and to do this you will need a try catch. This works on Sql Server 2005,2008 only.
DECLARE #Tries tinyint
SET #Tries = 1
WHILE #Tries <= 3
BEGIN
BEGIN TRANSACTION
BEGIN TRY
-- this line updates the last_auto_id
update last_auto_id set lastid = lastid+1
COMMIT
BREAK
END TRY
BEGIN CATCH
SELECT ERROR_NUMBER() AS ErrorNumber, ERROR_MESSAGE() as ErrorMessage
ROLLBACK
SET #Tries = #Tries + 1
CONTINUE
END CATCH
END
I prefer doing this using an identity field in a second table. If you make lastid identity then all you have to do is insert a row in that table and select #scope_identity to get your new value and you still have the concurrency safety of identity even though the id field in your main table is not identity.