In this sample console app I want to update a row in a table, and then insert another row in the same table.
The table is like this
CREATE TABLE [dbo].[Basket2](
[Id] [int] IDENTITY(1,1) NOT NULL,
[UserId] [int] NULL
) ON [PRIMARY]
CREATE UNIQUE NONCLUSTERED INDEX [IX_Basket] ON [dbo].[Basket2]
(
[UserId] ASC
)
So basically a user cannot have 2 baskets.
For reasons beyond this post baskets must not be deleted from the table. Therefore when a user needs a new basket the old one is just set to a unique number (id*-1).
The following code is a sample app that simulates the flow - and fails
private static void Main(string[] args)
{
ISessionFactory sessionFactory = CreateSessionFactory();
int userId = new Random().Next();
int basketId;
using (var session = sessionFactory.OpenSession())
{
using (var tx = session.BeginTransaction(IsolationLevel.ReadUncommitted))
{
var newBasket = new Basket {UserId = userId};
basketId = (int) session.Save(newBasket);
tx.Commit();
}
using (var tx = session.BeginTransaction(IsolationLevel.ReadUncommitted))
{
var basket = session.Get<Basket>(basketId);
basket.UserId = basket.Id*-1;
session.Save(basket);
// comment in this line to make it work:
//session.Flush();
var newBasket = new Basket {UserId = userId};
session.Save(newBasket);
tx.Commit();
}
}
}
The error is:
Unhandled Exception: NHibernate.Exceptions.GenericADOException: could not insert: [ConsoleApplication1.Basket][SQL: INSERT INTO [Basket] (UserId) VALUES (?); select SCOPE_IDENTITY()] ---> System.Data.SqlClient.SqlException: Cannot insert duplicate key row in object 'dbo.Basket' with unique index 'IX_Basket'.
If I Flush the session (commented out lines) it works, but why is this necessary?
I would prefer not having to Flush my session and letting Commit() handle it.
You don't need to Save / Update / SaveOrUpdate any entities which are already in the session.
But you are reusing the same id again. So make sure that the session is flushed before:
using (var tx = session.BeginTransaction(IsolationLevel.ReadUncommitted))
{
var basket = session.Get<Basket>(basketId);
basket.UserId = basket.Id*-1;
// no save
//session.Save(basket);
// flush change on unique field
session.Flush();
var newBasket = new Basket {UserId = userId};
// save new item which is not in the session yet
session.Save(newBasket);
tx.Commit();
}
This is because you add the same unique value again. Of course you change the existing value before, but this is not stored to the database before the session is flushed.
The session is flushed when:
you call flush
before queries (except of Get and Load)
on commit (except you use your own ADO connection)
It is a common misunderstanding that NH performs update or insert on the database when you call Save or Update. This is not the case. Insert and update are performed when flushing the session. (There are some exceptions on that, eg. when using native ids.)
Related
I have to insert a row into the database but the problem is that the primary key is generated based on the total counts of rows.
E.g. if the db has 25601 rows, the ID of the newly inserted record would be CT25602.
I want to use transactions for primary key collisions.
Here is the code I wrote.
public void CreateContact(ContactViewModel input)
{
var transactionScopeOptions = new TransactionOptions
{
IsolationLevel = IsolationLevel.Serializable,
Timeout = TimeSpan.MaxValue
};
using (TransactionScope transaction = new TransactionScope(TransactionScopeOption.Required, transactionScopeOptions))
{
var contactNo = GenerateIdentity();
var contact = MapContactFields(new NavContact { No_ = contactNo }, input);
_db.Contacts.InsertOnSubmit(contact);
_db.SubmitChanges();
transaction.Complete();
}
}
This code gives me deadlocks if two persons are trying to insert a contact in a small timespan.
Any suggestions ? Thank you
Yes, the scenario you described is very likely to deadlock. I would recommend using a sequence instead. If not, then one solution is to acquire an exclusive app lock in the transaction, before scannig for the next identity. See sp_getapplock.
When SaveChanges() is called on the context, all insert/delete/update operations are executed in a single transaction. It is also possible to use DbContextTransaction for transactions. I am trying to simulate deadlock using both of these approaches. When I use DbContextTransaction, I get the deadlock exception right away but SaveChanges() alone does not throw any deadlock exceptions even after an hour. Am I doing something wrong?
Here is the code with DbContextTransaction. I try to update the first row and then the second row in the main thread. I also start another task which tries to update the second row first and then the first row.
while (true)
{
using (var context = new SchoolDBEntities())
{
using (System.Data.Entity.DbContextTransaction dbTran = context.Database.BeginTransaction())
{
Random r = new Random();
int r1 = r.Next();
int r2 = r.Next();
Student std1 = context.Students.First();
std1.StudentName = "test"+r1;
context.SaveChanges();
Student std2 = context.Students.Find(2);
std2.StudentName = "test"+r2;
context.SaveChanges();
dbTran.Commit();
}
}
}
But when I try it with just SaveChanges() it does not generate deadlock:
while (true)
{
using (var context = new SchoolDBEntities())
{
try
{
Random r = new Random();
int r1 = r.Next();
int r2 = r.Next();
Student std1 = context.Students.First();
std1.StudentName = "test" + r1;
Student std2 = context.Students.Find(2);
std2.StudentName = "test" + r2;
context.SaveChanges();
}
}
}
I am using SQL Profiler to trace the transactions. I even added more updates to the second approach just to make that transaction's duration equal to the DbContextTransaction case thinking it might be the reason but still no luck! When I look at the trace, I see that updates belonging to a particular transaction start only after the previous transaction is committed. What could be the reason?
Upon further investigation, I found out that regadless of the order of changes I have made in the context, the order in which SaveChanges() method always sends update queries to the SQL Server is based on the primary key of the table. In other words, even though I try to reverse the order of update request by first changing row 2 and then row 1, SaveChanges() first executes the update query for row 1 and then for row 2. That's why I don't get a deadlock by using just SaveChanges() method. It does not reverse the order of the queries.
I have meeting a problem, the code look like simple, but exception:
DDS.Model.ATest atest = new DDS.Model.ATest();
atest.AID = Guid.NewGuid();
ISession session = SessionProvider.GetNewSession();
using (ITransaction transaction = session.BeginTransaction())
{
session.SaveOrUpdate(atest);
int count = session.CreateQuery("from ATest").List().Count;
//Above row throw a exception:
//Batch update returned unexpected row count from update; actual row count: 0; expected: 1
transaction.Commit();
}
You are trying to load items before saving. Commit the transaction first, and then execute the query.
DDS.Model.ATest atest = new DDS.Model.ATest();
//atest.AID = Guid.NewGuid(); // You should not assign IDs by yourself
ISession session = SessionProvider.GetNewSession();
using (ITransaction transaction = session.BeginTransaction())
{
session.SaveOrUpdate(atest);
transaction.Commit();
}
int count = session.CreateQuery("from ATest").List().Count;
But that doesn't seem to be the problem in your case. I believe you have ID mapped as Guid or Guid.comb. You should not assign the value to ID. NHibernate will take care of that.
When you assign the value and call session.SaveOrUpdate(), it will try to do update since ID value is not Guid.Empty. The update method will fail with the exception: Batch update returned unexpected row count from update; actual row count: 0; expected: 1, since UPDATE ... WHERE AID = <some guid> will be executed.
I'm trying to get the data that has been successfully input into the database via ADO.NET transaction.
Once you've called trans.Commit() there doesn't seem to be a way of getting back the data that has been committed since all identity columns that were created during the transaction are 'virtual' since it is an offline dataset until commit
Many thanks
[EDIT]
Ahh, the problem is, I can't do a reselect as I don't have anything unique to select on other than the identity of the data inserted as part of the transaction.
I can't get the last entered item as this is a multiuser system
Code Sample from a book, not the code in question, but good enough to illustrate what I need:
using System.Data;
using System.Data.SqlClient;
namespace DataAdapterTransaction
{
class Program
{
private static string sqlConnectString = "Data Source=(local);" +
"Integrated security=SSPI;Initial Catalog=AdoDotNet35Cookbook;";
private static string sqlSelect = "SELECT * FROM DataAdapterTransaction";
static void Main(string[] args)
{
object[,] o1 = {{ "1", "field 1.1", "field 2.1" },
{ "2", "field 1.2", "field 2.2" }};
InsertRecords(o1);
object[,] o2 = {{ "3", "field 1.3", "field 2.3" },
{ null, "field 1.4", "field 2.4" }};
InsertRecords(o2);
// Retrieve and output the contents of the table
SqlDataAdapter daRead = new SqlDataAdapter(sqlSelect, sqlConnectString);
DataTable dtRead = new DataTable( );
daRead.Fill(dtRead);
Console.WriteLine("---TABLE DataAdapterTransaction---");
foreach (DataRow row in dtRead.Rows)
Console.WriteLine("Id = {0}\tField1 = {1}\tField2 = {2}",
row["Id"], row["Field1"], row["Field2"]);
Console.WriteLine("\nPress any key to continue.");
Console.ReadKey( );
}
static void InsertRecords(object[,] o)
{
DataTable dt = new DataTable( );
SqlTransaction tran;
SqlConnection connection = new SqlConnection(sqlConnectString);
// Create a DataAdapter
SqlDataAdapter da = new SqlDataAdapter(sqlSelect, connection);
// Stop updating when an error is encountered for roll back.
da.ContinueUpdateOnError = false;
// Create CommandBuilder and generate updating logic.
SqlCommandBuilder cb = new SqlCommandBuilder(da);
// Create and fill a DataTable with schema and data
da.Fill(dt);
// Open the connection
connection.Open( );
// Begin a new transaction and assign it to the DataAdapter
tran = connection.BeginTransaction( );
da.SelectCommand.Transaction = tran;
// Add two rows that will succeed update
for (int i = 0; i <= o.GetUpperBound(0); i++)
{
dt.Rows.Add(new object[] { o[i, 0], o[i, 1], o[i, 2] });
Console.WriteLine(
"=> Row with [Id = {0}] added to DataTable.", o[i, 0]);
}
Console.WriteLine("=> Updating data source using DataAdapter.");
try
{
da.Update(dt);
tran.Commit( );
Console.WriteLine("\nTRANSACTION COMMIT.\n");
}
catch (Exception ex)
{
tran.Rollback( );
Console.WriteLine("\nTRANSACTION ROLLBACK.\n{0}\n", ex.Message);
}
finally
{
connection.Close( );
}
}
}
}
Okay, so what i'm after is just after the transaction commit, I want to get the (scope) identity of the the last inserted row.
My application is successful in updating three dataadapters as part of the transaction, however I am having dificulty looking at the final committed data. I can do a select of the table and see it in there, but that really isn't good enough for production code.
SC
You may just need to reselect the data.
The Books Online says that you should call a Fill again to bring the update your Dataset:
http://msdn.microsoft.com/en-us/library/33y2221y(VS.71).aspx
I generally set my Insert and Update commands so that they they return back a valid DataRow for my DataTable and control the update of the rows in the application that way.
Right so I should do:
// update datatable
da.Update(dt);
// commit updates
tran.Commit( );
// get the updated datatable
da.Fill(dt);
I assume all the identity colums will be updated.
I'll give it a go
SC
I understand you're using identity columns, but is there any natural key in the data you can use to reselect?
(that then raises the question of 'why use identities' but that's a whole other subject...)
Unfortunately no, I cannot reselect using a natural key... I'm stuck with identities... after 8 hours of head banging I contemplated adding a guid field so I could get it to work, but decided that it is against my principles to give up!
SC
This MSDN article describes how to get back identity values when calling the Update method of a DataAdapter.
I have the following Unit Test method:
void TestOrderItemDelete()
{
using (new SessionScope())
{
var order = Order.FindById(1234);
var originalItemCount = order.OrderItems.Count;
Assert.IsTrue(originalCount > 0);
var itemToDelete = order.OrderItems[0];
itemToDelete.DeleteAndFlush(); // itemToDelete.Delete();
order.Refresh();
Assert.AreEqual(originalCount - 1, order.OrderItems.Count);
}
}
As you can see from the comment after the DeleteAndFlush command, I had to change it from a simple Delete to get the Unit test to pass. Why is this? The same is not true for my other unit test for adding an OrderItem. This works just fine:
void TestOrderItemAdd()
{
using (new SessionScope())
{
var order = Order.FindById(1234);
var originalItemCount = order.OrderItems.Count;
var itemToAdd = new OrderItem();
itemToAdd.Order = order;
itemToAdd.Create(); // Notice, this is not CreateAndFlush
order.Refresh();
Assert.AreEqual(originalCount + 1, order.OrderItems.Count);
}
}
All of this came up when I started using Lazy Instantiation of the Order.OrderItems relationship mapping, and had to add the using(new SessionScope) block around the test.
Any ideas?
This is difficult to troubleshoot without knowing the contents of your mappings, but one possibility is that you have the ID property of the OrderItem mapped using an identity field (or sequence, etc.) in the DB. If this is the case, NHibernate must make a trip to the database in order to generate the ID field, so the OrderItem is inserted immediately. This is not true of a delete, so the SQL delete statement isn't executed until session flush.