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.