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:
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:
$entityManager->clear() para grandes conjuntos de datos.
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.