[Patterns] UnitOfWork

Leyendo el artículo de Javier Torrecilla  Patrón Unit of Work (UoW) o Unidad de Trabajo me acordé que en 2007 había escrito sobre este patrón y quiero rescatarlo aquí:
 
Este es uno de los patrones más útiles (desde mi punto de vista) con que me he encontrado en el libro "Patterns of Enterprise Application Architecture" (P of EAA) de Martin Fowler. Como dice Martin en su libro este patrón:
"Maintains a list of objects affected by a business transaction and coordinates the writing out of changes and the resolution of concurrency problems."
Este patrón se utiliza entonces para trabajar con un conjunto de objetos persistentes que deben tratarse como una "unidad" de trabajo, almacenándose en una base de datos de manera atómica. Este patrón es el encargado de hacer el seguimiento de todos aquellos objetos que son nuevos, y que por lo tanto deben persistirse, todos los objetos que han sido modificados y que deben actualizarse en la base de datos y todos los que han sido borrados y deben quitarse de la base de datos.
 
Mediante la implementación del patrón UnitOfWork, se logra una disminución de la cantidad de idas y vueltas hacia la base de datos ya que los cambios se realizan por lotes (o unidades de trabajo) redundando en incremento en el rendimiento del sistema porque muchas veces el cuello de botella de las aplicaciones se encuentra en la red de datos. Otro aspecto interesante es que posibilita el esquema de trabajo desconectado con bloqueos optimistas sobre la base de datos, es decir, no se mantienen bloqueados los registros por largos períodos de tiempo sino que se abre una conexión, se inicia una transacción, se hacen los cambios, se hace commit y se libera la conexión todo esto en muy pero muy poco tiempo.
De ser necesaria la incorporación de un control de concurrencia sobre los registros que serán afectados durante la transacción, de modo de mantener la consistencia de los datos, este patrón nos brinda el ámbito adecuado en donde implementarlo.
Veamos un caso concreto, el famoso ejemplo de la factura. Existe una factura que el usuario debe modificar, entonces traemos desde la base de datos la factura con sus ítems (de factura) cargados, luego, una vez que lo presentamos en pantalla, el usuario hace lo siguiente:
  • Agrega dos ítems más a la lista de ítems,
  • Modifica el importe y/o la cantidad de productos de uno de los ítems y finalmente,
  • Elimina algún que otro ítem.

En este caso, cuando deben persistirse los datos? apenas agrega, modifica o elimina un ítem? o debe tratarse a la factura como una unidad y persistir todo junto? Si algo falla,… debe guardarse el resto? que sucede si otro usuario (además del que estamos hablando) estaba trabajando con el mismo documento al mismo tiempo pero guardó primero? como sabemos que fue lo que modificó el usuario y que, por lo tanto, debemos guardar? guardamos primero la factura y luego sus ítems o al revés?

Todas las respuestas a estas preguntas (que considero que las fuiste contestando) se resuelven mediante la implementación de este patrón.

Voy a explicarlo un poco. El Unit Of Work, como se observa en la figura de arriba, se implementa mediante una única clase la cual tendrá al menos tres listas, una lista para los objetos nuevos que deben guardarse en la db, otra lista para los objetos que han sido modificados y que por lo tanto deben modificarse en la db y, otra lista para los objetos que deben eliminarse. Opcionalmente, o mejor dicho, según la implementación lo requiera, puede contener una cuarta lista para almacenar clones de los objetos que se traen desde la base de datos, de manera que antes de persistir un objeto pueda corroborar, mediante estos clones de los objetos originales, que los registros correspondientes no han sido modificados por otro usuario.

Una razón para leer el libro y no solo conformarse con este artículo es que Fowler hace un análisis exhaustivo de este patrón (y de todos sus patrones), sus formas de implementar, cuando implementarlos, pros y contras y sobre todo, lo que a mi más me gustó fueron los distintos intentos por hacer esta clase lo más transparente posible para el programador. Así, por ejemplo, analiza la posibilidad de que todas las clases persistibles hereden de una clase base que automáticamente las registre como nuevas en el contenedor, que cuando se invoque el setter de una propiedad, esta notifique al UnitOfWork correspondiente para que la registre como dirty, etc.

Acá dejo una implementación sencillísima de UnitOfWork sin control de concurrencias y en colaboración con un DataMapper trivial (actualización: el artículo original tenía un proyecto adjunto el cual he perdido, por esta razón el DataMapper no está presente pero lo que originalmente hacía era retornar la sentencia sql necesaria para insertar, modificar y borrar los objetos en la base de datos).

using System;
using System.Collections.Generic;
using System.Text;
using System.Transactions;
using System.Data.SqlClient;
using System.Configuration;
using System.Threading;
using System.Resources;
using ConsoleApplication7;

namespace Patterns
{
    // Nuestra clase UnitOfWork (Martin Fowler)
    public class UnitOfWork
    {
        // Aquí estan las tres listas de las que hablamos
        // Ademas de estas puede existir una cuarta que almacene
        // los objetos limpios (o que se leyeron de la db)
        List<IBusinessObject> newObjects;
        List<IBusinessObject> dirtyObjects;
        List<IBusinessObject> removedObjects;

        // Creamos las listas en este constructor
        public UnitOfWork()
        {
            newObjects = new List<IBusinessObject>();
            dirtyObjects = new List<IBusinessObject>();
            removedObjects = new List<IBusinessObject>();
        }

        public void New(IBusinessObject bo)
        {
            Guard.NotNull(Resources.parameter_is_null, "bo", bo);
            Guard.IsTrue(Resources.object_is_dirty, dirtyObjects.Contains(bo));
            Guard.IsTrue(Resources.object_is_deleted, removedObjects.Contains(bo));
            Guard.IsTrue(Resources.object_is_already_inserted, newObjects.Contains(bo));

            newObjects.Add(bo);
        }

        public void Remove(IBusinessObject bo)
        {
            Guard.NotNull(Resources.parameter_is_null, "bo", bo);
            if (newObjects.Remove(bo)) return;
            dirtyObjects.Remove(bo);

            if (!removedObjects.Contains(bo))
                removedObjects.Add(bo);
        }

        public void Update(IBusinessObject bo)
        {
            Guard.NotNull(Resources.parameter_is_null, "bo", bo);
            Guard.IsTrue(Resources.object_is_deleted, removedObjects.Contains(bo));

            if (!newObjects.Contains(bo) && !dirtyObjects.Contains(bo))
                dirtyObjects.Add(bo);
        }

        // El método Commit es el encargado de iniciar las transacciones,
        // y realizar la invocación a la base de datos.
        public void Commit()
        {
            string connectString = string.Empty;
            using (TransactionScope transactionScope = new TransactionScope())
            {
                connectString = ConfigurationManager.ConnectionStrings["Patterns"].ConnectionString;

                using (SqlConnection connection = new SqlConnection(connectString))
                {
                    // Construimos las sentencias SQL para luego pasárselas a la DB
                    // mediante un command. Para esto, en .Net hay que hacerlo separando
                    // las sentencias con un punto y como (;)
                    StringBuilder stringBuilder = new StringBuilder();

                    foreach (IBusinessObject bo in newObjects)
                        stringBuilder.Append(Mapper.Instance.Insert(bo) + ";");

                    foreach (IBusinessObject bo in dirtyObjects)
                        stringBuilder.Append(Mapper.Instance.Update(bo) + ";");

                    foreach (IBusinessObject bo in removedObjects)
                        stringBuilder.Append(Mapper.Instance.Delete(bo) + ";");

                    string command = stringBuilder.ToString();

                    // Abre la conexió, crea el comando con las sentencias SQL
                    // e invoca al RDBMS.
                    connection.Open();
                    SqlCommand command1 = new SqlCommand(command, connection);
                    command1.ExecuteNonQuery();

                    // Limpia las listas si todo estuvo bien.
                    ClearAll();
                }
                transactionScope.Complete();
            }
        }

        private void ClearAll()
        {
            newObjects.Clear();
            dirtyObjects.Clear();
            removedObjects.Clear();
        }
    }
}
Sin categoría

One thought on “[Patterns] UnitOfWork

Responder a anonymous Cancelar respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *