El generador de consultas
Keep on Learning!
If you liked what you've learned so far, dive in! Subscribe to get access to this tutorial plus video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeLa página /browse
funciona... ¿pero qué pasa si hacemos clic en uno de estos géneros? Bueno... eso funciona más o menos. Muestra el nombre del género... pero obtenemos una lista de todas las mezclas. Lo que realmente queremos es filtrarlas para que sólo se muestren las mezclas de ese género concreto.
Ahora mismo, todas las mezclas de la base de datos están en el género "Pop". Vuelve aMixController
y encuentra el método falso que crea nuevas mezclas para que podamos hacer algunos datos ficticios más interesantes. Añade una variable $genres
con "Pop" y "Rock" incluidos... Luego selecciona una al azar con $genres[array_rand($genres)]
.
// ... lines 1 - 10 | |
class MixController extends AbstractController | |
{ | |
// ... line 13 | |
public function new(EntityManagerInterface $entityManager): Response | |
{ | |
// ... lines 16 - 18 | |
$genres = ['pop', 'rock']; | |
$mix->setGenre($genres[array_rand($genres)]); | |
// ... lines 21 - 31 | |
} | |
} |
¡Genial! Ahora ve a /mix/new
y actualiza unas cuantas veces... hasta que tengamos unas 15 mezclas. Volvemos a /browse
... ¡yup! Tenemos una mezcla de géneros "Rock" y "Pop"... sólo que aún no se filtran.
Así que nuestra misión está clara: personalizar la consulta de la base de datos para que sólo devuelva los resultados de un género concreto. Bien, en realidad podemos hacerlo de forma súper sencilla en VinylController
a través del método findBy()
. El género está en la URL como el comodín $slug
.
Así que podríamos añadir una sentencia "if" en la que, si hay un género, devolvamos todos los resultados en los que genre
coincida con $slug
. Pero esta es una gran oportunidad para aprender a crear una consulta personalizada. Así que vamos a deshacerlo.
Método del repositorio personalizado
La mejor manera de hacer una consulta personalizada, es crear un nuevo método en el repositorio para la entidad de la que quieras obtener datos. En este caso, eso significaVinylMixRepository
. Esto contiene algunos métodos de ejemplo. Descomenta el primero... y empieza de forma sencilla
// ... lines 1 - 16 | |
class VinylMixRepository extends ServiceEntityRepository | |
{ | |
// ... lines 19 - 41 | |
/** | |
* @return VinylMix[] Returns an array of VinylMix objects | |
*/ | |
public function findByExampleField($value): array | |
{ | |
return $this->createQueryBuilder('v') | |
->andWhere('v.exampleField = :val') | |
->setParameter('val', $value) | |
->orderBy('v.id', 'ASC') | |
->setMaxResults(10) | |
->getQuery() | |
->getResult() | |
; | |
} | |
// ... lines 56 - 65 | |
} |
Llámalo findAllOrderedByVotes()
. No nos preocuparemos todavía del género: Sólo quiero hacer una consulta que devuelva todas las mezclas ordenadas por votos. Quitando el argumento, esto devolverá un array y el PHPdoc anterior ayuda a mi editor a saber que será un array de objetos VinylMix
// ... lines 1 - 41 | |
/** | |
* @return VinylMix[] Returns an array of VinylMix objects | |
*/ | |
public function findAllOrderedByVotes(): array | |
{ | |
// ... lines 47 - 51 | |
} | |
// ... lines 53 - 64 |
DQL y el QueryBuilder
Hay varias formas de ejecutar una consulta personalizada en Doctrine. Doctrine, por supuesto, acaba realizando consultas SQL. Pero Doctrine trabaja con MySQL, Postgres y otros motores de bases de datos... y el SQL necesario para cada uno de ellos es ligeramente diferente.
Para manejar esto, internamente, Doctrine tiene su propio lenguaje de consulta llamado Doctrine Query Language o "DQL", Tiene un aspecto similar a
SELECT v FROM App\Entity\VinylMix v WHERE v.genre = 'pop';
Puedes escribir estas cadenas a mano, pero yo aprovecho el "QueryBuilder" de Doctrine: un bonito objeto que ayuda... ya sabes... ¡a construir esa consulta!
Crear el QueryBuilder
Para utilizarlo, empieza con $this->createQueryBuilder()
y pasa un alias que se utilizará para identificar esta clase dentro de la consulta. Hazlo corto, pero único entre tus entidades - algo como mix
.
// ... lines 1 - 44 | |
public function findAllOrderedByVotes(): array | |
{ | |
return $this->createQueryBuilder('mix') | |
// ... lines 48 - 51 | |
} | |
// ... lines 53 - 64 |
Como estamos llamando a esto desde dentro de VinylMixRepository
, el QueryBuilder ya sabe que hay que consultar desde la entidad VinylMix
... y utilizará mix
como alias. Si ejecutáramos este query builder ahora mismo, sería básicamente:
SELECT * FROM vinyl_mix AS mix
El constructor de consultas está cargado de métodos para controlar la consulta. Por ejemplo, llama a ->orderBy()
y pasa a mix
-ya que es nuestro alias- .votes
y luegoDESC
.
// ... lines 1 - 44 | |
public function findAllOrderedByVotes(): array | |
{ | |
return $this->createQueryBuilder('mix') | |
->orderBy('mix.votes', 'DESC') | |
// ... lines 49 - 51 | |
} | |
// ... lines 53 - 64 |
Ya está Ahora que nuestra consulta está construida, para ejecutarla llama a ->getQuery()
(que la convierte en un objeto Query
) y luego a ->getResult()
.
// ... lines 1 - 44 | |
public function findAllOrderedByVotes(): array | |
{ | |
return $this->createQueryBuilder('mix') | |
// ... line 48 | |
->getQuery() | |
->getResult() | |
; | |
} | |
// ... lines 53 - 64 |
Bueno, en realidad, hay varios métodos a los que puedes llamar para obtener los resultados. Los dos principales son getResult()
-que devuelve una matriz de los objetos coincidentes- o getOneOrNullResult()
, que es el que utilizarías si estuvieras consultando un VinylMix
específico o nulo. Como queremos devolver una matriz de mezclas coincidentes, utiliza getResult()
.
Ahora podemos utilizar este método. En VinylController
(déjame cerrar MixController
...), en lugar de findBy()
, llama a findAllOrderedByVotes()
.
// ... lines 1 - 10 | |
class VinylController extends AbstractController | |
{ | |
// ... lines 13 - 36 | |
public function browse(VinylMixRepository $mixRepository, string $slug = null): Response | |
{ | |
// ... lines 39 - 40 | |
$mixes = $mixRepository->findAllOrderedByVotes(); | |
// ... lines 42 - 46 | |
} | |
} |
Me encanta lo claro que es este método: hace que sea súper obvio lo que estamos consultando exactamente. Y cuando lo probamos... ¡sigue funcionando! Todavía no está filtrando, pero el orden es correcto.
Añadir la sentencia WHERE
Bien, volvamos a nuestro nuevo método. Añade un argumento opcional string $genre = null
. Si se pasa un género, tenemos que añadir una sentencia "where". Para hacer espacio para ello, divide esto en varias líneas... y sustituye return
por $queryBuilder =
. A continuación, return $queryBuilder
por ->getQuery()
, y ->getResult()
.
// ... lines 1 - 16 | |
class VinylMixRepository extends ServiceEntityRepository | |
{ | |
// ... lines 19 - 44 | |
public function findAllOrderedByVotes(string $genre = null): array | |
{ | |
$queryBuilder = $this->createQueryBuilder('mix') | |
->orderBy('mix.votes', 'DESC'); | |
return $queryBuilder | |
->getQuery() | |
->getResult() | |
; | |
} | |
// ... lines 55 - 64 | |
} |
Ahora podemos decir if ($genre)
, y añadir la declaración "where". ¿Cómo? Apuesto a que puedes adivinar: $queryBuilder->andWhere()
.
Pero una advertencia. También hay un método where()
... pero yo nunca lo uso. Cuando llames a where()
, borrará cualquier sentencia "where" existente que pueda tener el constructor de consultas... por lo que podrías eliminar accidentalmente algo que hayas añadido antes. Por tanto, utiliza siempre andWhere()
. Doctrine es lo suficientemente inteligente como para darse cuenta de que, al ser el primer WHERE, no necesita añadir elAND
.
Dentro de andWhere()
, pasa mix.genre
=... pero no pongas el género dinámico justo en la cadena. Eso es un gran no-no: nunca lo hagas. Eso te abre a los ataques de inyección SQL. En su lugar, siempre que necesites poner un valor dinámico en una consulta, utiliza una "sentencia preparada"... que es una forma elegante de decir que pones un marcador de posición aquí, como :genre
. El nombre de esto puede ser cualquier cosa... como "dinosaurio" si quieres. Pero lo llames como lo llames, luego rellenarás el marcador de posición diciendo ->setParameter()
con el nombre del parámetro -así quegenre
- y luego el valor: $genre
.
// ... lines 1 - 44 | |
public function findAllOrderedByVotes(string $genre = null): array | |
{ | |
// ... lines 47 - 49 | |
if ($genre) { | |
$queryBuilder->andWhere('mix.genre = :genre') | |
->setParameter('genre', $genre); | |
} | |
// ... lines 54 - 58 | |
} | |
// ... lines 60 - 71 |
¡Qué bonito! De nuevo en VinylController
, pasa $slug
como género.
¡Vamos a probar esto! Vuelve a la página de navegación primero. ¡Genial! Obtenemos todos los resultados. Ahora haz clic en "Rock" y... ¡bien! Menos resultados y todos los géneros muestran "Rock"! Si filtro por "Pop"... ¡lo tengo! Incluso podemos ver la consulta para esto... aquí está. Tiene la sentencia "where" para el género igual a "Pop". ¡Guau!
Reutilización de la lógica del generador de consultas
A medida que tu proyecto se hace más y más grande, vas a crear más y más métodos en tu repositorio para las consultas personalizadas. Y puede que empieces a repetir la misma lógica de consulta una y otra vez. Por ejemplo, podríamos ordenar por los votos en un montón de métodos diferentes de esta clase.
Para evitar la duplicación, podemos aislar esa lógica en un método privado. ¡Compruébalo! Añade private function addOrderByVotesQueryBuilder()
. Esto aceptará un argumentoQueryBuilder
(queremos el de Doctrine\ORM
), pero hagámoslo opcional. Y también devolveremos un QueryBuilder
.
// ... lines 1 - 17 | |
class VinylMixRepository extends ServiceEntityRepository | |
{ | |
// ... lines 20 - 60 | |
private function addOrderByVotesQueryBuilder(QueryBuilder $queryBuilder = null): QueryBuilder | |
{ | |
// ... lines 63 - 65 | |
} | |
// ... lines 67 - 76 | |
} |
El trabajo de este método es añadir esta línea ->orderBy()
. Y por comodidad, si no pasamos un $queryBuilder
, crearemos uno nuevo.
Para permitirlo, empieza con$queryBuilder = $queryBuilder ??
$this->createQueryBuilder('mix'). Vuelvo a utilizar a propósito mix
para el alias. Para simplificar la vida, elige un alias para una entidad y utilízalo sistemáticamente en todas partes.
// ... lines 1 - 60 | |
private function addOrderByVotesQueryBuilder(QueryBuilder $queryBuilder = null): QueryBuilder | |
{ | |
$queryBuilder = $queryBuilder ?? $this->createQueryBuilder('mix'); | |
// ... lines 64 - 65 | |
} | |
// ... lines 67 - 78 |
En cualquier caso, esta línea puede parecer extraña, pero básicamente dice
Si existe un QueryBuilder, utilízalo. Si no, crea uno nuevo.
Debajo de return $queryBuilder
... ve a robar la lógica de ->orderBy()
de aquí arriba y... pega. ¡Impresionante!
// ... lines 1 - 60 | |
private function addOrderByVotesQueryBuilder(QueryBuilder $queryBuilder = null): QueryBuilder | |
{ | |
// ... lines 63 - 64 | |
return $queryBuilder->orderBy('mix.votes', 'DESC'); | |
} | |
// ... lines 67 - 78 |
PhpStorm está un poco enfadado conmigo... pero eso es sólo porque está teniendo una mañana dura y necesita reiniciarse: nuestro código está, esperemos, bien.
Vuelve al método original, simplifica a$queryBuilder = $this->addOrderByVotesQueryBuilder()
y no le pases nada.
// ... lines 1 - 45 | |
public function findAllOrderedByVotes(string $genre = null): array | |
{ | |
$queryBuilder = $this->addOrderByVotesQueryBuilder(); | |
// ... lines 49 - 58 | |
} | |
// ... lines 60 - 78 |
¿No es bonito? Cuando actualizamos... ¡no está roto! ¡Toma ese PhpStorm!
A continuación, vamos a añadir una página de "espectáculo de mezclas" en la que podamos ver una única mezcla de vinilos. Por primera vez, consultaremos un único objeto de la base de datos y nos ocuparemos de lo que ocurre si no se encuentra ninguna mezcla que coincida.
Hey SymfonyCast!
I had a question about querying images from the database. Would it be better to make URL requests for images statically from api's or is it better to query images from the database? And yes, I've learned that Doctrine queries the database in the background; with the focus on instantiating objects not tables.