Using Transactions or Savechanges(False) and Acceptallchanges()

Using Transactions or SaveChanges(false) and AcceptAllChanges()?

With the Entity Framework most of the time SaveChanges() is sufficient. This creates a transaction, or enlists in any ambient transaction, and does all the necessary work in that transaction.

Sometimes though the SaveChanges(false) + AcceptAllChanges() pairing is useful.

The most useful place for this is in situations where you want to do a distributed transaction across two different Contexts.

I.e. something like this (bad):

using (TransactionScope scope = new TransactionScope())
{
//Do something with context1
//Do something with context2

//Save and discard changes
context1.SaveChanges();

//Save and discard changes
context2.SaveChanges();

//if we get here things are looking good.
scope.Complete();
}

If context1.SaveChanges() succeeds but context2.SaveChanges() fails the whole distributed transaction is aborted. But unfortunately the Entity Framework has already discarded the changes on context1, so you can't replay or effectively log the failure.

But if you change your code to look like this:

using (TransactionScope scope = new TransactionScope())
{
//Do something with context1
//Do something with context2

//Save Changes but don't discard yet
context1.SaveChanges(false);

//Save Changes but don't discard yet
context2.SaveChanges(false);

//if we get here things are looking good.
scope.Complete();
context1.AcceptAllChanges();
context2.AcceptAllChanges();

}

While the call to SaveChanges(false) sends the necessary commands to the database, the context itself is not changed, so you can do it again if necessary, or you can interrogate the ObjectStateManager if you want.

This means if the transaction actually throws an exception you can compensate, by either re-trying or logging state of each contexts ObjectStateManager somewhere.

See my blog post for more.

SaveChanges vs. AcceptAllChanges in Entity Framework

ObjectContext.AcceptAllChanges Method - MSDN

If the SaveChanges method was called and the AcceptAllChangesAfterSave
was not specified, the user must call the AcceptAllChanges method. The
AcceptAllChanges method is useful in the scenario where a transaction
has failed and a user wants to retry.

You may see this: http://blogs.msdn.com/b/alexj/archive/2009/01/11/savechanges-false.aspx

If you call SaveChanges() or SaveChanges(true),the EF simply assumes
that if its work completes okay, everything is okay, so it will
discard the changes it has been tracking, and wait for new changes.

Unfortunately though if something goes wrong somewhere else in the
transaction, because the EF discarded the changes it was tracking, we
can’t recover.

This is where SaveChanges(false) and AcceptAllChanges() come in.

SaveChanges(false) tells the EF to execute the necessary database
commands, but hold on to the changes, so they can be replayed if
necessary.

Now if the broader transaction fails you can retry the EF specific
bits, with another call to SaveChanges(false). Alternatively you can
walk through the state-manager to log what failed.

Once the broader transaction succeeds, you simply call
AcceptAllChanges() manually, and the changes that were being tracked
are discarded.


Transactional operation with SaveChanges and ExecuteStoreCommand

Have a look at this answer:

Entity Framework - Using Transactions or SaveChanges(false) and AcceptAllChanges()?

The answer does exactly what you require having a transaction, over multiple data contexts.

This post on Transactions and Connections in Entity Framework 4.0 I found really helpful too.

For people who may need a simpler solution, here's what I use when I need to mix ExecuteStoreCommand and SaveChanges in a transaction.

using (var dataContext = new ContextEntities())
{
dataContext.Connection.Open();
var trx = dataContext.Connection.BeginTransaction();

var sql = "DELETE TestTable WHERE SomeCondition";
dataContext.ExecuteStoreCommand(sql);

var list = CreateMyListOfObjects(); // this could throw an exception
foreach (var obj in list)
dataContext.TestTable.AddObject(obj);
dataContext.SaveChanges(); // this could throw an exception

trx.Commit();
}

Understanding Entity Framework transactions and their behavior

As was suggested by @WynDysel in a comment section - the problem is resolvable by putting a context in a using block.

The actual reasons for the issue are still unknown to me. It looks logical, that unless something is explicitly said to be committed - to be committed. Well, I guess I have to live with this solution for now.

Perhaps I should make some clarifications about the reasons why I was not using the using block to begin with. It's because the DbContext is used from within a service. Within a service there are multiple operations being done in scope of the same transaction.
To multiple entities of database. So when the code is ready for commit - a Commit() method is executed and all of the changes done are pushed to DB at once. Otherwise if something goes wrong along the way, then all of the changes are rolled back. So for this I needed a service and normally am not allowed to use a using block by design.

To make a long story short - I will be using following service for managing context and transaction.

public class DbService : IDisposable
{
private bool _isDisposed = false;
private ApplicationDbContext _context;
private DbContextTransaction _transaction;

public DbService()
{
_context = new ApplicationDbContext();
_transaction = _context.Database.BeginTransaction();
}

public void InsertInvoice(int invoiceId)
{
try
{
var invoice1 = new OracleDatabaseService.Models.Invoice()
{
InvoiceId = (invoiceId).ToString(),
ClientId = "3",
ExternalSystemId = "0"
};

_context.Invoices.Add(invoice1);
_context.SaveChanges();
}
catch (Exception)
{
Dispose(false);
throw;
}
}

public void Commit(bool isFinal)
{
if (!_isDisposed)
{
_transaction.Commit();

if (isFinal)
{
Dispose(false);
}
else
{
_transaction.Dispose();
_transaction = _context.Database.BeginTransaction();
}
}
}

public void Rollback(bool isFinal)
{
if (!_isDisposed)
{
if (isFinal)
{
Dispose(false);
}
else
{
_transaction.Rollback();
_transaction.Dispose();
_transaction = _context.Database.BeginTransaction();
}
}
}

public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}

protected virtual void Dispose(bool disposing)
{
if (!_isDisposed)
{
if (disposing)
{
// Free other state (managed objects).
}

if (_transaction != null)
{
if (_transaction.UnderlyingTransaction.Connection != null)
{
_transaction.Rollback();
}

_transaction.Dispose();
}

if (_context != null)
{
_context.Dispose();
}

_isDisposed = true;
}
}

~DbService()
{
Dispose(false);
}
}

It is still possible to use the service in a using block. If something along the way goes wrong then a destructor shall be called to roll back the transaction and dispose of a context. There are 2 helper methods for committing and rolling back chnages manually. It could either be a final commit when the service is no longer needed or a temporary commit of current transaction and initialization of a new transaction while keeping an integrity of a service.

InsertInvoice method's contexts are also wrapped in a try/catch block in case something unexpected goes wrong.

I can't afford to insert any pending transaction data on a production environment so am taking all possible precautions! Perhaps I will be asking a question on Github about this issue Entity Framework creators themselves.

Update #1

It is very unfortunate, but the code I provided above does NOT guarantee that records will not be inserted. You have to make some additional validations, when using the service.

For example, this testcase will cause the data to be inserted into database sometimes:

[TestMethod]
public void TestFail()
{
int invoiceId = 3700;

Parallel.For(0, 30, i =>
{
var srvc = new DbService();
srvc.InsertInvoice(invoiceId + i, i);

if (i > 15)
{
throw new Exception();
}
});
}

And following code will guarantee disposal of context correctly:

[TestMethod]
public void TestGood()
{
int invoiceId = 3700;

Parallel.For(0, 30, i =>
{
DbService srvc = null;

try
{
srvc = new DbService();
srvc.InsertInvoice(invoiceId + i, i);

if (i > 25)
throw new Exception();
}
catch(Exception ex)
{
if (srvc != null)
srvc.Dispose();

throw ex;
}
});
}

When to use BeginTransaction in EF 6? vs SaveChanges

SaveChanges always executes its DML in a transaction even if you do not specify a transaction explicitly.

Adding entities to the context does not make database calls. The only time DML is executed is when SaveChanges is called. Therefore the code that you posted is atomic (which you call "correct").

Multiple contexts cannot share a transaction (easily). You should not be using multiple contexts anyway in the majority of the cases. Execute your work in one context and one transaction by default.

Entity Framework Core - Error Handling on multiple contexts

You can call context.Database.BeginTransaction as follows:

                using (var dbContextTransaction = context.Database.BeginTransaction())
{
context.Database.ExecuteSqlCommand(
@"UPDATE Blogs SET Rating = 5" +
" WHERE Name LIKE '%Entity Framework%'"
);

var query = context.Posts.Where(p => p.Blog.Rating >= 5);
foreach (var post in query)
{
post.Title += "[Cool Blog]";
}

context.SaveChanges();

dbContextTransaction.Commit();
}

(taken from the docs)

You can therefore begin a transaction for dbContext in your case and if the second command failed, call dbContextTransaction.Rollback();

Alternatively, you can implement the cleanup logic yourself, but it would be messy to maintain that as your code here evolves in the future.

What does SaveChanges() exactly do in EF6?

My understanding is, all changes to the entities (especially where there are relationships that have cascaded deletes, or, reinsert an item that has been deleted) is to sort the operations so they are carried out in the correct order.

For example, if you have a table with a unique constraint, and you have deleted one entity with a unique value on the column with the constraint and reinserted another entity with the same value, the operations are carried out in the correct order so the underlying dmbs doesn't throw a unique constraint exception. The goes for non auto incremented primary keys and a variety of other things although hopefully you get the gist of it.

The entities are stored in a graph with the relationships as edges so it can sort the graph and perform the operations in the correct order.

This is carried out by the ChangeTracker. I know this from working with / building my own entity tracker using the source code from the awesome IQToolkit.

I also understand that this is carried out in a single transaction, if the underlying dmbs supports it...

Also, in your example, you only need to call

SaveChanges()

Once not after each time you change an entity.

You also don't need to create an explicit transaction and commit it, as SaveChanges does this internally, unless you need to rollback the transaction due to some external factor

EDIT

To explicitly answer your questions in bold:

"Now what exactly does SaveChanges() Do? and how does it differ from the commit??"

It sorts the sql commands generated by each change made to the entities and executes them, in a single transaction, in an order that will not violate any relationship or field constraints setup within the database. As it uses its own transaction, you don't need to wrap the operation in a new transaction and commit it, unless you have a reason to roll the operations back due to some external factor.

It differs from Commit as Commit will commit any changes made during a transaction, while SaveChanges creates it's own transaction around the updates and commits the transaction. What you are doing is nesting the transaction created by SaveChanges in the outer transaction, so you can cancel it if required.

"Does it begin a new (maybe internal) transaction for each insert and then commit it?"

No, it wraps them all and commits in a single, internal transaction.



Related Topics



Leave a reply



Submit