Etiqueta: Doctrine

  • Cómo hacer un join en Doctrine

    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.

  • Cómo filtrar colecciones usando Doctrine ORM

    Estaba pensando cómo escribir este artículo en forma clara y didáctica y, después de darle algunas vueltas me pareció que lo más fácil era armar un video:

    Para más información sobre Doctrine podés consultar acá

  • Cómo definir relaciones Cero-a-Uno con Doctrine

    Cómo definir relaciones Cero-a-Uno con Doctrine

    En general cuando uno comienza a estudiar Bases de Datos Relacionales se habla de un tipo de relación Uno-a-Uno que, en la práctica se usa muy poco. Sin embargo las relaciones tipo Cero-a-Uno tienen muchísimo sentido.

    Es el caso de que se quiera modelar un sistema donde una entidad es un caso especial de otra (Algo similar al concepto de herencia de POO), como por ejemplo la relación entre personas y actores:

    • Todos los actores son personas
    • No todas las personas son actores

    La forma de implementar esto en una base de datos relacional es poner un campo tipo clave foránea en la tabla hija, el cual a su vez tendrá un índice único (Para reforzar la cardinalidad de la relación).

    En el caso del ORM Doctrine, todo esto parte de la definición de la clase Entity.

    Pongo un ejemplo de un proyecto en el que trabajé: hay una clase User (De FOSUserBundle) y una clase Taller. Cada taller tiene un responsable, pero no todos los usuarios son responsables de algún taller.

    En la clase User está este código

    /**
     * @var Taller
     * @ORM\OneToOne(targetEntity="AppBundle\Entity\Taller",mappedBy="responsable")
     */
    protected $taller;

    Y en la clase Taller este:

    /**
     * @var User
     * @ORM\OneToOne(targetEntity="AppBundle\Entity\User", inversedBy="taller")
     */
     private $responsable;

    Como podés observar, ambas clases tienen una referencia a la otra, pero en la base de datos, sólo la tabla Taller tiene una referencia a User.

    Esto se debe a cómo están escritas las definiciones del vínculo OneToOne.

    Más específicamente, se debe a qué clase tiene el mappedBy y el inversedBy: El lado 1 de la relación (es decir, la entidad que siempre existirá) es el que debe llevar inversedBy.

    Si bien este tipo de relaciones no son lo más común, ciertamente viene bien conocerlas y la posibilidad de definirlas usando las herramientas de Doctrine ayuda mucho.

    ¿Alguna duda? ¡Dejala en un comentario!