Cannot swap unique value on two rows with EF - c#

I'm trying to swap the value in a unique column for two (or more rows). For example:
Before update:
Row 1 = 1
Row 2 = 2
After update:
Row 1 = 2
Row 2 = 1
I'm using Entity Framework. These changes take place on a single commit for the same context. However, I always get a unique constrain violation when I attempt this update. Is EF not using a transaction?
For completeness, here is a snippet of my table design:
[FeeSchemeId] UNIQUEIDENTIFIER NOT NULL,
[SortPosition] INT NOT NULL,
UNIQUE (FeeSchemeId, SortPosition)
I'm trying to update the 'SortPosition' column. The code is a bit complex to display here, but I can assure you that it is the same context with a single final commit. The error is only thrown when EF tries to write to the database.
UPDATE:
-
Using SQL Server Profiler I can see that EF is running separate UPDATE for each affected row. Should EF not be using a single transaction for one call to SaveChanges()?
-
UPDATE 2:
Turns out EF is using a single transaction after all. SQL Profiler was filtering it out.

You can't do it with 2 statements also with SQL Server. You need to use a third value
BEGIN TRANSACTION;
UPDATE MyTable Set Id = 200 where Id = 1;
UPDATE MyTable Set Id = 1 where Id = 2;
UPDATE MyTable Set Id = 2 where Id = 200;
COMMIT;
BTW, SQL Server profiler shows BEGIN TRANSACTION/COMMIT statements

An alternative trick I have used which doesn't rely on temporary values (which themselves risk violating uniqueness) is to issue a single UPDATE as in:
UPDATE MyTable
SET ID = case when id = 1 then 2 else 1 end
WHERE ID in (1, 2)
Unfortunately EF is not smart enough to generate those type of statements by itself.

Related

Why is UPDATE CHECK for all properties set to 'NEVER' when a ROWVERSION/TIMESTAMP is added? [duplicate]

I am updating a single column in a table using Linq, take fictitious table below.
MyTable (PKID, ColumnToUpdate, SomeRandomColumn)
var row = (from x in DataContext.MyTable
where b.PKID == 5
select x).FirstOrDefault();
row.ColumnToUpdate = 20;
DataContext.SubmitChanges();
This updates the column to as expected, no surprises here. However when I inspect the SQL commands which are generated, it does this:
UPDATE [dbo].[MyTable ]
SET [ColumnToUpdate ] = #p2
WHERE ([PKID] = #p0) AND ([SomeRandomColumn] = #p1)
This is performing the update, but only if all columns have matched the values of what Entity expects them to be, rather than referencing the Primary Key column on it's own.
If a database column is changed by another process, which is very feasible in this particular project; eg. There is a window between getting the row you want to manipulate, calculating the changes you would like to set the value to, and issuing the update command as a batch of rows. In this situation the query will cause an exception, causing a partial update, unless I trap, reload the data and resend individual queries. It also has a downside that the row information can be quite large (ie, containing HTML mark up for instance), and the whole thing gets passed to SQL and slows the system down when larger batches are processed.
Is there a way of making Linq / Entity to issue update commands based only on the PK column in the Where clause?
I never used LINQ-to-SQL for production projects and I never were aware of it applying optimistic concurrency1 by default.
This is the default behavior:
If a table doesn't have a Timestamp/Rowversion column2, all columns have "Update Check" set to "Always" in the DBML (except primary key columns and computed columns, i.e. all updateable columns).
If a table does have a Timestamp/Rowversion column, this column has "Time Stamp" set to "True" in the DBML and all columns have "Update Check" = "Never".
Either "Update Check" or "Time Stamp" mark a column as concurrency token. That's why in update statements you see these additional predicates on (not so) "random" columns. Apparently, the tables in your model didn't have Timestamp/Rowversion columns, hence an update checks the values of all updateable columns in the table.
1 Optimistic concurrency: no exclusive locks are set when updating records, but existing values of all or selected columns are checked while updating. If one of those column value was changed by another user between getting the data and saving them, an update exception occurs.
2 A column of data type Timestamp or Rowversion is automatically incremented when a record is updated and therefore detects all concurrent changes to this record.

Map identity value to object after merge statement

I have a table called People with the following schema:
Id INT NOT NULL IDENTITY(1, 1)
FirstName NVARCHAR(64) NOT NULL
LastName NVARCHAR964) NOT NULL
I am using a query like this one to perform inserts and updates in one query:
MERGE INTO People AS TARGET
USING ( VALUES
(#id0, #firstName0, #lastname0),
(#id1, #firstName1, #lastname1)
...
) AS SOURCE ([Id],[FirstName],[LastName])
ON TARGET.[Id] = SOURCE.[Id]
WHEN MATCHED BY TARGET THEN
UPDATE SET
[FirstName] = SOURCE.[FirstName],
[LastName] = SOURCE.[LastName]
WHEN NOT MATCHED BY TARGET THEN
INSERT ([FirstName],[LastName])
VALUES ([FirstName],[LastName])
WHEN NOT MATCHED BY SOURCE THEN
DELETE
OUTPUT $action, INSERTED.*;
My application is structured such that the client calls back to the server to load the existing state of the app. The client then creates/modifies/deletes entities locally and pushes those changes to the server in one bunch.
Here's an example of what my "SaveEntities" code currently looks like:
public void SavePeople(IEnumerable<Person> people)
{
// Returns the query I mentioned above
var query = GetMergeStatement(people);
using(var command = new SqlCommand(query))
{
using(var reader = command.ExecuteReader())
{
while(reader.Read())
{
// how do I tie these records back to
// the objects in the people collection?
}
}
}
}
I can use the value in the $action column to filter down to just INSERTED records. INSERTED.* returns all of the columns in TARGET for the inserted record. The problem is I have no way of distinctly linking those results back to the collection of objects passed into this method.
The only solution I could think of was to add a writable GUID column to the table and allow the MERGE statement to specify that value so I could link back to these objects in code using that and assign the ID value from there, but that seems like it defeats the purpose of having an automatic identity column and feels convoluted.
I'm really curious how this can work because I know Entity Framework does something to mitigate this problem (to be clear, I believe I'd have to same problem were I to be using a pure INSERT statement instead of a MERGE). In EF I can add objects to the model and call Entity.SaveChanges() and have the entity's ID property auto-update using magic. I guess it's that kind of magic I'm looking to understand more.
Also, I know I could structure my saves to insert one record at a time and cascade the changes appropriately (by returning SCOPE_IDENTITY for every insert) but this would be terribly inefficient.
One of the things I love about the MERGE statement is that the source data is in scope in the OUTPUT clause.
OUTPUT $action, SOURCE.Id, INSERTED.Id;
On insert, this will give you three columns: 'INSERT' in the first, the values of #id0 and #id1 in the second, and the matching, newly inserted Id values in the third.
In your C# code, just read the rows as you normally would.
while (reader.Read())
{
string action = reader.GetString(0);
if (action == "INSERT")
{
int oldId = reader.GetInt32(1);
int newId = reader.GetInt32(2);
// Now do what you want with them.
}
}
You can check for "DELETE" and "UPDATE" too, but keep in mind that ordinal 2 will be NULL on "DELETE" so you need to make sure you check for that before calling reader.GetInt32 in that case.
I've used this, in combination with table variables (OUTPUT SOURCE.Id, INSERTED.Id INTO #PersonMap ([OldId], [NewId])), to copy hierarchies 4 and 5 tables deep, all with identity columns.

sql-server primary key (ID) does not increment correctly

I am working on a project with C# and SQL Server 2016.
In SQL Server Management Studio I add a primary key to an existing table, by right clicking the table and choosing design table, it worked fine.
But as soon as I add some new users and deleted some users, it started behaving strange, I mean after id=4 next user should have id=5 but instead it was given id=7.
Screenshot of SSMS
Actually you have deleted the users with id 5 and 6. The counter for the auto increment is not reset or reversed automatically. Its the normal and standard behavior.
This may sound unbelievable, but if you want an incrementing record number, SQL Server has no support for you. A transaction that is rolled back or a server restart can leave holes in the numbers.
If you delete a row, you'll have to implement that manually. Say that records 1, 2 and 3 exist. You delete record 2. What number should the new order get? If you say 2, remember that means order 2 is created after order 3, which would confuse a lot of people.
Probably records with Id 5 and 6 ar been deleted
IDENITY T-SQL
Specifically the 'Reuse of values'
'For a given identity property with specific seed/increment, the identity values are not reused by the engine. If a particular insert statement fails or if the insert statement is rolled back then the consumed identity values are lost and will not be generated again. This can result in gaps when the subsequent identity values are generated. '
Hope this helps to explain
It might not just be that the records have been deleted. Every time you try to INSERT data into that table the seed will be incremented. Take, as a very simple example:
CREATE TABLE #Sample (ID int IDENTITY(1,1), String char(1));
GO
INSERT INTO #Sample (String) VALUES ('A'); --This'll get ID 1
GO
INSERT INTO #Sample (String) VALUES ('AB'); --This'll get ID 2, but fail insertion (due to truncation)
GO
INSERT INTO #Sample (String) VALUES ('C'); --This'll be ID 3.
GO
--Let's Check.
SELECT *
FROM #Sample;
GO
--The SAme is tue for Transactions.
BEGIN TRANSACTION;
INSERT INTO #Sample (String) VALUES ('Q'); --This'll get ID 4
ROLLBACK;
INSERT INTO #Sample (String) VALUES ('S'); --This'll get ID 5
SELECT *
FROM #Sample;
GO
--DELETE
--Let's delete EVERYTHING
DELETE
FROM #Sample;
INSERT INTO #Sample (String) VALUES ('A'); --This'll get ID 6, Not 1
SELECT *
FROM #Sample;
GO
--Finally, howerver, truncate:
TRUNCATE TABLE #Sample;
--Truncate RESETS the seed.
INSERT INTO #Sample (String) VALUES ('A'); --This'll be 1;
SELECT *
FROM #Sample;
GO
--Clean up
DROP TABLE #Sample;
Hope that helps.
Assuming that you have used the identity feature in SQL server. When you try to insert a new record into a table having an identity column, the identity value will be increased even if the insert failed due.
Suppose I have a table where the identity column SeqNo have the current value as 9 and incremented by 1.
I'm inserting a new record but it failed due to some data issues.
So I fixed the issues are tried re-inserting the same.
Assuming no other inserts happened during this time, the SeqNo for the new record should be 10, but it will be 11.
because SQL Server won't roll back the Identity Seed if the insert fails. So you will have to manually re-seed the column
Same in the case of delete and insert. I have the Maximum SeqNo as 5.I deleted the SeqNo 5 and inserted a new record, but for my new record, the SeqNo will be 6 (or the next identity value)
So, if you want to specify some values to the identity column, try inserting with SET IDENTITY INSERT ON. like this
SET IDENTITY INSERT YourTable ON
INSERT INTO YourTable(SeqNo,Name)
VALUES(5,'ABC')
SET IDENTITY INSERT YourTable OFF
When we delete of cancel(by pressing ESC button while filling values in table), it takes it as increment, you can truncate your table(
NOTE: Truncate will delete all values from your table
)

Explain Code First CRUD auto-generated SQL for Identity column

Code-first auto generates an insert procedure code as below for a table that has ProductID as primary key (identity column).
CREATE PROCEDURE [dbo].[InsertProducts]
#ProductName [nvarchar](max),
#Date [datetime],
AS
BEGIN
INSERT dbo.ProductsTable([ProductName], [Date])
VALUES (#ProductName, #Date)
-- identity stuff starts here
DECLARE #ProductID int
SELECT #ProductID = [ProductID]
FROM dbo.FIT_StorageLocations
WHERE ##ROWCOUNT > 0 AND [ProductID] = scope_identity()
SELECT t0.[ProductID]
FROM dbo.ProductsTable AS t0
WHERE ##ROWCOUNT > 0 AND t0.[ProductID] = #ProductID
END
GO
Could you please explain the code that handles the identity column? Also, if an insert procedure is to be manually written from scratch, would it be handled differently?
If for example I would remove this auto generated code, I would encounter one of the following errors:
Procedure ....expects parameter '#ProductID', which was not supplied
Store update, insert, or delete statement affected an unexpected number of rows (0). Entities may have been modified or deleted since entities were loaded. See http://go.microsoft.com/fwlink/?LinkId=472540 for information on understanding and handling optimistic concurrency exceptions.
In the app, this is how I call the procedure which works fine until I try to mess with the code first auto generated SQL:
using (var db = new AppContext())
{
var record = new ProductObj()
{
ProductName= this.ProductName,
Date = DateTime.UtcNow
};
db.ProductDbSet.Add(record);
db.SaveChanges();
}
I guess there are two things to be explained here.
Why a SELECT statement when I insert stuff?
Let's first see what a regular insert by Entity Framework looks like. By "regular" I mean an insert without mapping CUD actions to stored procedures. The normal pattern is:
INSERT [dbo].[Product]([Name], ...)
VALUES (#0, ...)
SELECT [Id]
FROM [dbo].[Product]
WHERE ##ROWCOUNT > 0 AND [Id] = scope_identity()
So the INSERT is followed by a SELECT. This is because EF needs to know the identity value that the database assigns to the new Product to assign it to the entity object's Product.ProductId property and to track the entity. If for some reason you'd decide to do an update immediately after the insert, EF will be able to generate an update statement like UPDATE ... WHERE Id = #0.
When the insert is handled by a stored procedure, the sproc should return the new Id value in a way that looks like the regular insert. It expects to receive a one-column result set of which the column is named after the identity column. It should contain one row, the new identity value.
So that's why there is a SELECT statement in there, and why EF complains if you remove it. But, you might ask, does EF really need 7 lines of code to get an assigned identity value?
Why so much code?
Honestly, I have to speculate a bit here, because it isn't documented as far as I can find. But let's look at a minimal working version:
INSERT [dbo].[Products]([Name])
VALUES (#Name)
SELECT scope_identity() AS ProductId;
This does the job. It's even the standard example of many tutorials, including official ones, on mapping CUD actions to stored procedures.
But a database can be stuffed with triggers, constraints, defaults, etc. It's hard to predict their influence on the returned scope_identity() under the wide range of circumstances EF may encounter. So EF wants to guarantee that the returned value really belongs to the newly inserted record. And that a record has actually been inserted in the first place. That's why it adds the SELECT from the Product table, including the ##ROWCOUNT.
To implement these safeguards, a minimal version would be:
INSERT [dbo].[Products]([Name])
VALUES (#Name)
SELECT t0.[ProductId]
FROM [dbo].[Products] AS t0
WHERE ##ROWCOUNT > 0 AND t0.[ProductId] = scope_identity()
Same as in the regular insert.
That's as far as I can follow EF. It puzzles me a bit that this single SELECT apparently is enough for a regular INSERT but not for a stored procedure. I can't explain why there are two SELECTs in the generated code.

Avoiding race-condition when manually implementing IDENTITY-like increment for a SQL Server DB column

I'm building an ASP.NET MVC 2 site that uses LINQ to SQL. In one of the places where my site accesses the DB, I think a race condition is possible.
DB Architecture
Here are some of the columns of the relevant DB table, named Revisions:
RevisionID - bigint, IDENTITY, PK
PostID - bigint, FK to PK of Posts table
EditNumber - int
RevisionText - nvarchar(max)
On my site, users can submit a Post and edit a Post later on. Users other than the original poster are able to edit a Post - so there is scope for multiple edits on a single Post simultaneously.
When submitting a Post, a record in the Posts table is created, as well as a record in the Revisions table with PostID set to the ID of the Posts record, RevisionText set to the Post text, and EditNumber set to 1.
When editing a Post, only a Revisions record is created, with EditNumber being set to 1 higher than the latest edit number.
Thus, the EditNumber column refers to how many times a Post has been edited.
Incrementing EditNumber
The challenge that I see in implementing those functions is incrementing the EditNumber column. As that column can't be an IDENTITY, I have to manipulate its value manually.
Here's my LINQ query for determining what EditNumber a new Revision should have:
using(var db = new DBDataContext())
{
var rev = new Revision();
rev.EditNumber = db.Revisions.Where(r => r.PostID == postID).Max(r => r.EditNumber) + 1;
// ... (fill other properties)
db.Revisions.InsertOnSubmit(rev);
db.SubmitChanges();
}
Calculating a maximum and incrementing it can lead to a race condition.
Is there a better way to implement that function?
Update directly in the database and return the new revision:
update Revisions
set EditNumber += 1
output INSERTED.EditNumber
where PostID = #postId;
Unfortunately, this is not possible in LINQ. In fact, is not possible in the client at all, no matter the technology used, short of doing pessimistic locking which has too many drawback to worth considering.
Updated:
Here is how I would insert a new revision (including first revision):
create procedure usp_insertPostRevision
#postId int,
#text nvarchar(max),
#revisionId bigint output
as
begin
set nocount on;
declare #nextEditNumber (EditNumber int not null);
declare #rc int = 0;
begin transaction;
begin try
update Posts
set LastRevision += 1
output INSERTED.LastRevision
into #nextEditNumber (EditNumber)
where PostId = #postId;
set #rc = ##rowcount;
if (#rc <> 1)
raiserror (N'Expected exactly one post with Id:%i. Found:%i',
16, 1 , #postId, #rc);
insert into Revisions
(PostId, Text, EditNumber)
select #postID, #text, EditNumber
from #nextEditNumber;
set #revisionId = scope_identity();
commit;
end try
begin catch
... // Error handling omitted
end catch
end
I omitted the error handling, see Exception handling and nested transactions for a template procedure than handles errors and nested transactions properly.
You'll notice the Posts table has a LastRevision field that is used as the increment for the post revisions. This is much better than computing the MAX each time you add a revision, as it avoid a (range) scan of Revisions. It also acts as a concurrency protection: only one transaction at a time will be able to update it, and only that transaction will proceed with inserting a new revision. Concurrent transactions will block and wait until the first one commits, then the next transaction unblocked will correctly update the revision number to +1.
Can multiple users edit the same post at the same time? If not then you do not have a race condition unless some how a single user can submit multiple edits simultaneously.
If revisions are only permitted by the user who submitted the comment then you're OK with the above - if multiple users can be revising a single comment then there's scope for problems.
Since there is only one record in the Posts table per Post, use a lock.
Read the record in the Posts table and use a table hint [WITH (ROWLOCK, XLOCKX)] to get an exclusive lock. Set the lock timeout to wait a few milliseconds.
If the process gets the lock, then it can add the revision record. If the process cannot get the lock, then have the process try again. After a few retries if the process cannot get a lock, return an error.
Since EditNumber is a property determined by membership in a collection, have the collection provide it.
Make EditNumber a computed column - COUNT of records for same post with lesser RevisionID.

Categories