TableAdapters transaccionales? Si por favor!

¿Usar DataSets Tipados o no? Uhm…

Hola a todos,

En el proyecto que actualmente me ocupa, al desarrollar la capa de acceso a datos he pensado usar DataSets Tipados en lugar de otro modelo habitual basado en clase – colección genérica. ¿Por qué? Bueno porque en principio simplifica el desarrollo, ya que (cómo ya sabéis) el diseñador de DataSets se ocupa de generar las operaciones básicas de lectura/escritura en la base de datos. Esto ahorra bastante tiempo de desarrollo y permite centrarnos en lo realmente importante: La lógica de la aplicación.

Además este método es de fácil extensión mediante el uso de las clases parciales (una de las características que más me gustan del FWK 2.0), de forma que si deseamos agregar más funcionalidad al adaptador, podemos hacerlo extendiendo la clase de forma muy sencilla:

 
Namespace AdventureWorksDALTableAdapters
    Partial Public Class CurrencyTableAdapter
        'Implementar aquí el código que extiende la clase
    End Class
End Namespace

¿Fácil verdad? Por un lado tenemos una herramienta que nos genera automáticamente el código de la capa de datos, y que además nos permite extenderlo… entonces ¿dónde está el truco?

Bueno, los DataSets Tipados aportan un montón de ventajas pero también tienen sus inconvenientes: Por ejemplo, nunca serán tan ligeros como una lista genérica de objetos, ya que precisamente aportan mucha funcionalidad (que no siempre es necesaria). Por otro lado el hecho de ser un código autogenerado hace que mucha gente (yo al principio) lo mire con cierto recelo, ya que al margen de los patrones habituales cada desarrollador tiene su propio método de hacer las cosas, y muchas veces uno piensa que sólo su código es el apropiado (ese defecto lo tenemos muchos :-P).

AdventureWorksDAL

Figura 1 – DataSets Tipados generados a partir de la B.D. AdventureWorks

Pero tal vez el mayor inconveniente que particularmente les encontraba a los DataSets Tipados es lo complicado que resulta utilizarlos en un entorno transaccional, más cuando esto es algo básico, que casi todo desarrollador necesita utilizar hoy en día.

El contexto transaccional ¿cómo definirlo?

¿Quién no se ha encontrado ante la necesidad de realizar varias operaciones de lectura/escritura contra un origen de datos que precisen que se validen todas como una sola? El clásico ejemplo sencillo es el de un documento que contiene n líneas, aunque podemos imaginarnos lo que sería de los entornos bancarios sin la posibilidad de realizar transacciones… Pero sigamos, no quiero apartarme de tema que nos ocupa.

Actualmente el Framework proporciona dos alternativas para el uso de entornos transaccionales, la primera consiste en el viejo modelo conocido por todos, el clásico, en el que a partir de una conexión abierta con un origen de datos se inicia una transacción, a continuación se realizan todas las operaciones implicadas, y posteriormente se termina con éxito (Commit) o con error (Rollback), de forma que todas las operaciones se guardan o se vuelve al estado inicial.

SqlTransaction

Figura 2 – La clase SqlTransaction

La segunda alternativa es nueva de la versión 2.0 del Framework y consiste en el uso del nuevo espacio de nombres “System.Transactions” que forma parte de los “Enterprise Services Team” de Microsoft. La verdad es que el concepto es muy atractivo ya que permite cosas impensables hasta ahora, como por ejemplo definir un ámbito de contexto transaccional mediante una cláusula Using – End using, en la que todas las operaciones realizadas pertenecerán implícitamente a dicho contexto transaccional.

TransactionScope

Figura 3- La clase TransactionScope

Otra característica destacable de este nuevo modelo es que permite escalar de forma automática una transacción, pasando de transacción ligera a transacción distribuida. Esto es así porque con al definir una nueva transacción, por defecto se intenta crear una transacción ligera, que significa que sólo accede a un recurso y es manejada por el “Lightweight Transaction Manager” o LTM, incluido con el espacio de nombres “System.Transactions”. En el momento que esa transacción excede las capacidades del LTM, ya sea porque accede a un segundo recurso en la misma transacción o porque utiliza múltiples dominios de aplicación, ésta es promovida a lo que se llama “transacción distribuida” y pasa a ser manejada por el servicio coordinador de transacciones distribuidas o DTC.

Este proceso se realiza de forma automática, y la transacción resultante (que es controlada por COM+) ofrece un rendimiento bastante inferior al anterior, con que lo convierte en menos atractivo a nuestros ojos salvo en algunos casos puntuales (como implementar transaccionalidad entre distintas bases de datos).

DTC

Figura 4 – El coordinador de transacciones distribuidas en acción

A día de hoy, utilizar esta nueva metodología transaccional tiene una serie de desventajas frente al modelo clásico, ya que lo interesante sería usar siempre que fuese posible transacciones ligeras, y esto no siempre es posible. De hecho es bastante complicado ya que actualmente sólo existe soporte para SQL Server 2005, así que si usamos una versión anterior u otro motor de datos como Oracle no podremos usar las transacciones ligeras. Del mismo modo, antes he comentado que si se exceden las capacidades del LTM también se promocionan automáticamente a transacciones distribuidas, y realmente es bastante sencillo que esto ocurra. Basta con utilizar dos variables de conexión (aunque realmente apunten a la misma) dentro del contexto transaccional para promoverse a transacción distribuida.

A lo que íbamos, creando un adaptador transaccional

Bien, armado con el conocimiento de lo que nos ofrece el Framework me propuse implementar transaccionalidad en mis DataSets Tipados, algo sencillo en su concepto pero complicadillo en su implementación ya que en el código que se genera automáticamente cada adaptador dispone de una colección de objetos Command, y debemos alterar su comportamiento predeterminado para lograr nuestro propósito.

¡Manos a la obra! Empezaremos por definir el modelo transaccional, usaremos transaccionalidad clásica mediante el uso del objeto SqlTransaction. Partiremos de un modelo de datos conocido por todos: la base de datos AdventureWorks, y en esta ocasión usaremos el viejo y bueno VB como lenguaje de desarrollo.

Una vez tenemos en nuestro proyecto un conjunto de datos llamado AdventureWorksDAL.xsd (similar al de la figura 1) con los DataAdapters y DataTables necesarios, vamos extender las clases generadas mediante el uso de clases parciales. Para ello es importante saber que las clases a extender se encuentran dentro de un namespace llamado igual que el archivo del conjunto de datos más el sufijo “TableAdapters”:

Imports System.Data.SqlClient
 
Namespace AdventureWorksDALTableAdapters
    Partial Public Class CurrencyTableAdapter
 
        Private _Transaction As SqlTransaction
 
        Public Property Transaction() As SqlTransaction
            Get
                Return _adapter.SelectCommand.Transaction
            End Get
            Set(ByVal value As SqlTransaction)
                If _adapter Is Nothing Then InitAdapter()
                For Each cmd As SqlCommand In Me.CommandCollection
                    cmd.Transaction = value
                Next
                If Not _adapter.InsertCommand Is Nothing Then _
                    _adapter.InsertCommand.Transaction = value
                If Not _adapter.UpdateCommand Is Nothing Then _
                    _adapter.UpdateCommand.Transaction = value
                If Not _adapter.DeleteCommand Is Nothing Then _
                    _adapter.DeleteCommand.Transaction = value
            End Set
        End Property
 
    End Class
End Namespace 

Nótese que el código crea una propiedad de tipo SqlTransaction, a la cual asignaremos posteriormente el valor de una transacción iniciada, y que dicho valor será propagado por todos los objetos SqlCommand que contiene el adaptador llamado “_adapter”, el cual es privado e inaccesible por cualquier otro método que no sea la extensión de esta clase parcial.

Una vez realizado esto por cada una de las clases de nuestro conjunto de datos, vamos a ver cómo se utilizan estas clases en un entorno real. Let’s play! En primer lugar comprobaremos si la transaccionalidad en un solo adaptador funciona y posteriormente complicaremos un poco más el ejemplo y realizaremos cambios en distintos adaptadores para ver si realmente disponemos de un completo y robusto entorno transaccional.

Imports System.Data.SqlClient
Imports TestSqlTransaction_Adapters.AdventureWorksDAL
Imports TestSqlTransaction_Adapters.AdventureWorksDALTableAdapters
 
Public Class Form1
 
    Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
        Dim Transaction As SqlTransaction = Nothing
        Try
            Dim CurrencyAdapter As New CurrencyTableAdapter()
            'Abre la conexión, inicia la trasnsacción y la asigna 
            'a la nueva propiedad 'Transaction' para su propagación
            CurrencyAdapter.Connection.Open()
            Transaction = CurrencyAdapter.Connection.BeginTransaction()
            CurrencyAdapter.Transaction = Transaction
            Using Currencies As CurrencyDataTable = CurrencyAdapter.GetData()
                Dim Currency As CurrencyRow = Currencies.FindByCurrencyCode("EUR")
                Currency.Name += "*"
                Dim Changes As CurrencyDataTable = _
                  CType(Currencies.GetChanges(), CurrencyDataTable)
                If Not Changes Is Nothing Then CurrencyAdapter.Update(Changes)
            End Using
            Dim r As DialogResult = MessageBox.Show( _
              "Save changes to EUR?", _
              "Transaction", MessageBoxButtons.YesNo)
            If r = Windows.Forms.DialogResult.Yes Then
                Transaction.Commit()
            Else
                Transaction.Rollback()
            End If
        Catch ex As Exception
            Transaction.Rollback()
            MessageBox.Show(ex.Message)
        End Try
 
    End Sub
End Class

El ejemplo anterior ha sido sencillo y hemos podido comprobar cómo configurar nuestro adaptador y observar cómo era capaz de revertir a su estado anterior o de guardar los cambios. Vamos a realizar el mismo proceso con varios adaptadores para ver un ejemplo de uso un poco más real, de los de la calle vamos…

Imports System.Data.SqlClient
Imports TestSqlTransaction_Adapters.AdventureWorksDAL
Imports TestSqlTransaction_Adapters.AdventureWorksDALTableAdapters
 
Public Class Form1
 
    Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
        Dim Transaction As SqlTransaction = Nothing
        Try
            Dim CurrencyAdapter As New CurrencyTableAdapter()
            Dim Currency As CurrencyRow
            'Abre la conexión, inicia la trasnsacción y la asigna 
            'a la nueva propiedad 'Transaction' para su propagación
            CurrencyAdapter.Connection.Open()
            Transaction = CurrencyAdapter.Connection.BeginTransaction()
            CurrencyAdapter.Transaction = Transaction
            Using Currencies As CurrencyDataTable = CurrencyAdapter.GetData()
                Currency = Currencies.FindByCurrencyCode("EUR")
                Currency.Name += "*"
                Dim Changes As CurrencyDataTable = _
                  CType(Currencies.GetChanges(), CurrencyDataTable)
                If Not Changes Is Nothing Then CurrencyAdapter.Update(Changes)
            End Using
 
            Dim CurrencyRateAdapter As New CurrencyRateTableAdapter()
            'Propaga la conexión y la transacción a este adapter
            CurrencyRateAdapter.Connection = CurrencyAdapter.Connection
            CurrencyRateAdapter.Transaction = Transaction
            Using CurrencyRates As CurrencyRateDataTable = CurrencyRateAdapter.GetData()
                'Inserta una nueva fila con una cotización a fecha de hoy
                Dim oNewRate As CurrencyRateRow = _
                  CurrencyRates.AddCurrencyRateRow(DateTime.Today, Currency, _
                    Currency, 10D, 11D, DateTime.Now)
                Dim Changes As CurrencyRateDataTable = _
                  CType(CurrencyRates.GetChanges(), CurrencyRateDataTable)
                If Not Changes Is Nothing Then CurrencyRateAdapter.Update(Changes)
            End Using
            Dim r As DialogResult = MessageBox.Show( _
              "Save changes to EUR?", _
              "Transaction", MessageBoxButtons.YesNo)
            If r = Windows.Forms.DialogResult.Yes Then
                Transaction.Commit()
            Else
                Transaction.Rollback()
            End If
        Catch ex As Exception
            Transaction.Rollback()
            MessageBox.Show(ex.Message)
        End Try
 
    End Sub
End Class

Creo que con este ejemplo queda claro que la implementación de transaccionalidad en objetos TableAdapter no es demasiado complejo, y el resultado bien merece la pena. Al fin y al cabo ¿quién no necesita transaccionalidad hoy en día?

Saludos a todos y ser buenos, o los reyes no os traerán nada…

** crossposting desde el blog de Lluís Franco en geeks.ms **

4 thoughts on “TableAdapters transaccionales? Si por favor!

  1. Hola,
    Segui tu ejemplo paso a paso, y me sale que no puedo acceder a los tableadapter generados con el diseñador, la verdad no se cual es mi error. Serias tan amable de darme un ejemplo completo.

    Muchisimas gracias.

    mi correo noeandreita@hotmail.com

  2. Hace tiempo que tengo intenciones de utilizar transaciones con tableadapter pero no habia podido sino con el MDTC muy interesante.

    Gracias

  3. Felicitaciones!!!, muy buen post, me fue de mucha utilidad. Tienes toda la razón, quién no necesita transaccionalidad en estos días

Leave a Reply

Your email address will not be published. Required fields are marked *


*