views:

222

answers:

3

Right now i have code that initiates transactions on SQL Server using the intended method:

ExecuteNonQuery(connection, "BEGIN TRANSACTION");
try
{
   DoABunchOnStuff(connection);
   DoSomeMoreStuff(connection);
   JustAFewMoreThings(connection);

   ExecuteNonQuery(connection, "COMMIT TRANSACTION");
} 
catch (Exception)
{  
   ExecuteNonQuery(connection, "ROLLBACK TRANSACTION");
   throw;
}

Now i'm looking at thinking about the possibility of investigating the idea of using the transaction abstraction provided by ADO.NET:

DbTransaction trans = connection.BeginTransaction();
try
{
   DoABunchOnStuff(connection);
   DoSomeMoreStuff(connection);
   JustAFewMoreThings(connection);

   trans.Commit();
} 
catch (Exception)
{  
   trans.Rollback();
   throw;
}

Problem with this simple conversion from SQL Server based transactions, to ADO.NET transactions, is the error:

ExecuteNonQuery requires the command to have a transaction when the connection assigned to the command is in a pending local transaction. The Transaction property of the command has not been initialized.

Am i correct in assuming that if i wanted to use ADO.NET transactions i would have to completely gut the infrastructure, passing along a DbTransaction object to every method that does, or may, operate inside a transaction?

+2  A: 

Am i correct in assuming that if i wanted to use ADO.NET transactions i would have to completely gut the infrastructure, passing along a DbTransaction object to every method that does, or may, operate inside a transaction?

Yes, exactly - you basically need to associate the transaction you've created with each SqlCommand that ought to be executed under that transaction's umbrella - so you'd have to have something like:

DbTransaction trans = connection.BeginTransaction();
try
{
   DoABunchOnStuff(connection, trans);
   DoSomeMoreStuff(connection, trans);
   JustAFewMoreThings(connection, trans);

   trans.Commit();
} 
catch (Exception)
{  
   trans.Rollback();
   throw;
}

and inside those methods something along the lines of:

public void DoABunchOnStuff(SqlConnection connection, SqlTransaction trans)
{
    using(SqlCommand cmd = new SqlCommand(--sql stmt--, connection, trans)
    {
       ........
    } 
} 
marc_s
It really is an odd construct. What could a transaction **be** on, except for a connection. (in sql server at least)
Ian Boyd
+3  A: 

You are correct, but since you are evidently keeping the connection open the whole time, you could replace this with a TransactionScope instead; it won't promote to DTC as long as there's only one open connection.

Example:

using (TransactionScope tsc = new TransactionScope())
{
    DoABunchOnStuff(connection);
    DoSomeMoreStuff(connection);
    JustAFewMoreThings(connection);
    tsc.Complete();
}

Notes about using the TransactionScope:

  • You must make sure to include Transaction Binding = Explicit Unbind in your connection string. By default transactions are run in implicit-unbind mode which means that they switch to auto-commit mode if the transaction times out. You almost never want the default behaviour, as it can interfere with the atomicity of your transactions and cause what some people refer to as data corruption (even though it's not actual "corruption"). As long as you use the correct parameters in your connection string, you don't need to worry about this.

  • TransactionScope will promote to DTC (distributed transaction) if there is more than one connection in scope, which includes linked servers and OPENROWSET. Although this might seem like undesirable behaviour, your code isn't going to be transactionally safe any other way. Executing manual BEGIN TRAN statements on multiple connections and putting multiple ROLLBACK statements in an exception handler does not ensure atomicity of the entire transaction.

  • Transaction Scopes are designed to be nested and will automatically figure out the difference between beginning a new transaction and enlisting in an existing one. This is a lot more powerful than matching up BEGIN TRAN and COMMIT/ROLLBACK statements, as the latter rely on a connection-local transaction count, whereas the former is actually... scoped. Using TransactionScope is similar to structured transaction handling in SQL Server using SAVE TRAN, TRY/CATCH, and named ROLLBACK - you do not need to worry about what happens if a downstream process or procedure flubs the transactional logic, which is a serious risk when sending raw BEGIN and ROLLBACK statements over ADO.NET.

Aaronaught
TransactionScope looks interesting, but the documentation can't even decide on the proper syntax. And there are comments that talk about possible data corruption in the default mode. i think i'll stick to tried and true transactions.
Ian Boyd
@Ian Boyd: What in the world are you talking about? I've never heard of data corruption caused by `TransactionScope`. More importantly, you can nest them, something you can't do with regular transactions. Way, way more reliable and easier to maintain that what you have now. The code examples on MSDN suck, admittedly, but the library is rock-solid.
Aaronaught
@Aaronaught: i'm only telling you what **Florin Lazar - MSFT** says on the documentation page for `TransactionScope`. Click the link in your answer. He says, *"When using TransactionScope with System.Data.SQLClient, one must specify "Transaction Binding = Explicit Unbind" in the connection string to ensure atomicity inside the scope and prevent data inconsistency or corruption."*Don't shoot the messenger - talk to the guy from Microsoft who warns of possible data corruption.
Ian Boyd
@Ian Boyd: It's an old bug that is trivially easy to work around. Just remember to put that in your connection string and you'll be fine.
Aaronaught
@Aaronaught. It may be fixed, or easily worked around. But since i'm dealing with something as important as atomic transactional integrity, and i've had bad experiences with the transaction abstraction provided by two other database access technologies (BDE and ADO), i'm gun shy about using it. Once bitten, twice shy. Twice bitten, eight times shy. And then i *happen* to read how the default usage pattern of this transactional model can have data corruption. Thrice bitten....
Ian Boyd
@Ian Boyd: I hope you realize that the approach you're using now can also cause data corruption if anyone ever forgets to explicitly roll back or makes a typo and omits the second "M" in `COMMIT`. Connection pooling will cause the connection to be reused while it is still in the middle of a transaction and this can cause some very *interesting* side-effects. With `SqlTransaction` or `TransactionScope`, you are protected against this.
Aaronaught
@Aaronaught: i realize that. That is why the actual code calls functions called **BeginTransaction**, **CommitTransaction**, **RollbackTransaction**. They were omitted for clarity.
Ian Boyd
@Ian Boyd: Okay, so which is safer - depending on a few words being in the connection string, which is usually specified once for the entire application in `app.config`/`web.config`, or relying on developers to always get the `Begin`/`Commit`/`Rollback`/ sequence correct in every place where transactional logic is required? I can't tell you what to do, but at least think about it before dismissing what is more or less the standard now. Just because you haven't been burned **yet** by your solution doesn't mean it's the safest one.
Aaronaught
TransactionScope is not standard: http://www.google.com/search?q=sql+server+transactions+.net i'm sure TransactionScope can be better - only if you get everything exactly right. Your answer has no mention of the thing that has to be set to not get data corruption. i don't know what it is, or why, or how it causes corruption. Is there anything else that needs to be done? What if my update crosses linked servers? What if i do OPENROWSET? What if i can an outer TransactionScope?
Ian Boyd
What's more, is that you were shocked that TransactionScope can lead to data corruption - that i was crazy. You must have found this same issue, and dealt with it already. Not even the MSDN documentation mentions this problem, it happens to be in the community comments. A technology that is the most common configuration is mis-configured? No thanks. Go visit http://connectionstrings.com, and see if any of the SQL Server ADO.NET connection strings mention **explicitunbind**. Anyone using ConnectionStrings.com with TransactionScope risks transactions that aren't.
Ian Boyd
@Ian Boyd: Seeing as how you mention these things, I have to assume you already read about them. But fine - I updated my answer to include all of this information. Incidentally, `TransactionScope` is a lot newer than `SqlTransaction` and took a while to get adopted because it had performance issues with SQL Server 2000, which a lot of people were still using at the time of release. Today it **is** the standard - just get the connection string right.
Aaronaught
I wasn't "shocked", I just said that `TransactionScope` doesn't cause data corruption, and it *doesn't*. An auto-committed transaction is an atomicity problem, not a *data integrity* problem. They are two completely different concepts and it's very important to understand the distinction when working with databases. Community comments should generally be taken with a grain of salt.
Aaronaught
A: 

You may also want to take a look at Linq to SQL. As you can also "SubmitChanges()" (or not submit them) in code to the database. This means you can wrap it in a try catch just like your transaction. This is a bit of an infratstucture change as well but with SQLMetal you can auto generate all the necessary classes. It may or may not be right for your situation.

more info: http://weblogs.asp.net/scottgu/archive/2007/05/19/using-linq-to-sql-part-1.aspx

AGoodDisplayName