Ajustar las modificaciones de base de datos dentro de una transacción (C#)

por Scott Mitchell

Descargar PDF

Este tutorial es el primero de cuatro en el que se examina la actualización, eliminación e inserción de lotes de datos. En este tutorial, verá cómo las transacciones de base de datos permiten realizar modificaciones por lotes como una operación atómica, lo que garantiza que todos los pasos se realicen correctamente o que se produzcan errores en todos los pasos.

Introducción

Como ha visto a partir del tutorial Introducción a la inserción, actualización y eliminación de datos, el control GridView proporciona compatibilidad integrada para la edición y eliminación de nivel de fila. Con unos pocos clics del mouse, es posible crear una interfaz de modificación de datos enriquecida sin escribir una línea de código, siempre que se conforme con operaciones de edición y eliminación en cada fila. Pero en determinados escenarios esto no es suficiente y es necesario proporcionar a los usuarios la capacidad de editar o eliminar un lote de registros.

Por ejemplo, la mayoría de los clientes de correo electrónico basados en web usan una cuadrícula para enumerar cada mensaje, donde cada fila incluye una casilla junto con la información del correo electrónico (asunto, remitente, etc.). Esta interfaz permite al usuario eliminar varios mensajes si los selecciona y hace clic en el botón Eliminar mensajes seleccionados. Una interfaz de edición por lotes es ideal en situaciones en las que los usuarios suelen editar muchos registros diferentes. En lugar de forzar al usuario a hacer clic en Editar, realizar el cambio y, después, hacer clic en Actualizar para cada registro que se debe modificar, una interfaz de edición por lotes representa cada fila con su interfaz de edición. El usuario puede modificar rápidamente el conjunto de filas que deben cambiarse y, después, guardar estos cambios al hacer clic en un botón Actualizar todo. En este conjunto de tutoriales, se examinará cómo crear interfaces para insertar, editar y eliminar lotes de datos.

Al realizar operaciones por lotes, es importante determinar si es posible que algunas de las operaciones del lote se realicen correctamente, mientras que otras no. Considere una interfaz de eliminación por lotes: ¿qué debe ocurrir si el primer registro seleccionado se elimina correctamente, pero se produce un error en el segundo, por ejemplo, debido a una infracción de la restricción de clave externa? ¿Se debe revertir la eliminación del primer registro o es aceptable que el primer registro permanezca eliminado?

Si quiere que la operación por lotes se trate como una operación atómica en la que todos los pasos se realicen correctamente o se produzca un error en todos los pasos, es necesario aumentar la capa de acceso a datos para incluir compatibilidad con las transacciones de base de datos. Las transacciones de base de datos garantizan la atomicidad del conjunto de instrucciones INSERT, UPDATE y DELETE ejecutadas bajo la transacción y son una característica compatible con la mayoría de los sistemas de base de datos modernos.

En este tutorial verá cómo ampliar la DAL para usar transacciones de base de datos. En tutoriales posteriores se examinará la implementación de páginas web para interfaces de inserción, actualización y eliminación por lotes. Comencemos.

Nota:

Al modificar datos en una transacción por lotes, la atomicidad no siempre es necesaria. En algunos escenarios, puede ser aceptable que algunas modificaciones de datos se realicen correctamente y otras en el mismo lote produzcan un error, como al eliminar un conjunto de correos electrónicos de un cliente de correo electrónico basado en web. Si se produce un error de base de datos a mitad del proceso de eliminación, es probable que los registros procesados sin error permanezcan eliminados. En tales casos, no es necesario modificar la DAL para admitir transacciones de base de datos. Pero hay otros escenarios de operaciones por lotes en los que la atomicidad es vital. Cuando un cliente transfiere sus fondos de una cuenta bancaria a otra, se deben realizar dos operaciones: los fondos deben deducirse de la primera cuenta y, después, agregarse a la segunda. Aunque es posible que al banco no le importe que el primer paso se realice correctamente pero que se produzca un error en el, sus clientes tendrían motivos para estar molestos. Le recomendamos que trabaje en este tutorial e implemente las mejoras en la DAL para admitir transacciones de base de datos incluso si no planea usarlas en las interfaces de inserción, actualización y eliminación por lotes que se crearán en los tres tutoriales siguientes.

Información general sobre las transacciones

La mayoría de las bases de datos incluyen compatibilidad con las transacciones, que permiten agrupar varios comandos de base de datos en una sola unidad de trabajo lógica. Se garantiza que los comandos de base de datos que componen una transacción sean atómicos, lo que significa que todos los comandos producirán un error o que todos se realizarán correctamente.

En general, las transacciones se implementan mediante instrucciones SQL con el siguiente patrón:

  1. Se indica el inicio de una transacción.
  2. Se ejecutan las instrucciones SQL que componen la transacción.
  3. Si hay un error en cualquiera de las instrucciones del paso 2, se realiza la reversión de la transacción.
  4. Si todas las instrucciones del paso 2 se completan sin errores, se confirma la transacción.

Las instrucciones SQL que se usan para crear, confirmar y revertir la transacción se pueden escribir manualmente al escribir scripts SQL o crear procedimientos almacenados, o bien mediante programación con ADO.NET o las clases del espacio de nombres System.Transactions. En este tutorial solo se examinará la administración de transacciones mediante ADO.NET. En un tutorial posterior verá cómo usar procedimientos almacenados en la capa de acceso a datos y se explorarán las instrucciones SQL para crear, revertir y confirmar transacciones.

Nota:

La clase TransactionScope del espacio de nombres System.Transactions permite a los desarrolladores encapsular mediante programación una serie de instrucciones dentro del ámbito de una transacción e incluye compatibilidad con transacciones complejas que implican varios orígenes, como dos bases de datos diferentes o incluso tipos heterogéneos de almacenes de datos, como una base de datos de Microsoft SQL Server, una base de datos Oracle y un servicio web. En este tutorial se ha optado por usar transacciones de ADO.NET en lugar de la clase TransactionScope porque ADO.NET es más específico para las transacciones de base de datos y, en muchos casos, consume mucho menos recursos. Además, en determinados escenarios, la clase TransactionScope usa el Coordinador de transacciones distribuidas de Microsoft (MSDTC). Los problemas de configuración, implementación y rendimiento relacionados con MSDTC lo convierten en un tema bastante especializado y avanzado, que supera el ámbito de estos tutoriales.

Al trabajar con el proveedor SqlClient en ADO.NET, las transacciones se inician mediante una llamada al método BeginTransaction de la clase SqlConnection, que devuelve un objeto SqlTransaction. Las instrucciones de modificación de datos que constituyen la transacción se colocan dentro de un bloque try...catch. Si se produce un error en una instrucción del bloque try, la ejecución se transfiere al bloque catch donde se puede revertir la transacción por medio del método Rollback del objeto SqlTransaction. Si todas las instrucciones se completan correctamente, una llamada al método Commit del objeto SqlTransaction al final del bloque try confirma la transacción. En el siguiente fragmento de código se muestra este patrón. Vea Mantenimiento de la coherencia de la base de datos con transacciones.

// Create the SqlTransaction object
SqlTransaction myTransaction = SqlConnectionObject.BeginTransaction();
try
{
    /*
     * ... Perform the database transaction�s data modification statements...
     */
    // If we reach here, no errors, so commit the transaction
    myTransaction.Commit();
}
catch
{
    // If we reach here, there was an error, so rollback the transaction
    myTransaction.Rollback();
    throw;
}

De manera predeterminada, los elementos TableAdapter de un conjunto de datos con tipo no usan transacciones. Para proporcionar compatibilidad con las transacciones, es necesario aumentar las clases TableAdapter para incluir métodos adicionales que usan el patrón anterior para realizar una serie de instrucciones de modificación de datos dentro del ámbito de una transacción. En el paso 2 verá cómo usar clases parciales para agregar estos métodos.

Paso 1: Creación de las páginas web Trabajo con datos por lotes

Antes de empezar a explorar cómo aumentar la DAL para admitir transacciones de base de datos, primero debe dedicar un momento a crear las páginas web ASP.NET que necesitará para este tutorial y los tres siguientes. Para empezar, agregue una nueva carpeta denominada BatchData y, después, agregue las siguientes páginas ASP.NET, cada una de ellas asociada con la página maestra Site.master.

  • Default.aspx
  • Transactions.aspx
  • BatchUpdate.aspx
  • BatchDelete.aspx
  • BatchInsert.aspx

Add the ASP.NET Pages for the SqlDataSource-Related Tutorials

Figura 1: Adición de las páginas ASP.NET para los tutoriales relacionados con el control SqlDataSource

Como sucede con las demás carpetas, Default.aspx usará el control de usuario SectionLevelTutorialListing.ascx para enumerar los tutoriales dentro de su sección. Por tanto, para agregar este control de usuario a Default.aspx arrástrelo desde el Explorador de soluciones a la vista Diseño de la página.

Add the SectionLevelTutorialListing.ascx User Control to Default.aspx

Figura 2: Adición del control de usuario SectionLevelTutorialListing.ascx a Default.aspx (Haga clic para ver la imagen a tamaño completo)

Por último, agregue estas cuatro páginas como entradas al archivo Web.sitemap. En concreto, agregue el marcado siguiente después del elemento <siteMapNode> Personalización del mapa del sitio:

<siteMapNode title="Working with Batched Data" 
    url="~/BatchData/Default.aspx" 
    description="Learn how to perform batch operations as opposed to 
                 per-row operations.">
    
    <siteMapNode title="Adding Support for Transactions" 
        url="~/BatchData/Transactions.aspx" 
        description="See how to extend the Data Access Layer to support 
                     database transactions." />
    <siteMapNode title="Batch Updating" 
        url="~/BatchData/BatchUpdate.aspx" 
        description="Build a batch updating interface, where each row in a 
                      GridView is editable." />
    <siteMapNode title="Batch Deleting" 
        url="~/BatchData/BatchDelete.aspx" 
        description="Explore how to create an interface for batch deleting 
                     by adding a CheckBox to each GridView row." />
    <siteMapNode title="Batch Inserting" 
        url="~/BatchData/BatchInsert.aspx" 
        description="Examine the steps needed to create a batch inserting 
                     interface, where multiple records can be created at the 
                     click of a button." />
</siteMapNode>

Después de actualizar Web.sitemap, dedique un momento a ver el sitio web de los tutoriales desde un explorador. El menú de la izquierda ahora incluye elementos para trabajar con tutoriales de datos por lotes.

The Site Map Now Includes Entries for the Working with Batched Data Tutorials

Figura 3: El mapa del sitio ahora incluye entradas para los tutoriales Trabajo con datos por lotes

Paso 2: Actualización de la capa de acceso a datos para admitir transacciones de base de datos

Como se ha descrito en el primer tutorial, Creación de una capa de acceso a datos, el conjunto de datos con tipo de la DAL se compone de elementos DataTable y TableAdapter. Las instancias de DataTable contienen datos, mientras que las de TableAdapter proporcionan la funcionalidad de leer datos de la base de datos en las instancias de DataTable, actualizar la base de datos con cambios realizados en las instancias de DataTable, etc. Recuerde que las instancias de TableAdapter proporcionan dos patrones para actualizar datos, denominados actualización por lotes y DB-Direct. Con el patrón de actualización por lotes, se pasa a TableAdapter una instancia de DataSet, de DataTable o una colección de elementos DataRow. Estos datos se enumeran y para cada fila insertada, modificada o eliminada, se ejecuta InsertCommand, UpdateCommand o DeleteCommand. Con el patrón DB-Direct, a TableAdapter se le pasan los valores de las columnas necesarios para insertar, actualizar o eliminar un único registro. Después, el método de patrón DB-Direct usa esos valores pasados para ejecutar la instrucción InsertCommand, UpdateCommand oDeleteCommand adecuada.

Independientemente del patrón de actualización que se use, los métodos generados automáticamente por las instancias de TableAdapter no usan transacciones. De manera predeterminada, cada inserción, actualización o eliminación realizada por TableAdapter se trata como una sola operación discreta. Por ejemplo, imagine que un código de BLL usa el patrón DB-Direct para insertar 10 registros en la base de datos. Este código llamaría diez veces al método Insert de TableAdapter. Si las cinco primeras inserciones se realizan correctamente, pero la sexta da lugar a una excepción, los cinco primeros registros insertados permanecerán en la base de datos. De forma similar, si se usa el patrón de actualización por lotes para realizar inserciones, actualizaciones y eliminaciones en las filas insertadas, modificadas y eliminadas de una instancia de DataTable, si las primeras modificaciones se realizan correctamente, pero en una posterior se produce un error, esas modificaciones anteriores que se hayan completado permanecerán en la base de datos.

En determinados escenarios querrá garantizar la atomicidad en una serie de modificaciones. Para ello, debe extender manualmente TableAdapter y agregar nuevos métodos que ejecuten InsertCommand, UpdateCommand y DeleteCommand bajo una transacción. En Creación de una capa de acceso a datos se ha descrito el uso de clases parciales para ampliar la funcionalidad de DataTable dentro del conjunto de datos con tipo. Esta técnica también se puede usar con instancias de TableAdapter.

El conjunto de datos con tipo Northwind.xsd se encuentra en la subcarpeta DAL de la carpeta App_Code. Cree una subcarpeta en la carpeta DAL con el nombre TransactionSupport y agregue un nuevo archivo de clase con el nombre ProductsTableAdapter.TransactionSupport.cs (vea la figura 4). Este archivo contendrá la implementación parcial de ProductsTableAdapter que incluye métodos para realizar modificaciones de datos mediante una transacción.

Add a Folder Named TransactionSupport and a Class File Named ProductsTableAdapter.TransactionSupport.cs

Figura 4: Adición de una nueva carpeta denominada TransactionSupport y un archivo de clase denominado ProductsTableAdapter.TransactionSupport.cs

Escriba el siguiente código en el archivo ProductsTableAdapter.TransactionSupport.cs:

using System;
using System.Data;
using System.Data.SqlClient;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
namespace NorthwindTableAdapters
{
    public partial class ProductsTableAdapter
    {
        private SqlTransaction _transaction;
        private SqlTransaction Transaction
        {
            get
            {                
                return this._transaction;
            }
            set
            {
                this._transaction = value;
            }
        }
        public void BeginTransaction()
        {
            // Open the connection, if needed
            if (this.Connection.State != ConnectionState.Open)
                this.Connection.Open();
            // Create the transaction and assign it to the Transaction property
            this.Transaction = this.Connection.BeginTransaction();
            // Attach the transaction to the Adapters
            foreach (SqlCommand command in this.CommandCollection)
            {
                command.Transaction = this.Transaction;
            }
            this.Adapter.InsertCommand.Transaction = this.Transaction;
            this.Adapter.UpdateCommand.Transaction = this.Transaction;
            this.Adapter.DeleteCommand.Transaction = this.Transaction;
        }
        public void CommitTransaction()
        {
            // Commit the transaction
            this.Transaction.Commit();
            // Close the connection
            this.Connection.Close();
        }
        public void RollbackTransaction()
        {
            // Rollback the transaction
            this.Transaction.Rollback();
            // Close the connection
            this.Connection.Close();
        }
   }
}

La palabra clave partial de la declaración de clase indica al compilador que los miembros agregados se van a agregar a la clase ProductsTableAdapter del espacio de nombres NorthwindTableAdapters. Observe la instrucción using System.Data.SqlClient en la parte superior del archivo. Como TableAdapter se ha configurado para usar el proveedor SqlClient, usa internamente un objeto SqlDataAdapter para emitir sus comandos a la base de datos. Por tanto, es necesario usar la clase SqlTransaction para iniciar la transacción y, después, confirmarla o revertirla. Si usa un almacén de datos distinto de Microsoft SQL Server, tendrá que usar el proveedor adecuado.

Estos métodos proporcionan los bloques de creación necesarios para iniciar, revertir y confirmar una transacción. Se marcan como public, lo que les permite usarse desde dentro de ProductsTableAdapter, desde otra clase de DAL o desde otra capa de la arquitectura, como BLL. BeginTransaction abre el elemento SqlConnection interno de TableAdapter (si es necesario), inicia la transacción y la asigna a la propiedad Transaction, y adjunta la transacción a los objetos SqlCommand de la instancia interna de SqlDataAdapter. CommitTransaction y RollbackTransaction llaman a los métodos Commit y Rollback del objeto Transaction, respectivamente, antes de cerrar el objeto Connection interno.

Paso 3: Adición de métodos para actualizar y eliminar datos bajo una transacción

Con estos métodos completados, ya puede agregar métodos a ProductsDataTable o BLL que realicen una serie de comandos bajo una transacción. El método siguiente usa el patrón de actualización por lotes para actualizar una instancia de ProductsDataTable mediante una transacción. Para iniciar una transacción llama al método BeginTransaction y, después, usa un bloque try...catch para emitir las instrucciones de modificación de datos. Si la llamada al método Update del objeto Adapter da como resultado una excepción, la ejecución se transferirá al bloque catch donde se revertirá la transacción y se volverá a iniciar la excepción. Recuerde que el método Update implementa el patrón de actualización por lotes mediante la enumeración de las filas del valor ProductsDataTable proporcionado y la realización de las operaciones InsertCommand, UpdateCommand y DeleteCommand necesarias. Si se produce un error en alguno de estos comandos, la transacción se revierte y se deshacen las modificaciones anteriores realizadas durante la vigencia de la transacción. Si la instrucción Update se completa sin errores, la transacción se confirma en su totalidad.

public int UpdateWithTransaction(Northwind.ProductsDataTable dataTable)
{
    this.BeginTransaction();
    try
    {
        // Perform the update on the DataTable
        int returnValue = this.Adapter.Update(dataTable);
        // If we reach here, no errors, so commit the transaction
        this.CommitTransaction();
        return returnValue;
    }
    catch
    {
        // If we reach here, there was an error, so rollback the transaction
        this.RollbackTransaction();
        throw;
    }
}

Agregue el método UpdateWithTransaction a la clase ProductsTableAdapter mediante la clase parcial de ProductsTableAdapter.TransactionSupport.cs. Como alternativa, este método se podría agregar a la clase ProductsBLL de la capa lógica de negocios con algunos cambios sintácticos menores. Básicamente, la palabra clave this de this.BeginTransaction(), this.CommitTransaction() y this.RollbackTransaction() se tendría que reemplazar por Adapter (recuerde que Adapter es el nombre de una propiedad de ProductsBLL de tipo ProductsTableAdapter).

El método UpdateWithTransaction usa el patrón de actualización por lotes, pero también se pueden usar una serie de llamadas DB-Direct dentro del ámbito de una transacción, como se muestra en el método siguiente. El método DeleteProductsWithTransaction acepta como entrada un valor List<T> de tipo int, que son las instancias de ProductID que se van a eliminar. El método inicia la transacción mediante una llamada a BeginTransaction y, después, en el bloque try, itera por la lista proporcionada y llama al método Delete del patrón DB-Direct para cada valor ProductID. Si se produce un error en alguna de las llamadas a Delete, el control se transfiere al bloque catch, donde se revierte la transacción y se vuelve a iniciar la excepción. Si todas las llamadas a Delete se realizan correctamente, se confirma la transacción. Agregue este método a la clase ProductsBLL.

public void DeleteProductsWithTransaction
    (System.Collections.Generic.List<int> productIDs)
{
    // Start the transaction
    Adapter.BeginTransaction();
    try
    {
        // Delete each product specified in the list
        foreach (int productID in productIDs)
        {
            Adapter.Delete(productID);
        }
        // Commit the transaction
        Adapter.CommitTransaction();
    }
    catch
    {
        // There was an error - rollback the transaction
        Adapter.RollbackTransaction();
        throw;
    }
}

Aplicación de transacciones entre varias instancias de TableAdapter

El código relacionado con la transacción que se ha examinado en este tutorial permite que varias instrucciones en el objeto ProductsTableAdapter se traten como una operación atómica. Pero, ¿qué ocurre si es necesario realizar varias modificaciones en tablas de base de datos diferentes de forma atómica? Por ejemplo, al eliminar una categoría, es posible que primero quiera reasignar sus productos actuales a otra categoría. Estos dos pasos de reasignar los productos y eliminar la categoría se deben ejecutar como una operación atómica. Pero ProductsTableAdapter solo incluye métodos para modificar la tabla Products y CategoriesTableAdapter solo incluye métodos para modificar la tabla Categories. Por tanto, ¿cómo puede una transacción abarcar las dos instancias de TableAdapter?

Una opción consiste en agregar un método a CategoriesTableAdapter con el nombre DeleteCategoryAndReassignProducts(categoryIDtoDelete, reassignToCategoryID) y hacer que ese método llame a un procedimiento almacenado que reasigne los productos y elimine la categoría dentro del ámbito de una transacción definida dentro del procedimiento almacenado. En un tutorial posterior verá cómo iniciar, confirmar y revertir transacciones en procedimientos almacenados.

Otra opción consiste en crear una clase auxiliar en la DAL que contenga el método DeleteCategoryAndReassignProducts(categoryIDtoDelete, reassignToCategoryID). Este método crearía una instancia de CategoriesTableAdapter y ProductsTableAdapter, y después establecería las propiedades Connection de las dos instancias de TableAdapter en la misma instancia de SqlConnection. En ese momento, cualquiera de las dos instancias de TableAdapter iniciaría la transacción con una llamada a BeginTransaction. Los métodos de TableAdapter para reasignar los productos y eliminar la categoría se invocarían en un bloque try...catch con la transacción confirmada o revertida según sea necesario.

Paso 4: Adición del método UpdateWithTransaction a la capa de lógica de negocios

En el paso 3 se ha agregado un método UpdateWithTransaction a ProductsTableAdapter en la DAL. Debería agregar un método correspondiente a la BLL. Aunque la capa de presentación podría llamar directamente a la DAL para invocar el método UpdateWithTransaction, en estos tutoriales se ha optado por definir una arquitectura superpuesta que aísle la DAL de la capa de presentación. Por tanto, es obligatorio continuar con este enfoque.

Abra el archivo de clase ProductsBLL y agregue un método con el nombre UpdateWithTransaction que simplemente llame al método de la DAL correspondiente. Ahora debería haber dos métodos nuevos en ProductsBLL: UpdateWithTransaction, que acaba de agregar y DeleteProductsWithTransaction, que se ha agregado en el paso 3.

public int UpdateWithTransaction(Northwind.ProductsDataTable products)
{
    return Adapter.UpdateWithTransaction(products);
}
public void DeleteProductsWithTransaction
    (System.Collections.Generic.List<int> productIDs)
{
    // Start the transaction
    Adapter.BeginTransaction();
    try
    {
        // Delete each product specified in the list
        foreach (int productID in productIDs)
            Adapter.Delete(productID);
        // Commit the transaction
        Adapter.CommitTransaction();
    }
    catch
    {
        // There was an error - rollback the transaction
        Adapter.RollbackTransaction();
        throw;
    }
}

Nota:

Estos métodos no incluyen el atributo DataObjectMethodAttribute asignado a la mayoría de los otros métodos de la clase ProductsBLL porque se invocarán directamente desde las clases de código subyacente de las páginas ASP.NET. Recuerde que DataObjectMethodAttribute se usa para marcar qué métodos deben aparecer en el Asistente para configurar orígenes de datos ObjectDataSource y en qué pestaña (SELECT, UPDATE, INSERT o DELETE). Como el control GridView carece de compatibilidad integrada para la edición o eliminación por lotes, tendrá que invocar estos métodos mediante programación en lugar de usar el enfoque declarativo sin código.

Paso 5: Actualización atómica de datos de la base de datos desde la capa de presentación

Para ilustrar el efecto de la transacción al actualizar un lote de registros, se creará una interfaz de usuario que muestre todos los productos en un control GridView e incluya un control web Button que, al hacer clic en él, reasigne los valores CategoryID de los productos. En concreto, la reasignación de categorías avanzará para que a los primeros productos se les asigne un valor CategoryID válido, mientras que al resto se les asigne un valorCategoryID no existente. Si intenta actualizar la base de datos con un producto cuyo valor CategoryID no coincide con un valor CategoryID de una categoría existente, se producirá una infracción de restricción de clave externa y se iniciará una excepción. En este ejemplo verá que, cuando se usa una transacción, la excepción generada a partir de la infracción de restricción de clave externa hará que se reviertan los cambios CategoryID válidos anteriores. Pero cuando no se usa una transacción, se conservarán las modificaciones de las categorías iniciales.

Para empezar, abra la página Transactions.aspx en la carpeta BatchData y arrastre un control GridView desde el Cuadro de herramientas al Diseñador. Establezca ID en Products y, desde su etiqueta inteligente, vincule a un nuevo objeto ObjectDataSource denominado ProductsDataSource. Configure ObjectDataSource para extraer sus datos del método GetProducts de la clase ProductsBLL. Será un control GridView de solo lectura, por lo que debe establecer las listas desplegables de las pestañas UPDATE, INSERT y DELETE en (None) y hacer clic en Finalizar.

Figure 5: Configure the ObjectDataSource to Use the ProductsBLL Class s GetProducts Method

Figura 5: Configuración de ObjectDataSource para usar el método GetProducts de la clase ProductsBLL (Haga clic para ver la imagen a tamaño completo)

Set the Drop-Down Lists in the UPDATE, INSERT, and DELETE Tabs to (None)

Figura 6: Establecimiento de las listas desplegables de las pestañas UPDATE, INSERT y DELETE en (None) (Haga clic para ver la imagen a tamaño completo)

Después de completar el Asistente para configurar orígenes de datos, Visual Studio creará instancias de BoundField y CheckBoxField para los campos de datos del producto. Quite todos estos campos excepto ProductID, ProductName, CategoryID y CategoryName, y cambie el nombre de las propiedades HeaderText de las instancias ProductName y CategoryName de BoundField a Product y Category, respectivamente. Desde la etiqueta inteligente, active la opción Habilitar paginación. Después de realizar estas modificaciones, el marcado declarativo de GridView y ObjectDataSource debe ser similar al siguiente:

<asp:GridView ID="Products" runat="server" AllowPaging="True" 
    AutoGenerateColumns="False" DataKeyNames="ProductID" 
    DataSourceID="ProductsDataSource">
    <Columns>
        <asp:BoundField DataField="ProductID" HeaderText="ProductID" 
            InsertVisible="False" ReadOnly="True" 
            SortExpression="ProductID" />
        <asp:BoundField DataField="ProductName" HeaderText="Product" 
            SortExpression="ProductName" />
        <asp:BoundField DataField="CategoryID" HeaderText="CategoryID" 
            SortExpression="CategoryID" />
        <asp:BoundField DataField="CategoryName" HeaderText="Category" 
            SortExpression="CategoryName" />
    </Columns>
</asp:GridView>
<asp:ObjectDataSource ID="ProductsDataSource" runat="server" 
    OldValuesParameterFormatString="original_{0}"
    SelectMethod="GetProducts" TypeName="ProductsBLL">
</asp:ObjectDataSource>

A continuación, agregue tres controles web Button encima de GridView. Establezca la primera propiedad Texto d Button en Actualizar cuadrícula, la segunda en Modificar categorías (WITH TRANSACTION) y la tercera en Modificar categorías (WITHOUT TRANSACTION).

<p>
    <asp:Button ID="RefreshGrid" runat="server" Text="Refresh Grid" />
</p>
<p>
    <asp:Button ID="ModifyCategoriesWithTransaction" runat="server"
        Text="Modify Categories (WITH TRANSACTION)" />
</p>
<p>
    <asp:Button ID="ModifyCategoriesWithoutTransaction" runat="server"
        Text="Modify Categories (WITHOUT TRANSACTION)" />
</p>

En este punto, la vista Diseño de Visual Studio debe ser similar a la captura de pantalla que se muestra en la figura 7.

The Page Contains a GridView and Three Button Web Controls

Figura 7: La página contiene un control GridView y tres controles web Button (Haga clic para ver la imagen a tamaño completo)

Cree controladores de eventos para cada uno de los eventos Click de los tres controles Button y use el código siguiente:

protected void RefreshGrid_Click(object sender, EventArgs e)
{
    Products.DataBind();
}
protected void ModifyCategoriesWithTransaction_Click(object sender, EventArgs e)
{
    // Get the set of products
    ProductsBLL productsAPI = new ProductsBLL();
    Northwind.ProductsDataTable products = productsAPI.GetProducts();
    // Update each product's CategoryID
    foreach (Northwind.ProductsRow product in products)
    {
        product.CategoryID = product.ProductID;
    }
    // Update the data using a transaction
    productsAPI.UpdateWithTransaction(products);
    // Refresh the Grid
    Products.DataBind();
}
protected void ModifyCategoriesWithoutTransaction_Click(object sender, EventArgs e)
{
    // Get the set of products
    ProductsBLL productsAPI = new ProductsBLL();
    Northwind.ProductsDataTable products = productsAPI.GetProducts();
    // Update each product's CategoryID
    foreach (Northwind.ProductsRow product in products)
    {
        product.CategoryID = product.ProductID;
    }
    // Update the data WITHOUT using a transaction
    NorthwindTableAdapters.ProductsTableAdapter productsAdapter = 
        new NorthwindTableAdapters.ProductsTableAdapter();
    productsAdapter.Update(products);
    // Refresh the Grid
    Products.DataBind();
}

El controlador de eventos Click del botón de actualización simplemente vuelve a enlazar los datos al control GridView mediante la llamada al método DataBind de la instancia Products de GridView.

El segundo controlador de eventos reasigna el valor CategoryID de los productos y usa el nuevo método de transacción del BLL para realizar las actualizaciones de la base de datos bajo una transacción. Tenga en cuenta que el valor CategoryID de cada producto se establece arbitrariamente en el mismo valor de ProductID. Esto funcionará bien para los primeros productos, ya que tienen valores ProductID que se asignan a valores CategoryID válidos. Pero una vez que ProductID comienza a ser demasiado grande, esta superposición coincidente de ProductID y CategoryID ya no se aplica.

El tercer controlador de eventos Click actualiza el valor CategoryID de los productos de la misma manera, pero envía la actualización a la base de datos mediante el métodoUpdate predeterminado de ProductsTableAdapter. Este método Update no encapsula la serie de comandos dentro de una transacción, por lo que esos cambios se realizan antes que se conserve el primer error de infracción de restricción de clave externa encontrado.

Para demostrar este comportamiento, visite esta página en un explorador. Inicialmente debería ver la primera página de datos como se muestra en la figura 8. A continuación, haga clic en el botón Modificar categorías (WITH TRANSACTION). Esto provocará un postback e intentará actualizar todos los valores CategoryID de los productos, pero provocará una infracción de restricción de clave externa (vea la figura 9).

The Products are Displayed in a Pageable GridView

Figura 8: Los productos se muestran en un control GridView con paginación (Haga clic para ver la imagen a tamaño completo)

Reassigning the Categories Results in a Foreign Key Constraint Violation

Figura 9: La reasignación de las categorías provoca una infracción de restricción de clave externa (Haga clic para ver la imagen a tamaño completo)

Ahora presione el botón Atrás del explorador y, después, haga clic en el botón Actualizar cuadrícula. Al actualizar los datos, debería ver exactamente la misma salida que se muestra en la figura 8. Es decir, aunque algunos de los valores CategoryID de los productos se han cambiado por valores válidos y se han actualizado en la base de datos, se han revertido al producirse la infracción de restricción de clave externa.

Ahora intente hacer clic en el botón Modificar categorías (WITHOUT TRANSACTION). Esto provocará el mismo error de infracción de restricción de clave externa (vea la figura 9), pero esta vez, los productos cuyos valores CategoryID se han cambiado a un valor válido no se revertirán. Presione el botón Atrás del explorador y, después, el botón Actualizar cuadrícula. Como se muestra en la figura 10, se han reasignado los valores CategoryID de los ocho primeros productos. Por ejemplo, en la figura 8, Chang tenía un valor CategoryID de 1, pero en la figura 10 se ha reasignado a 2.

Some Products CategoryID Values were Updated While Others Were Not

Figura 10: Los valores CategoryID de algunos productos se han actualizado pero otros no (Haga clic para ver la imagen a tamaño completo)

Resumen

De manera predeterminada, los métodos de TableAdapter no encapsulan las instrucciones de base de datos ejecutadas dentro del ámbito de una transacción, pero con un poco de trabajo se pueden agregar métodos que crearán, confirmarán y revertirán una transacción. En este tutorial ha creado tres métodos de este tipo en la clase ProductsTableAdapter: BeginTransaction, CommitTransaction y RollbackTransaction. Ha visto cómo usar estos métodos junto con un bloque try...catch para convertir en atómicas una serie de instrucciones de modificación de datos. En concreto, ha creado el método UpdateWithTransaction en ProductsTableAdapter, que usa el patrón de actualización por lotes para realizar las modificaciones necesarias en las filas de un valor ProductsDataTable proporcionado. También has agregado el método DeleteProductsWithTransaction a la clase ProductsBLL de BLL, que acepta un List de valores ProductID como entrada y llama al método Delete de patrón DB-Direct para cada instancia de ProductID. Ambos métodos comienzan con la creación de una transacción y, después, ejecutan las instrucciones de modificación de datos dentro de un bloque try...catch. Si se produce una excepción, la transacción se revierte; de lo contrario, se confirma.

En el paso 5 se ha mostrado el efecto de las actualizaciones por lotes transaccionales frente a las actualizaciones por lotes en las que no se usa una transacción. En los tres tutoriales siguientes, creará la base establecida en este tutorial y creará interfaces de usuario para realizar actualizaciones, eliminaciones e inserciones por lotes.

¡Feliz programación!

Lecturas adicionales

Para más información sobre los temas tratados en este tutorial, consulte los siguientes recursos:

Acerca del autor

Scott Mitchell, autor de siete libros de ASP y ASP.NET, y fundador de 4GuysFromRolla.com, trabaja con tecnologías web de Microsoft desde 1998. Scott trabaja como consultor independiente, entrenador y escritor. Su último libro es Sams Teach Yourself ASP.NET 2.0 in 24 Hours. Puede ponerse en contacto con él a través de mitchell@4GuysFromRolla.com. o de su blog, que se puede encontrar en http://ScottOnWriting.NET.

Agradecimientos especiales a

Esta serie de tutoriales fue revisada por muchos revisores. Los revisores principales de este tutorial han sido Dave Gardner, Hilton Giesenow y Teresa Murphy. ¿Le interesaría revisar mis próximos artículos de MSDN? Si es así, escríbame a mitchell@4GuysFromRolla.com.