Thursday, Feb 22, 2024 22:34 PM

> Patrón Unit of Work. Mejora el tratamiento que le das a tus transacciones de manera eficaz

Con este patrón aprenderás a realizar complejas transacciones en bases de datos relacionales de manera más sencilla

El patrón Unit of Work normalmente viene enlazado a otro reconocido patrón llamado Repository por lo que es recomendable que te encuentres familiarizado con este concepto previamente. Esto ocurre principalmente porque ambos patrones se utilizan en bases de datos de tipo relacional y son muy populares a la hora de trabajar con .NET, dado que numerosas aplicaciones empresariales lo implementan.

¿Qué es Unit of Work? 🤔 Como bien dice su nombre, el cual podríamos traducirlo como "Unidad de trabajo", estamos hablando de una unidad, y esto nos lleva a pensar a las operaciones que realizamos de manera consolidada, es decir que nos referimos a un conjunto cohesivo de operaciones que forman parte de un todo integral. Es normal a la hora de hablar de bases de datos relacionales que nombremos el término "transacción", el cual podríamos definirlo de la siguiente forma:

Una transacción es una unidad de trabajo que agrupa una serie de operaciones de base de datos en una ejecución única y coherente. La transacción garantiza que todas las operaciones se completen con éxito o que se reviertan en caso de un fallo, asegurando la integridad y consistencia de la base de datos.

Con Unit of Work vamos a tener la posibilidad de realizar numerosas operaciones que posiblemente alteren a más de una tabla en una misma transacción, esto conlleva considerables ventajas como:

  1. Atomicidad de las transacciones: Unit of Work nos permite agrupar múltiples operaciones en una única transacción lo que garantiza que todas las operaciones se completen con éxito o que se reviertan en caso de que alguna falle, manteniendo la integridad de los datos.
  2. Consistencia de datos: la agrupación de las operaciones en una transacción asegura que la base de datos pase de un estado consistente a otro de manera uniforme, por lo que si alguna operación falla todas las operaciones serán revertidas para mantener la consistencia.
  3. Facilita el mantenimiento de la base de datos: si te decides por utilizar Unit of Work podrás encapsular todas las operaciones relacionadas con una tarea específica en una sola unidad lógica en consecuencia, el código es más mantenible y se facilita su lectura.
  4. Mejora el rendimiento: utilizar una transacción para agrupar operaciones puede mejorar el rendimiento en ciertos casos. Por ejemplo, si estás realizando varias operaciones de escritura, agruparlas en una transacción puede reducir el número de operaciones de bloqueo y confirmación, mejorando así el rendimiento general.
  5. Aislamiento de transacciones: las operaciones realizadas dentro de la Unit of Work no son visibles para otras transacciones hasta que la Unit of Work se completa y se confirman los cambios.
  6. Facilita la implementación de patrones de diseño: la Unit of Work se alinea bien con otros patrones de diseño como Repository (que anteriormente nombramos), donde podrás encapsular la lógica de acceso a datos dentro de las operaciones de la Unit of Work.
  7. Manejo de relaciones complejas: si estás trabajando con un modelo de datos complejo que involucra operaciones en varias tablas relacionadas, la Unit of Work facilita la coordinación de estas operaciones en una única transacción.

Dicho esto, vayamos al código, vamos a mostrar un paso a paso sencillo de implementación a modo de ejemplo, de lo que sería una aplicación que se encarga de realizar publicaciones para un blog...

NOTA ❗ : La clean architecture utilizada es Onion (Cebolla), por lo que verán que los namespaces hacen referencia a la misma.

1) Creación de la interfaz IUnitOfWork:

namespace UnitOfWork.Core.Interfaces.Repositories
{
    public interface IUnitOfWork : IDisposable
    {
        IPostRepository Post { get; }
        IEmailRepository Email { get; }

        Task Save();
    }
}

Por el momento solo voy a decir que nuestra interfaz de IUnitOfWork debe heredar de la interfaz IDisposable, entraremos en detalles en los pasos siguientes del porqué. Como podemos ver establecemos en nuestra interfaz que haremos uso de otras dos interfaces: IPostRepository y IEmailRepository, la primera hace referencia a la posibilidad de realizar posts (publicaciones) y la segunda a la creación de correos electrónicos. Por último (y no menos importante) agregaremos una tarea llamada Save la cual será responsable del guardado en nuestra base de datos.

2) Creación de la clase UnitOfWork:

using UnitOfWork.Core.Interfaces.Repositories;
using UnitOfWork.Infrastructure.Data;

namespace UnitOfWork.Infrastructure.Repositories
{
    public sealed class UnitOfWork : IUnitOfWork
    {
        private readonly ApplicationDbContext _db;
        public IPostRepository Post { get; }
        public IEmailRepository Email { get; }

        public UnitOfWork(ApplicationDbContext db)
        {
            _db = db;
            Post = new PostRepository(_db);
            Email = new EmailRepository(_db);
        }

        public void Dispose()
        {
            _db.Dispose();
        }

        public async Task Save()
        {
            await _db.SaveChangesAsync();
        }
    }
}

Sigamos el código paso a paso, lo primero y principal que vamos a necesitar es inyectar en nuestro constructor nuestro contexto de base de datos dado que con él tendremos acceso a nuestro método Save y a la posibilidad de pasarle a nuestros respectivos repositorios el contexto que van a necesitar para operar. Es importante que las propiedades que representan a nuestros repositorios posean solo y únicamente el get y no el set (en caso de querer dejarlo hacerlo private). También establecí la clase como sealed dado que no será posible que ninguna otra clase herede de la misma.

Segundo, vamos a implementar la interfaz IDisposable anteriormente mencionada... ¿por qué necesitamos usar Dispose? 🤔 En el contexto de Entity Framework y el patrón de Unit Of Work, el método Dispose generalmente se utiliza para liberar recursos no administrados, como conexiones de base de datos, asegurándonos de que se liberen adecuadamente. Al invocar _db.Dispose() en el método Dispose de nuestro UnitOfWork, estamos liberando los recursos asociados al contexto de la base de datos ApplicationDbContext.

¿Qué pasa si no utilizo Dispose? 🤔 Al no usarlo estás dejando la responsabilidad de liberar los recursos al recolector de basura de .NET. Aunque el recolector de basura eventualmente liberará los recursos no administrados, el momento exacto en que lo haga es incierto y depende del comportamiento del recolector de basura. Por lo que es importante que una vez hecha nuestra transacción nos acordemos de liberar los recursos.

Tercero, método Save. ¿Por qué tenemos el Save por fuera de nuestros repositorios? 🤔 Para aquellos familiarizados con el patrón Repository, es común crear consultas mediante LINQ y finalizarlas con un SaveChangesAsync(). Sin embargo, al separar la operación de guardado de los repositorios, logramos desacoplar la responsabilidad de persistencia de la información de los repositorios. Esto posibilita la ejecución de múltiples operaciones dentro de una transacción, permitiéndonos llamar a nuestro método Save al finalizar dichas operaciones. Esta separación brinda flexibilidad, permitiendo coordinar y optimizar operaciones antes de confirmar cambios en la base de datos, así como revertir cambios en caso de ser necesario.

3) Registramos Unit of Work como un servicio:

Vayamos a nuestro Program.cs y agreguemos el siguiente servicio:

builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();

Este paso es de suma importancia dado que de esta manera tendremos acceso a nuestra unidad de trabajo cuando sea necesaria mediante inyección de dependencias.

4) Utilizando nuestra Unit of Work dentro de nuestro servicio:

using UnitOfWork.Core.Entities;
using UnitOfWork.Core.Interfaces.Repositories;

namespace UnitOfWork.Services
{
    public class PostService : IPostService
    {
        private readonly IUnitOfWork _unitWork;

        public PostService(IUnitOfWork unitWork)
        {
            _unitWork = unitWork;
        }

	public async Task CreatePostAndEmail(Post newPost, string subject)
        {
            await _unitWork.Post.Add(newPost);
            await _unitWork.Email.Add(subject);
            await _unitWork.Save();
            _unitWork.Dispose();
        }
    }
}

El ejemplo es sencillo y como dije en un principio no nos interesa conocer en detalle las implementaciones de los repositorios, lo importante acá es quedarnos con la idea general de lo que estamos realizando. Como podemos visualizar en el código, tenemos creado un servicio llamado PostService el cual posee un único método llamado CreatePostAndEmail. Se hizo uso de la inyección de dependencias para utilizar nuestra Unit of Work y lo curioso en este caso es que gracias a la misma hemos podido realizar en una única transacción dos operaciones de creación.

Primero se crea el post (publicación) y luego el Email para poder anunciar luego a los usuarios de este. Por último, se realiza el guardado y el liberado de los recursos en memoria. Lo bueno de este enfoque es que gracias a Unit of Work el Email nunca será creado si previamente ha fallado la creación de nuestro Post, como así tampoco será creado el Email si previamente no hemos podido crear el post, todo se mantiene ligado a una única transacción, caso contrario si hubiéramos realizado ambas operaciones en transacciones individuales, en caso de fallar una u la otra habría un post creado que nunca hubiera sido notificado a los usuarios o usuarios notificados sobre algo inexistente.

La implementación dada es sencilla pero será de fácil complejización acorde a lo que cada uno de ustedes necesite en su aplicación, las posibilidades son infinitas 🚀.

Espero haber sido claro con este sencillo ejemplo y se animen a implementarlo en sus proyectos tanto personales como del trabajo. Ante cualquier duda me avisan en los comentarios y estaré gustoso de ayudarlos, por más buenas prácticas en nuestros códigos! Gracias por su tiempo y leer hasta aquí 😀.

(1) -   Download -  by elsmrls

> Comments (0)