Cómo hacer un join en Doctrine

Parece algo bien simple, ¿no? Después de todo, hacerlo en SQL lo es. Pero si estás usando Doctrine, lo correcto es usar las capacidades del ORM.

Recientemente tenía que resolver un problema de un cliente: se le habían duplicado registros en la base y era necesario eliminarlos.

El punto es que no se tenían que eliminar todos los registros, si no sólo los problemáticos.

El modelo de datos es algo así:

Lo que yo necesitaba hacer era eliminar aquellas transacciones que, entre otros criterios de filtro, pertenecieran a un proveedor en particular.

La forma en que lo hice fue mediante una consulta que busque todos los objetos necesarios y luego los elimine:

$qb = $this->em
            ->createQueryBuilder()
            ->select('t')
            ->from(Transaction::class, 't')
            ->where('t.date BETWEEN :f AND :t')
            ->setParameter('f', $fromDate)
            ->setParameter('t', $toDate)
            ->orderBy('t.date')
        ;

Hasta aquí se arma la consulta genérica, sólo incluyendo los filtros obligatorios (Las fechas básicamente).

Luego, si se desea usar el filtro por proveedor tenemos:

$qb->innerJoin(Account::class, 'a')
   ->andWhere('a.provider = :p')
   ->setParameter('p', $provider);

Todo normal, ¿cierto?

Pues aquí arrancaron los problemas.

Cuando fuí a verificar en la pantalla de la aplicación cuántos registros correspondían al rango de fecha y al proveedor encontré unos 3000, sin embargo, el script me estaba diciendo que iba a eliminar unas 100k transacciones.

De modo que decidí agregarle al script una opción para ver el SQL que estaba a punto de ejecutar y esto es lo que encontré:

SELECT t0_.id AS id_1, t0_.amount AS amount_5, t0_.account_id AS account_id_10 FROM transaction t0_ INNER JOIN account a1_ WHERE (t0_.date BETWEEN ? AND ?) AND a1_.provider_id = ? ORDER BY t0_.date ASC

Nuevamente me quedé un rato mirando el SQL. Todo se veía bien.

¿Qué podía estar pasando?

Bueno… la verdad es que no estaba tan bien el SQL. Si le prestás un poco de atención notarás que al INNER JOIN le falta un detalle: la cláusula ON.

El SQL que debería haberse generado debía ser más parecido a:

SELECT t0_.id AS id_1, t0_.amount AS amount_5, t0_.account_id AS account_id_10 FROM transaction t0_ INNER JOIN account a1_ ON a1_.id = t0.account_id WHERE (t0_.date BETWEEN ? AND ?) AND a1_.provider_id = ? ORDER BY t0_.date ASC

Sí. Ese simple detalle estaba haciendo una diferencia fundamental.

Sin la cláusula ON el INNER JOIN se convierte en el producto cartesiano de las dos tablas, con lo cual se pierde totalmente el sentido de usar un JOIN y el resultado tiene muy poco sentido

Perfecto, el problema está identificado. ¿Cómo lo solucionamos?

Como de costumbre, se trata de volver a las fuentes. La documentación de Doctrine es bastante clara al respecto.

Se puede hacer algo como:

$qb->innerJoin(Account::class, 'a', Join::ON, 't.account_id = a.id')
   ->andWhere('a.provider = :p')
   ->setParameter('p', $provider);

Y el resultado será correcto, pero si lo dejamos así estamos haciendo trabajo extra… y ya que tenemos un ORM, ¿por qué hacerlo?

Siendo t el alias de la clase Transaction y estando la relación definida como parte del mapeo objeto-relacional una mejor versión es esta:

$qb->innerJoin('t.a', 'a')
   ->andWhere('a.provider = :p')
   ->setParameter('p', $provider);

En conclusión SQL y DQL son parecidos pero no tanto. Vale la pena conocer las diferencias y usar lo mejor de cada uno en cada ocasión.

mchojrin

Por mchojrin

Ayudo a desarrolladores PHP a acceder mercados y clientes más sofisticados y exigentes

¿Te quedó alguna duda? Publica aca tu pregunta

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.