La verdad es que el mundo de los TableAdapters y los Datasets Tipados abre muchísimas puertas de cara a la productividad en el desarrollo y a la disminución de tasa de errores a la hora de trabajar contra una fuente de datos.
El problema que nos hemos encontrado muy amenudo es querer utilizarlos transaccionalmente pero sin emplear la herramienta TransactionScope de ADO.NET, y seguir utilizando una conexión y transacción controlada por nosotros mismos. No sé por qué pero en su día intenté hacerlo así y no hubo manera y tuve que morir al palo del TransactionScope. Recientemente ha vuelto a surgir la necesidad, y, esta vez con Visual Studio 2008 y el Framework 3.5, parece que la cosa ha funcionado.
No sé si es cosa del Framework 3.5 (en su día lo intenté con la versión 2.0), o que hoy me ha dado por estar líºcido, o las otras veces demasiado poco líºcido, pero hoy con una tontería de código he conseguido trabajar de forma transaccional entre dos TableAdapters que están en dos Datasets distintos, compartiendo la misma conexión y transacción.
Sin más rodeos, el código:
public void InsertMarca(string marca, int veces)
{
SqlTransaction tran = null;
SqlConnection con = null;
try
{
// abrimos una conexión
con = new SqlConnection(“Data Source=.\\SQLEXPRESS;AttachDbFilename=|DataDirectory|\\BBDD.mdf;Integrated Security=True;User Instance=True”);
con.Open();
//establecemos que la conexión de la marca es la que hemos creado
MarcaTableAdapter adapterMarca = new MarcaTableAdapter();
adapterMarca.Connection = con;
// establecemos que la conexión del coche es la que hemos creado
CocheTableAdapter adapterCoche = new CocheTableAdapter();
adapterCoche.Connection = con;
// iniciamos una transacción sobre dicha conexión
tran = con.BeginTransaction();
// le pasamos a los adaptadores la transacción que acabamos de crear
adapterMarca.Transaction = tran;
adapterCoche.Transaction = tran;
for (int i = 0; i < veces; i++)
{
adapterMarca.InsertMarca(marca);
adapterCoche.InsertCoche(“4444-cxv”, 4, 8);
if (i > 10)
{
// forzamos un error para ver qué pasa
throw new Exception(“prueba de transacción”);
}
}
tran.Commit();
}
catch (Exception ex)
{
tran.Rollback();
}
finally
{
con.Close();
}
}
Aquí la pieza que falta es la siguiente, en el diseñador de Datasets, hay que irse a las propiedades de cada uno de los TableAdapter implicados, y establecer la propiedad “ConnectionModifier” a “public”.
Existen otras soluciones a este mismo problema que podéis encontrar descritas en el siguiente artículo que explica bastante bien el problema:
https://www.codeproject.com/KB/dotnet/transactionta.aspx
En concreto el autor del artículo defiende una solución utilizando Reflection, ya que el otro tipo de solución con la que se suele trabajar para este mismo problema es con el uso de clases parciales.
El problema del uso de las clases parciales es que necesitas generar una clase parcial por cada uno de los TableAdapters, y esto a la larga puede hacer generar mucho código. La solución con Reflection tampoco es la panacea, ya que aunque te evites generar una clase parcial por TableAdapter, aparece el problema de siempre al utilizar Reflection, que nadie te garantiza que tras una actualización del framework la propiedad que has explotado cambie su nombre…
La solución que aporto tiene alguna ventaja, por ejemplo no usa Reflection así que nos evitamos problemas de compatibilidades futuras, tampoco es necesario generar una clase parcial por TableAdapter, lo íºnico que tendremos que hacer es en los casos de actuar de forma transaccional (por experiencia el volumen de accesos transaccionales a base de datos no suele llegar ni a la mitad de los accesos de modificación en base de datos) añadir algunas líneas más en el código.
Eso sí, hay una cosa que no me gusta un pelo, que es que tengamos ahí en el código la apertura y el cierre de la conexión. Que nos dejemos la apertura lo veo complicado ya que si lo probamos aunque sea una vez nos dará error. El terrible problema lo veo en el cierre de la conexión, ya que es la historia de antaño, como te dejes el finally acabas de reventar tu sistema. Dándole más vueltas, este problemas con una clase que maneje la conexión, que herede de la interfaz IDisposable y que utilicemos mediante un using podríamos solventar el problema de olvidarnos cerrar la conexión, pero vamos a seguir teniendo que tragarnos el asignar la conexión y la transacción a todos los adapters que participen.
Saludos.
Miguel.
Interesante, pero en el escenario que planteas, ¿por qué se ha de cambiar a “public” la propiedad “ConnectionModifier”?
Por defecto es “internal”, suficiente para el ejemplo y también para escenarios de assemblies-DAL que encapsulen el acceso a datos (normalmente los TableAdapters no se exponen hacia fuera del assembly).
¿No?
PD: He llegado a tu blog a través de bíºsquedas de información sobre Agile Point.
Es muy buen artículo y lo estoy probando, pero tengo una duda, en la parte en la parte donde asignas la transaccion:
// le pasamos a los adaptadores la transacción que acabamos de crear
adapterMarca.Transaction = tran;
adapterCoche.Transaction = tran;
los tableadapters no tienen la propiedad Transaction q mencionas. No se podría hacer mejor
adapterMarca.Connection.BeginTransaction()??
Jarid, así lo hago yo.
Extracto del código de mi aplicación:
Try
If (TAQuery.Connection.State = ConnectionState.Closed) Then
TAQuery.Connection.Open()
End If
TATechInfo.Connection = TAQuery.Connection
TAUnitConversor.Connection = TAQuery.Connection
TAQuery.Transaction = TAQuery.Connection.BeginTransaction()
TATechInfo.Transaction = TAQuery.Transaction
TAUnitConversor.Transaction = TAQuery.Transaction
‘Hago lo que sea
TAQuery.Transaction.Commit()
Catch ex As Exception
TAQuery.Transaction.Rollback()
MessageBox.Show(ex.Message, “Error”, MessageBoxButtons.OK, MessageBoxIcon.Error)
End Try
Aunque sería mejor usar el patrón de diseño Factoría o Envelope, y de esta forma crearse una clase con la que inicializar y asignar las transacciones, de los DataAdapters implicados.
Pienso en algo que deje el código de arriba en esto:
Try
mitransaccion = MiFactoria.BeginTrans(TA1, TA2, …)
‘ Hago lo que sea
mitransaccion.Commit()
Catch ex As Exception
mitransaccion.Rollback()
MessageBox.Show(ex.Message, “Error”, MessageBoxButtons.OK, MessageBoxIcon.Error)
End Try
Esto es algo que no tengo implementado, pero me pondré a ello porque me hará la vida más cómoda y con menos código, que con esto de .NET parece que vamos para atrás. En vez de facilitarnos la vida, las nuevas librerías nos hacen escribir más…
¡¡¡ Con lo fácil que era todo esto en ADO o con los TDataSet de Delphi …!!!
Y a todo esto …
¿ Me sabéis responder porqué los DataAdapters, por defecto, tienen una conexión diferente ? ¿ No sería más cómodo por defecto, compartir la misma conexión, y el que lo desee o así quiera para un caso concreto, establecer una conexión distinta para ese DataAdapter ?
A veces me da la impresión que todas estas cosas nos hacen trabajar más de lo que debiéramos. Vamos si en 1000 bombillas hay 1 apagada y 999 encendidas , ¿ qué es más facil ?
a) Memorizar la que está apagada.
b) Memorizar las 999 que están encendidas.
Pues con todo esto del .NET me parece en ocasiones se nos obliga a memorizar las 999 que están encendidas, en vez de la que estaba apagada como se hacía y se hace en otros lenguajes.