Typed DataSets and SqlTransaction
I've recently been using typed DataSets to talk to a database for the first time. Strange, I know, but I learned how to do database access using raw ADO.NET back in the 1.1 days, and never really bothered with the nice visual DataSet designers or the new TableAdapter helpers.
Anyway, I have a project here with a DataSet containing four tables (and hence four TableAdapter classes). When the client program sends the changes in the DataSet back to the server, I want to update the tables, but I want to do it in a transaction so that if any one thing fails, no changes are written to the database.
Turns out that this is non-trivial. Support for transactions is not built in to the generated TableAdapter classes. Luckily it's possible to implement it yourself using .NET 2.0's partial class feature.
Sahil has written a bit about how to do this if you just have the one TableAdapter. In his post, he talks about creating a BeginTransaction method on that TableAdapter. That works well, but if you have more than one, you need some way to share a transaction across all your adapters.
So here's my solution. Instead of a BeginTransaction method, I have implemented a Transaction property on my TableAdapters, like this:
partial class CategoriesTableAdapter { public SqlTransaction Transaction { get { return _adapter.SelectCommand.Transaction; } set { if (_adapter == null) InitAdapter(); Connection = value.Connection; _adapter.InsertCommand.Transaction = value; _adapter.UpdateCommand.Transaction = value; _adapter.DeleteCommand.Transaction = value; } } }
As you can see, the property assigns the given transaction to the Transaction property on all its commands. This means I can do this in my Update method:
CategoriesTableAdapter catsTA = new CategoriesTableAdapter(); CategoryCodesTableAdapter codesTA = new CategoryCodesTableAdapter(); AllocationsTableAdapter allocsTA = new AllocationsTableAdapter(); CostsTableAdapter costsTA = new CostsTableAdapter(); catsTA.Connection.Open(); try { SqlTransaction txn = catsTA.Connection.BeginTransaction(); try { catsTA.Transaction = txn; codesTA.Transaction = txn; allocsTA.Transaction = txn; costsTA.Transaction = txn; // update each table, commit transaction or // rollback if there's a problem
What do you think? It certainly seems to work. There's probably more to it ... like I should probably check that there's not already a Transaction set on the _adapter commands etc. It's doing the job for me, though, so I hope it helps someone else out there.
Comments
# Steopa
9/06/2006 12:36 PM
Thanks for the advice. It came in really handy. I have a table adapter with more than one Delete query (delete a single item or a set of items). In order to add the transaction to the 2nd query, I added a bit of code to go through the additional commands:
set {
if (_adapter == null) {
InitAdapter();
}
if (_commandCollection == null) {
InitCommandCollection();
}
_adapter.InsertCommand.Transaction = value;
_adapter.UpdateCommand.Transaction = value;
_adapter.DeleteCommand.Transaction = value;
foreach (System.Data.OleDb.OleDbCommand cmd in _commandCollection)
cmd.Transaction = value;
}
# James
12/06/2006 5:42 PM
Thanks for sharing, you solved my problems too!
# Jonas Follesø
27/06/2006 7:26 PM
Neat stuff. Doing it the same way my self. Works great :)
# Mark
1/07/2006 2:10 AM
I can't figure out how to make this work. I have several tables in a dataset and each has the Transaction property, from above. I copied the code verbatum and then do my updates, but get an error saying "The transaction is either not associated with the current connection or has been completed.".
ArtistTableAdapter artistAdapter = new ArtistTableAdapter();
ArtistGenreTableAdapter genreAdapter = new ArtistGenreTableAdapter();
ArtistContactTableAdapter contactAdapter = new ArtistContactTableAdapter();
ArtistWebsiteTableAdapter websiteAdapter = new ArtistWebsiteTableAdapter();
ArtistRelationTableAdapter relationAdapter = new ArtistRelationTableAdapter();
artistAdapter.Connection.Open();
SqlTransaction trans = artistAdapter.Connection.BeginTransaction();
try
{
artistAdapter.Transaction = trans;
genreAdapter.Transaction = trans;
contactAdapter.Transaction = trans;
websiteAdapter.Transaction = trans;
relationAdapter.Transaction = trans;
// Handle deletes
if (HasChanges(DataRowState.Deleted))
{
ArtistDS deleted = GetChanges(DataRowState.Deleted) as ArtistDS;
genreAdapter.Update(deleted);
contactAdapter.Update(deleted);
websiteAdapter.Update(deleted);
relationAdapter.Update(deleted);
// No need to merge - dataSet.Merge(deleted);
}
// Handle modifications
if (HasChanges(DataRowState.Modified))
{
ArtistDS modified = GetChanges(DataRowState.Modified) as ArtistDS;
artistAdapter.Update(modified);
genreAdapter.Update(modified);
contactAdapter.Update(modified);
websiteAdapter.Update(modified);
relationAdapter.Update(modified);
Merge(modified);
}
// Handle inserts
if (HasChanges(DataRowState.Added))
{
ArtistDS added = GetChanges(DataRowState.Added) as ArtistDS;
artistAdapter.Update(added);
genreAdapter.Update(added);
contactAdapter.Update(added);
websiteAdapter.Update(added);
relationAdapter.Update(added);
Merge(added);
}
// Commit the transaction
trans.Commit();
AcceptChanges();
}
catch (Exception ex)
{
trans.Rollback();
throw (ex);
}
# Mark
1/07/2006 2:26 AM
I forgot to mention that it fails when updating one of the child tables, not artistAdapter.
# Mark
1/07/2006 2:44 AM
I fixed it by adding the following line to the very top of the Transaction property's set code.
Connection = value.Connection;
# mabster
1/07/2006 10:12 AM
Hi Mark,
Yeah, sorry about that - you're spot on. I actually had to make that change myself and forgot to update the blog post accordingly. At least you've documented it here now! :)
# T
6/01/2007 6:00 AM
Any way to scale this for when your dataset has ~50 adapters?
# mabster
6/01/2007 9:52 AM
Hey T,
I hear you. It's a bitch having to add partial classes for every adapter.
With C# 3.0 we might be able to use extension methods to add a method to *any* TableAdapter which updates within a transaction. That would be cool.
# Rob White
1/05/2007 7:09 PM
I'm a bit confused about something here, why don't you set the _adapter.SelectCommand.Transaction? You use it to get the transaction back later, but you don't set it, surely it will always be null. I know logically it doesn’t make a lot of sense to do a select as part or a transaction, but that is still where you pick it back up from.
So what’s the thinking behind it?
p.s. Really useful bit of code, thanks for posting it.
# mabster
1/05/2007 9:06 PM
Hi Rob,
Good catch. I never bother setting it I guess because selecting doesn't really need a transaction. However, since I don't set it I shouldn't be using it in the property's "getter".
So yeah - that was a bit of a brain fart. Glad you got some use out of the code though.
# Rob Whiet
1/05/2007 10:19 PM
It turns out I can't set the select even if I wanted to, the _adapter.SelectCommand is always null, even though when I look at the adapter itself it has a valid select command. I'm calling InitCommand, but that doesn't seem to help. Ah well, I guess I'll just create a private member for it.
# Pablo
10/06/2007 10:11 PM
Hi, I'm in trouble with this piece of code:
I've been trying this:
catsTA.Connection.Open();
But, I can't access to Connection property of catsTA tableAdapter (catsTa does not show Connection property).
Any idea to sort it out?.
# mabster
10/06/2007 10:56 PM
You're testing my memory now Pablo! :)
If it's not catsTA.Connection.Open() then perhaps you could use the transaction's connection (since it's the same connection).
# Pablo
11/06/2007 2:12 AM
I thing is missing "Public Connection property" like to the very top "SqlTransaction Transaction". It let´s set up Open the "internal Connection" something like this:
partial class CategoriesTableAdapter
{
public SqlTransaction Transaction
{
get { return _adapter.SelectCommand.Transaction; }
set
{
if (_adapter == null)
{
InitAdapter();
}
Connection = value.Connection;
_adapter.InsertCommand.Transaction = value;
_adapter.UpdateCommand.Transaction = value;
_adapter.DeleteCommand.Transaction = value;
}
}
public SqlConnection plcConnection
{return this.Connection;}
}
Now I can access to the Connection internal property of catsTA CategoriesTableAdapter:
catsTA.Connection.Open();
# Pablo
11/06/2007 2:16 AM
sorry, I should use the next line code:
catsTA.plcConection.Open();
# Pablo
11/06/2007 4:33 AM
Well, problem is that TableAdapters do not expose Connection property. That's the reason to set up plcConnection public Connection to access to internal property Connection in TableAdapter.
Now it works well!.
# Dennis Johansson
5/07/2007 7:58 PM
Pablo: to access the Connection inside your TableAdapter you have to do it in a Partial class of your TableAdapter. For me it´s declared like this.
Private _connection As System.Data.SqlClient.SqlConnection
# Nadeem
9/08/2007 8:20 PM
I used a different way, I think it is more flexible:
1- Change scope of Adapter property in your DataSet Adapters code from Private to Public
2- Then define some procedure like this one
Private Sub IntDA(ByRef DA As SqlDataAdapter, ByVal Trn As SqlTransaction)
DA.DeleteCommand.Connection = Trn.Connection
DA.UpdateCommand.Connection = Trn.Connection
DA.InsertCommand.Connection = Trn.Connection
DA.DeleteCommand.Transaction = Trn
DA.UpdateCommand.Transaction = Trn
DA.InsertCommand.Transaction = Trn
End Sub
3- Finaly you can use somethig like this
Dim Trn As SqlTransaction=SomeConnection.BeginTransaction
IntDA(MyTypedDataSet.MyTypedDataAdapter.Adapter,Trn)
# Chris
17/09/2007 8:23 PM
any ideas for .net 1.1? I seem unable to use transactions with a typed datset containing parent-child tables.
this.mdaHistoricPayPeriod.SelectCommand.Transaction = testTransaction;
this.mdaHistoricPayPeriod.InsertCommand.Transaction = testTransaction;
this.mdaHistoricPayPeriod.UpdateCommand.Transaction = testTransaction;
this.mdaHistoricPayPeriod.DeleteCommand.Transaction = testTransaction;
i.e. On the this.mdaHistoricPayPeriod.Update(rowstoinsert) method i just get an error every time:
"Execute requires the command to have a transaction object when the connection assigned to the command is in a pending local transaction. The Transaction property of the command has not been initialized."
Any ideas, id really appreciate any help.
# mabster
17/09/2007 8:29 PM
Sorry Chris, I came onto the .NET scene as of 2.0.
I do know that 1.1 didn't have partial classes, which no doubt makes this a bit uglier to implement. Perhaps another reader can help out.
# Fabrizio
12/10/2007 12:09 AM
Hello,
have you tried TransactioScope in Net 2.0?
You can combine TransactionScope, TableAdapter and SqlConnection.
using (TransactionScope ts = new TransactionScope())
{
using (SqlConnection sqlConn = new SqlConnection(Properties.Settings.Default.FabryConnectionString))
{
sqlConn.Open();
PersoneTableAdapter pta = new PersoneTableAdapter();
OggettiTableAdapter ota = new OggettiTableAdapter();
pta.Connection = sqlConn;
ota.Connection = sqlConn;
MyDS.PersoneDataTable persone = pta.GetAllPersone();
MyDS.OggettiDataTable oggetti = ota.GetData();
persone[0].Cognome = "XXXXXXX_" + DateTime.Now.ToString();
oggetti[0].NomeOggetto = "NomeOggetto_" + DateTime.Now.ToString();
pta.Update(persone);
ota.Update(oggetti);
sqlConn.Close();
}
ts.Complete();
}
# mabster
12/10/2007 9:08 AM
Hi Fabrizio,
I've briefly looked at the TransactionScope stuff, and it looks great. However, at the time I wrote this post we were using a mixture of SQL 2005 and SQL 2000, and TransactionScope did some funky things if you tried using it on SQL 2000.
Nowadays it wouldn't be a problem.
# Josh
4/01/2008 5:49 AM
I just wanted to thank everyone in this thread, as well as Mabster. I got a lot of great ideas that were very helpful.
# Bret
14/01/2008 2:39 PM
I tried something similiar but all my commands were null when I debugged into the Designer source. After looking into the autogenerated source it appears these command adapters are assigned at method call but stored in a local datastructure. This is what I did to create an all encompassing solution for SQL 2000:
namespace GatewayDataAccessTableAdapters {
public partial class ClientTableAdapter : Component {
public void SetTransaction(SqlTransaction pTransaction) {
foreach (SqlCommand cmd in CommandCollection) {
cmd.Transaction = pTransaction;
} // foreach
} // SetTransaction
} // ClientTableAdapter
} // ClientTableAdapters
# Darren
18/01/2008 9:50 PM
I am currently updating 3 tables from a strongly typed dataset. I am using VB.NET 2005 and Access 2003 db.
I've tried using transactionScope as follows:
Using ts As New TransactionScope
getRemoteDBProperties()
' Get the new customers
downloadNewCustomers()
' Get the new orders
downloadNewOrders()
ts.Complete()
End Using
But I'm struggling.
The three routines I call all use table adapters to select and update to the ODBC database.
Can I use TransactionScope or do I have to use BeginTransaction as described above?
Thanks
Darren
# nanestev
27/02/2008 8:03 AM
Thanks for sharing it mate. I used it for my DataSet.
# Stephen Davies
2/03/2008 8:45 AM
I attempted to utilise your solution for transactions support in an MSAccess 2003 database. Switched the SQLConnection to OleDbTransaction (seemed simple enough) but received:
ExecuteReader 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.
Solved with the following addition to your partial class:
partial class NotesTableAdapter
{
public OleDbTransaction Transaction
{
get { return _adapter.SelectCommand.Transaction; }
set
{
if (_adapter == null)
{
InitAdapter();
}
Connection = value.Connection;
_adapter.InsertCommand.Transaction = value;
_adapter.UpdateCommand.Transaction = value;
_adapter.DeleteCommand.Transaction = value;
foreach (OleDbCommand cmd in CommandCollection)
{
cmd.Transaction = value;
}
}
}
}
Works like a charm! Thanks Bret for the hint!
Chris, This may well solve your issue under 1.1
Great solution mabster, thanks.