Este patrón se utiliza entonces para trabajar con un conjunto de objetos persistentes que deben tratarse como una "unidad" de trabajo, almacenandose en una base de datos de manera atómica. Este patrón es el encargado de trackear 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 DB y todos los que han sido borrados y deben quitarse de la base de datos.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 items (de factura) cargados, luego, una vez que lo presentamos en pantalla, el usuario hace lo siguiente:
- Agrega dos items más a la lista de items,
- Modifica el importe y/o la cantidad de productos de uno de los items y finalmente,
- Elimina uno de los item.
En este caso, cuando deben persistirse los datos? apenas agrega, modifica o elimina un item? 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 los items 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álisi exaustivo 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 methos de una propiedad, esta notifique al UnitOfWork correspondiente para que la registre como dirty, etc. Personalmente estube probando esto y otras ideas como AOP, que no me convenció para nada, y extensions methods con los cuales le agrega m'etodos de persistencia a los objetos que heredaban de esa clase base.
En cuanto a AOP, no me convención porque las opciones eran todas muy feas, o usaba el .Net Profiling API con C++ para capturar la ejecución del JIT y entonces inyectarle código MSIL a los setters, o las clases heredaban de ContextBoundObject para, mediante los proxies de remounting, poder interceptar las invocaciones a las propiedades (lento, sucio, que más?) o, usando un compilador de terceros (no MS) casi en todos los casos en beta 0.000001. Así que definitivamente por el momento no lo ví como una opción.
En resumen, creo que es mejor, aunque un poquito mas tedioso y propenso a errores, dejar que sea el programador quien registre los objetos en el Unit of Work.
Acá dejo una implementación sencillísima de UnitOfWork sin control de concurrencias y en colaboración con un DataMapper trivial. Más abajo les dejo un proyecto que implementa esta clase.
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;
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))
{
try
{
// 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();
}
catch (Exception ex)
{
System.Console.WriteLine("Exception Message: {0}", ex.Message);
}
}
transactionScope.Complete();
}
}
private void ClearAll()
{
newObjects.Clear();
dirtyObjects.Clear();
removedObjects.Clear();
}
}
}
El proyecto: http://www.carloszanini.com.ar/shared/UnitOfWork.zip
No esperen gran cosa. Gracias a Carlos Zanini por el hosting.
Lucas Ontivero
5 comentarios:
Hola! A mi me gusta su blog! Hay une myu bueno software factory que se llama "XI-Factory"... utilisamos este factory para nuestros projectos..
http://www.xifactory.com
Antes de nada, muchas gracias por este post. Estoy buscando información sobre el patrón UnitOfWork, porque ando implementando una. Me he encontrado con problemas a la hora de persistir objetos que dependen unos de otros en la base de datos (por ejemplo, eliminar una factura sin eliminar antes las líneas de factura). ¿Cómo resolvió este problema? Tengo medio implementado un orden topológico entre objetos de negocio, pero me gustaría saber si se encontró con el mismo problema y cómo lo resolvió. Muchas gracias de antemano.
Instrospectre, el problema que tienes no es propio del UnitOfWork sino del mapper. Es el mapper el que debe conocer como guardar, actualizar, eliminar y consultar los objetos.
En mi portal preguntaalexperto.net he puesto un artículo en el que muestro como se hace un mapper, quizás te interese consultarlo, en http://www.preguntaalexperto.net/articles/lontivero-Como-crear-un-peque241o-ORM.aspx. La idea es que el los métodos save, update y delete se éxplique´como deben realizarse estas operaciones.
Otra posibilidad es usar algo ya existente como linq, hibernate u otro.
Saludos
Estimado Lucas: muchas gracias por su contestación. Creo que no me expliqué bien. La responsabilidad acerca de los métodos CRUD están implementados en mi caso, como bien dice, por medio de un data mapper (o broker, como lo denomina Larman). Cada uno de ellos es responsable de realizar la operación solicitada sobre un registro particular. El probleama surge cuando la existencia del estado de un objeto en la base de datos depende de la existencia previa de otro (por medio de una clave foránea, por ejemplo). En el caso de bases de datos Oracle no existe tal problema, porque la comprobación de las restricciones de integridad se realiza al finalizar la transacción, pero sí existe por ejemplo en SQL Server, dado que comprueba las restricciones de integridad tras cada operación CUD. En ese caso, y para evitar errores, es necesario establecer un orden de ejecución entre los mappers. ¿Se ha encontrado con una situación similar? Muchas gracias.
Instrospectre, respondo exactamente lo mismo. El problema que Ud. tiene es con el mapper y no con el UoW.
Publicar un comentario