Blog


Implementación del patrón Unit of Work en Symfony con Doctrine

IMPLEMENTACIÓN DEL PATRÓN UNIT OF WORK EN SYMFONY CON DOCTRINE

25 / 06 / 2024 Symfony

El patrón Unit of Work es una técnica de diseño crucial para manejar múltiples operaciones de base de datos de manera eficiente y consistente. En este artículo, exploraremos cómo implementar este patrón en Symfony utilizando Doctrine ORM, mejorando así el rendimiento y la integridad de nuestras aplicaciones.

Antes de nada, necesitamos conocer el patrón Unit of Work.

El patrón Unit of Work mantiene una lista de objetos afectados por una transacción de negocio y coordina la escritura de cambios y la resolución de problemas de concurrencia. En el contexto de Doctrine ORM, este patrón está implementado en el EntityManager. 

Algunos de los beneficios que nos aporta:

  • Mejora el rendimiento al agrupar operaciones de base de datos.
  • Mantiene la consistencia de los datos.
  • Reduce el riesgo de condiciones de carrera.
  • Simplifica la gestión de transacciones.

En Symfony, el EntityManager de Doctrine ya implementa el patrón Unit of Work. Veamos un ejemplo básico:

<?php

namespace App\Controller;

use App\Entity\Pedido;
use App\Entity\Producto;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class PedidoController extends AbstractController
{
    #[Route('/pedido/crear', name: 'pedido_crear')]
    public function crear(EntityManagerInterface $entityManager): Response
    {
        $pedido = new Pedido();

        $producto1 = $entityManager->getRepository(Producto::class)->find(1);
        $producto2 = $entityManager->getRepository(Producto::class)->find(2);

        $pedido->addProducto($producto1);
        $pedido->addProducto($producto2);

        $entityManager->persist($pedido);
        $entityManager->flush();

        return new Response('Pedido creado correctamente.');
    }
}

En este ejemplo, el EntityManager está manejando múltiples operaciones (crear una orden y asociar productos) como una única unidad de trabajo.

Para operaciones más complejas, es recomendable usar transacciones explícitas:

public function crearPedidoComplejo(EntityManagerInterface $entityManager): Response
{
    $entityManager->beginTransaction();

    try {
        $pedido = new Pedido();
        $producto1 = $entityManager->getRepository(Producto::class)->find(1);
        $producto2 = $entityManager->getRepository(Producto::class)->find(2);

        $pedido->addProducto($producto1);
        $pedido->addProducto($producto2);

        // actualiza inventario
        $producto1->decrementarStock(1);
        $producto2->decrementarStock(1);

        $entityManager->persist($pedido);
        $entityManager->flush();

        $entityManager->commit();

        return new Response('Pedido complejo creado correctamente.');
    } catch (\Exception $e) {
        $entityManager->rollback();
        throw $e;
    }
}

Llegados a este punto, tenemos que preguntarnos cómo podemos optimizar el Unit of Work en nuestros desarrollos.

Batch Processing

Para operaciones en lote, podemos limpiar periódicamente el EntityManager para liberar memoria:

public function procesoLotes(EntityManagerInterface $entityManager): Response
{
    $batchSize = 20;
    $i = 0;

    $repository = $entityManager->getRepository(Producto::class);
    $productos = $repository->findAll();

    foreach ($productos as $producto) {
        $producto->actualizaPrecio();
        $entityManager->persist($producto);

        if (($i % $batchSize) === 0) {
            $entityManager->flush();
            $entityManager->clear(); // limpia el EntityManager
        }

        $i++;
    }

    $entityManager->flush(); // flush final para cualquier entidad restante

    return new Response('Procesamiento por lotes completado');
}

Detach y Merge

Para trabajar con objetos grandes que no necesitamos seguir rastreando:

public function procesaObjectoGrande(EntityManagerInterface $entityManager): Response
{
    $objetoGrande = $entityManager->find(ObjectoGrande::class, 1);
    $entityManager->detach($objetoGrande);

    // procesa $objetoGrande...

    $entityManager->merge($objetoGrande);
    $entityManager->flush();

    return new Response('Objeto grande procesado.');
}

Manejo de concurrencia

Doctrine proporciona bloqueo optimista para manejar problemas de concurrencia:

<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
class Producto
{
    // ...

    #[ORM\Version]
    #[ORM\Column(type: 'integer')]
    private $version;

    // ...
}

Cuando intentamos actualizar una entidad, Doctrine verifica la versión:

public function actualizaProducto(EntityManagerInterface $entityManager, int $id): Response
{
    $producto = $entityManager->find(Producto::class, $id);
    $producto->setNombre('Nuevo nombre');

    try {
        $entityManager->flush();
        return new Response('Producto actualizado correctamente.');
    } catch (\Doctrine\ORM\OptimisticLockException $e) {
        return new Response('El producto ha sido modificado por otro usuario.', 409);
    }
}

Eventos del ciclo de vida

Podemos usar eventos del ciclo de vida para realizar acciones adicionales durante el proceso de Unit of Work:

<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
#[ORM\HasLifecycleCallbacks]
class Pedido
{
    // ...

    #[ORM\PrePersist]
    public function setCreatedAtValue()
    {
        $this->createdAt = new \DateTime();
    }

    #[ORM\PreUpdate]
    public function setUpdatedAtValue()
    {
        $this->updatedAt = new \DateTime();
    }
}

 

Para depurar el Unit of Work, podemos usar el Symfony Profiler. También podemos obtener información sobre el estado actual del Unit of Work:

public function depurarUnitOfWork(EntityManagerInterface $entityManager): Response
{
    $uow = $entityManager->getUnitOfWork();
    $scheduledInsertions = $uow->getScheduledEntityInsertions();
    $scheduledUpdates = $uow->getScheduledEntityUpdates();
    $scheduledDeletions = $uow->getScheduledEntityDeletions();

    // procesar esta información...

    return new Response('Unit of Work depurado.');
}

 

Para mejorar nuestras prácticas con Unit of Work podemos:

  • Agrupar operaciones relacionadas en una sola transacción.
  • Usar $entityManager->clear() para grandes conjuntos de datos.
  • Implementar bloqueo optimista para manejar concurrencia.
  • Utilizar eventos del ciclo de vida para lógica de negocio consistente.
  • Monitorear y optimizar consultas usando el Symfony Profiler.

 

Unit of Work es una herramienta poderosa para manejar operaciones de base de datos complejas de manera eficiente y consistente. Al entender y utilizar correctamente este patrón, podemos mejorar significativamente el rendimiento y la integridad de nuestras aplicaciones Symfony.



ARTÍCULOS RELACIONADOS