Curso de Doctrine2. Por Juan David Rodríguez García (juanda@juandarodriguez.es)

Curso Doctrine2

1 Licencia        2

2 Documentación  oficial de Doctrine        2

3 El concepto        2

4 Configuración        2

5 Generación de las entidades        3

6 Creación del schema en la base de datos        4

7 Persistir objetos        4

8 Recuperación y modificación de objetos        6

9 Eliminación de objetos        6

10 El repositorio        7

11 Asociaciones entre objetos y mapeo en la base de datos        7

11.1 OneToOne unidireccional        9

11.2 OneToOne bidireccional        10

11.3 ManyToOne unidireccional        11

11.4 OneToMany-ManyToOne bidireccional        12

11.5 ManyToMany unidireccional        13

11.6 ManyToMany bidireccional        13

11.7 OneToMany unidireccional        14

11.8 Persistencia de las asociaciones        15

12 Las consultas DQL        16

12.1 Anatomía de una consulta DQL        16

12.1.1 Consultas de selección de objetos        16

12.1.2 Consulta para la actualización y el borrado de objetos        18

12.2 Ejemplo de borrado en bloque        18

12.3 Ejemplo de actualización en bloque        19

12.4 Consultas DQL en la acción        19

12.5 El constructor de consultas QueryBuilder        19

12.6 Los repositorios a medida        20

13 Generación mediante ingeniería inversa de los metadatos de mapeo        21

14 Generación de un formulario asociado a una entidad de Doctrine        22

15 Generación de módulos CRUD de Doctrine        23

16 LifeCicle Events        23

1 Licencia

La propiedad intelectual de esta obra pertenece a su autor: Juan David Rodríguez García (juanda@juandarodriguez.es)

Esta obra está bajo una Licencia Creative Commons Atribución-NoComercial-CompartirIgual 4.0 Internacional.

2 Documentación  oficial de Doctrine  

http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/index.html

3 El concepto

Los elementos del ORM:

4 Configuración

La configuración del ORM se lleva a cabo, como todo en Symfony, en el fichero app/config/config.yml. Aquí podemos configurar la capa de acceso a base de

datos (dbal) que admite los siguientes drivers:

Drivers para la configuración de DBAL:

Referencia de la documentación oficial de Symfony

Puedes ver que en el fichero de configuración se hace referencia a parámetros (referenciados con los signos %%) que se definen  en app/config/parameters.yml.

5 Generación de las entidades

Es necesario que la base de datos exista para que podamos usar las siguientes operaciones. Si no existe podemos crearla así:

# php bin/console doctrine:database:create

Generamos dos entidades: AppBundle:Usuario y AppBundle:Nota.

Para ello usamos el comando generador interactivo de entidades de Symfony2:

# php bin/console generate:doctrine:entity

Respondemos a sus preguntas que son:

Nota:  Las relaciones entre entidades no se pueden definir con esta herramienta.

El generador de entidades es muy cómodo pero limitado. Exige, por ejemplo, que las entidades deben "vivir" en el directorio Entity de algún bundle. Esto no es siempre lo que deseamos. Por ejemplo en el caso de que queramos organizar las entidades en distintos espacios de nombre. O si queremos que las entidades no pertenezcan a un bundle en particular. En tales casos simplemente tendremos que realizar alguna afinamiento manual.

Repasamos el código generado y lo completamos con nuestras necesidades. Por ejemplo es muy típico añadir (nullable="true"). En la documentación sobre el mapeo de entidades podemos encontrar todas las opciones.

Ya tenemos entidades y metadatos.

6 Creación del schema en la base de datos

Una vez que tenemos las entidades podemos crear  automáticamente las tablas asociadas mediante el comando:

# php bin/console doctrine:schema:create

Cuando queramos actualizar la base de datos porque hayamos realizado cambios en los metadatos o hayamos añadido más entidades usaremos:

# php bin/console doctrine:schema:update

Aún no hemos terminado, quedan las relaciones, pero ya podemos ver el funcionamiento básico del ORM.

7 Persistir objetos

Para persistir un objetos de alguna de nuestras entidades se utiliza el servicio de persistencia de Doctrine. Basta con pasarle como argumento el objeto que deseamos persistir al método persist del servicio. Es importante recordar que la persistencia no se hace efectiva hasta que no se llame al método flush() del servicio. Este último método debería llamarse una sola vez cuando hayamos "encolado" todas solicitudes de persistencia de nuestros objetos. Todas las operaciones de escritura se encolan hasta que se llama al método flush. Si hay un fallo se hace automáticamente un rollback.

/**
* @Route("/persistir", name="persistir")
*/

public function persistirAction() {
        $usuario =
new Usuario();
        $usuario->setNombre(
'Alberto');
        $usuario->setApellidos(
'Einstein');
        $usuario->setEmail(
'alberto@einstein.es');
        $usuario->setRoles(
'');
        $usuario->setUsername(
'alberto');
        $usuario->setPassword(
'einstein');

        $em =
$this->getDoctrine()->getManager();

        $em->persist($usuario);
        
// Antes de hacer la escritura en la base de datos el objeto no tiene id
        
echo 'ID (antes de persistir): ' . $usuario->getId() . '<br/>';
        dump($usuario);

        $em->flush();

        $usuarioFromDB = $em->getRepository(
'AppBundle:Usuario')->find($usuario->getId());

        
echo 'ID (después de persistir): '. $usuario->getId();
        dump($usuario);
        dump($usuarioFromDB);

        
return new Response('<html><body></body></html>');
}

        

Una observación importante es que el objeto no tiene asignado el atributo  id hasta que no es persistido en la base de datos mediante flush().   También es importante percatarse de que el objeto $usuario y el   objeto $usuarioFromDB son distintos aunque representan a la misma entidad:   el primero ha sido creado a partir del constructor de la clase y el segundo   ha sido creado a través de un servicio de Doctrine realizando una consulta a  la base de datos y creando el objeto con los datos obtenidos.

Transacciones explícitas

$em->getConnection()->beginTransaction(); // suspend auto-commit
try {
   
//... do some work
   $user =
new User;
   $user->setName(
'George');
   $em->persist($user);
   $em->flush();
   $em->getConnection()->commit();
}
catch (Exception $e) {
   $em->getConnection()->rollBack();
   
throw $e;
}

8 Recuperación y modificación de objetos

Para modificar un objeto existente, primero hay que recuperarlo de la base de datos. Una vez que lo tengamos lo podemos modificar mediante sus setters y persistirlo de nuevo de la misma forma que lo hicimos antes.

/**
* @Route("/modificar", name="modificar")
*/

public function modificarAction() {

        $em =
$this->getDoctrine()->getManager();

        $usuario = $em->getRepository(
'AppBundle:Usuario')->find(4);

        
echo $usuario;
        dump($usuario);

        $usuario->setApellidos(
'Rodríguez');

        
// El objeto ha cambiado
        
echo '<br/>';
        
echo $usuario;

        $em->persist($usuario);

        $em->flush();

        
echo $usuario;
        dump($usuario);

        
return new Response('<html><body></body></html>');
}

9 Eliminación de objetos

Se hace con el método remove del entity manager:

// $product se ha extraído de la base de dato mediante consulta

$entityManager->remove($product);
$entityManager->flush();

10 El repositorio

En el código que hemos propuesto para recuperar y modificar un objeto se utiliza un concepto de Doctrine denominado repositorio. Cada entidad tiene su repositorio, el cual no es más que una clase que contiene métodos para recuperar colecciones de objetos de esa entidad. Doctrine ofrece una serie de métodos muy útiles:

No obstante, es muy probable que nuestra aplicación tenga necesidades más sofisticadas para la consulta y recuperación de objetos. Podemos, entonces, ampliar el repositorio con nuestros propios métodos. Lo veremos más adelante.

11 Asociaciones entre objetos y mapeo en la base de datos

Las entidades de Doctrine admiten los siguientes tipos de relaciones entre objetos:

Cada una de estos tipos de relaciones puede ser:

Como en las bases de datos relacionales tan solo existe la relación ManyToOne, Doctrine mapea las relaciones de la siguiente manera:

Respecto a la direccionalidad, es un concepto propio del mundo de los objetos y no tiene cabida en el espacio de la base de datos, por lo que es algo que no se puede mapear.

De cara al mapeo de las asociaciones entre objetos en la base de datos y a la persistencia de los mismos, cuando la asociación es bidireccional, Doctrine denomina a uno de los objetos parte propietaria y al otro parte inversa. En el caso de asociaciones unidireccionales Doctrine tan sólo establece la parte propietaria.

Las reglas que hemos de tener en cuenta para reconocer la parte propietaria y la inversa son las siguientes:

En las asociaciones bidireccionales:

          correspondientes a la entidad posee la clave.

En los tres casos, la selección de la parte propietaria se realiza en los metadatos de mapeo.

Los tres tipos de asociaciones combinada con la direccionalidad de las mismas da lugar a los siguientes casos:

Aquí puedes encontrar la Documentación oficial sobre asociaciones

A continuación vamos a explicar cómo se especifican las relaciones unidireccionales y bidireccionales. Si quieres saber cómo se trabaja con las autoreferentes puedes consultar el documento anterior.

11.1 OneToOne unidireccional

Ejemplo: Asociación Persona-NIF, unidirecional desde persona hasta NIF, es decir, que será la entidad Persona la que implemente métodos para acceder al NIF, pero no al revés.

Primero debemos crear las entidades Persona y NIF, para lo que podemos usar el generador de entidades

# php bin/console generate:doctrine:entitity.

Añadimos a la entidad Persona un atributo $nif con los siguientes metadatos de mapeo:

        

/**
* @ORM\OneToOne(targetEntity=
"NIF")
* @ORM\JoinColumn(name=
"nif_id", referencedColumnName="id")
*
/
private $nif;

Y listo, no hay que tocar la entidad NIF para nada, puesto que la asociación es unidireccional desde Persona a NIF.

Como hemos modificado la entidad Persona hay que reconstruir la entidades para que se añadan los getters y setters del atributo $nif. Usamos

# php bin/console doctrine:generate:entities AppBundle

Como consecuencia de la esta operación se han creado los métodos setNIF() y getNIF().

También hay que actualizar la base de datos para que se establezca la correspondiente asociación entre las tablas.

# php bin/console doctrine:schema:update --force

Como consecuencia de esta operación se han creado (o actualizado) las tablas Persona y NIF, incorporando la primera una clave foránea nif_id hacia la tabla NIF.

11.2 OneToOne bidireccional

Ejemplo: Asociación Persona-NIF, bidirecional con Persona como parte propietaria, es decir, que será la entidad Persona la que posea la clave foránea.

Añadimos a la entidad Persona un atributo $nif con los siguientes metadatos de mapeo:

        

/**
* @ORM\OneToOne(targetEntity=
"NIF", inversedBy="persona", cascade={"persist"})
* @ORM\JoinColumn(name=
"nif_id", referencedColumnName="id")
*
/
private $nif;

Y en la entidad NIF añadimos un atributo $persona con los siguientes metadatos de mapeo:

        

/**
* @ORM\OneToOne(targetEntity=
"Persona", mappedBy="nif", cascade={"persist"})
*/
private
$persona;

Fíjate en que se ha añadido una opción inversedBy en la parte propietaria y una opción mappedBy en la parte inversa. Cada una de estas opciones indica el nombre del atributo que, en la otra parte, referencia a la entidad en cuestión.

También es importante percatarse de que es la parte propietaria la que define la clave foránea.

Al regenerar los getters y setters de estas entidades con:

# php bin/console doctrine:generate:entities AppBundle

comprobamos que en la entidad NIF aparecen los métodos getPersona() y setPersona(). Estableciéndose la bidireccionalidad.

Sin embargo, al actualizar la base de datos comprobamos que no sufre ningún cambio. La razón es que el concepto de direccionalidad es propio de las entidades, no de la base de datos. Esta última no "sabe" que significa una asociación uni o bidireccional.

11.3 ManyToOne unidireccional

Ejemplo: Persona-Dirección, de manera que en una casa pueden vivir varias personas, y una persona tan sólo puede vivir en una casa. Además queremos que Persona implemente los métodos de acceso a Dirección. Es decir que persona sea la parte propietaria.

Según hemos expuesto antes en las reglas que Doctrine aplica sobre cada una de las partes, en una relación OneToMany-ManyToOne, la parte ManyToOne es siempre la propietaria. Observa que es precisamente lo que sucede en este caso: Persona es la parte ManyToOne.

Primero debemos crear la entidad Direccion, para lo que podemos usar el generador de entidades

# php bin/console doctrine:generate:entity

Añadimos a la entidad Persona un atributo direccion con los siguientes metadatos de mapeo:

        

/**
* @ORM\ManyToOne(targetEntity=
"Direccion")
* @ORM\JoinColumn(name=
"direccion_id", referencedColumnName="id")
*

 */
private $direccion;

Y listo, no hay que tocar la entidad Direccion para nada, puesto que la asociación es unidireccional desde Persona a Direccion.

Reconstruimos las entidades y observamos que aparecen en la entidad Persona los métodos getDireccion() y setDireccion()..

Al actualiza la base de datos, además de crearse la tabla Direccion, se añade una clave foránea direccion_id a la tabla Persona, que la relaciona con la tabla Direccion.

11.4 OneToMany-ManyToOne bidireccional

Ejemplo: Persona-Dirección, de manera que en una casa puedan vivir varias personas, y una persona tan sólo pueda vivir en una casa. Además queremos que tanto Persona como Direccion implementen métodos de acceso, es decir, que la asociación sea bidireccional.

Añadimos a la parte propietaria (Persona) el atributo $direccion con los siguientes metadatos:

        

/**
* @ORM\ManyToOne(targetEntity=
"Direccion", inversedBy="personas", cascade={"persist"})
* @ORM\JoinColumn(name=
"direccion_id", referencedColumnName="id")
*
*
/
private $direccion;

Es igual que en el caso unidireccional pero especificando el atributo inversedBy, es decir el nombre del atributo en la otra parte (Direccion) que hace referencia a la clase Persona.

En la clase Direccion añadimos el atributo $personas con los siguientes metadatos:

     

/**
* @ORM\OneToMany(targetEntity="Persona", mappedBy="direccion",
cascade={"persist"})
*

 */
private $personas;

La opción mappedBy indica el nombre del atributo en la otra parte (Persona) que hace referencia a la entidad Direccion.

Regeneramos las entidades y aparecen en la clase Direccion los métodos addPersona() y getPersonas().

Sin embargo al actualizar la base de datos no se produce ningún cambio. Recuerda, la base de datos no sabe nada acerca del concepto de direccionalidad.

11.5 ManyToMany unidireccional

Ejemplo: Usuario-Grupo, de manera que un usuario pueda pertenecer a varios grupos y un grupo pueda contener varios usuarios. Queremos que la relación sea unidireccional siendo Usuario la entidad que implemente los métodos de acceso a Grupo.

En la entidad Usuario añadimos el atributo $grupos con los siguientes metadatos de mapeo:

        

/**
* @ORM\ManyToMany(targetEntity=
"Grupos")
* @ORM\JoinTable(name=
"usuario_grupo",
*          joinColumns={@ORM\JoinColumn(name=
"usuario_id", referencedColumnName="id")},
*          inverseJoinColumns={@ORM\JoinColumn(name=
"grupo_id", referencedColumnName="id")}
*          )
*

 */
private $$grupos;

En la entidad Grupo no hacemos nada puesto que la relación en unidireccional.

Generamos las entidades y aparecen en la entidad Usuario los métodos addGrupo() removeGrupo() y getGrupo(). Y cuando actualizamos la base de datos aparece una tabla intermedia que implementa la relación ManyToMany con claves foráneas hacia las tablas Usuario y Grupo.

11.6 ManyToMany bidireccional

Ejemplo: Usuario-Grupo, de manera que un usuario pueda pertenecer a varios grupos y un grupo pueda contener varios usuarios. Queremos que la relación sea bidireccional. Es decir, que Grupo también implemente los métodos de acceso a Usuario.

Debemos añadir al atributo $grupos de la entidad Usuario la opción inversedBy en sus metadatos, indicando el nombre del campo en la entidad Grupo que invierte la relación:

         

/**
* @ORM\ManyToMany(targetEntity=
"Grupo", inversedBy="usuarios", cascade={"persist"})
* @ORM\JoinTable(name=
"usuario_grupo",
*          joinColumns={@ORM\JoinColumn(name=
"usuario_id", referencedColumnName="id")},
*          inverseJoinColumns={@ORM\JoinColumn(name=
"grupo_id", referencedColumnName="id")}
*          )
*
*
/
private $grupos;

Y en la entidad Grupo añadimos el atributo ``$usuarios`` con los siguientes metadatos:

        

/**
* @ORM\ManyToMany(targetEntity=
"Usuario", mappedBy="grupos", cascade={"persist"})
*/
private
$usuarios;

Generamos las entidades y aparecen en la entidad Pelicula los métodos ``addGrupo()``, removeGrupo()y getGrupo().

Por otro lado, al actualizar la base de datos no ocurre ningún cambio. De nuevo, la direccionalidad no es algo que incumba a la base de datos.

11.7 OneToMany unidireccional

Ejemplo: Persona-Telefono, de manera que una persona tiene varios teléfonos, pero cada teléfono sólo pueda pertenecer a una persona. La parte propietaria será persona.

Este caso plantea un problema: las reglas de Doctrine que hemos mencionado más arriba declaran rotundamente que en una relación OneToMany-ManyToOne la parte propietaria es la ManyToOne. El caso que estamos tratando establece como parte propietaria la parte OneToMany. Así que, en principio, Doctrine no resuelve este caso.

La solución que propone Doctrine es utilizar, en lugar de una asociación OneToMany-ManyToOne, una ManyToMany en la que se establezca a única la clave foránea que apunta a la parte inversa (Many). Así, dado que en una asociación ManyToMany podemos elegir cualquier entidad como parte propietaria, queda resuelto el problema. Veámos como se hace en la práctica.

Creamos la entidad teléfono.

Añadimos a la entidad Persona un atributo $telefonos con los siguientes metadatos de mapeo:

        

/**
* @ORM\ManyToMany(targetEntity=
"Telefono", cascade={"persist"})
* @ORM\JoinTable(name=
"persona_telefono",
*          joinColumns={@ORM\JoinColumn(name=
"persona_id", referencedColumnName="id")},
*          inverseJoinColumns={@ORM\JoinColumn(name=
"telefono_id", referencedColumnName="id", unique=true)}
*          )
**
/
private $telefonos;

Dado que la asociación es unidireccional, no hay que tocar la parte no propietaria (teléfono).

Regeneramos las entidades y actualizamos la base de datos. Observamos que en la entidad Persona aparecen los métodos addTelefono() (singular), removeTelefono() (singular) y getTelefonos() (plural). Además, en la base de datos se ha creado la tabla Telefono y una tabla intermedia persona_telefono con claves foráneas a Persona y

Telefono. Además la clave foránea a Teléfono debe ser única, para garantizar la relación OneToMany desde Persona a Telefono.

Observa también que se ha creado un constructor de la clase Persona, en el que se inicializa el atributo telefonos como un ArrayCollection, que es un tipo de array propio de Doctrine que soporta ciertas operaciones que el ORM necesita:

        

public function __construct()
{
   
$this->telefonos = new \Doctrine\Common\Collections\ArrayCollection();
}

11.8 Persistencia de las asociaciones

En la documentación oficial:

Doctrine deja muy claro que ante cambios (inserciones, actualizaciones y borrados) tan sólo se chequeará la parte propietaria de la asociación.

Lo que significa que al utilizar el método persist() del Entity Manager, únicamente los cambios que se hayan realizado con los setters (set{Propiedad}, add{Propiedad}, remove{Propiedad}) de la entidad propietaria serán efectuados la base de datos.

En el caso de las asociaciones unidireccionales no hay ningún problema pues los únicos setter que existen son los de la parte propietaria, pero si la asociación es bidireccional debemos asegurarnos de usar dichos métodos si deseamos persistir en la base de datos. Además, si queremos mantener en memoria la bidireccionalidad, también debemos utilizar los setters de la otra parte.

Ilustrémoslo con un ejemplo. Si $persona es un objeto de la clase Persona, y $direccion lo es de la clase Direccion, y queremos asociar la dirección a la persona, la manera correcta de hacerlo es:

        $persona->setDireccion($direccion);  

        $direccion->addPersona($persona);    

Cuando recuperamos objetos desde la base de datos a través del mecanismo de consulta de Doctrine (que veremos en el siguiente apartado), todos los métodos getters de las entidades, tanto de la parte propietaria como de la inversa, funcionan perfectamente. Lo anterior tiene que ver únicamente con las operaciones de persistencia (modificación/inserción).

12 Las consultas DQL

La recuperación de objetos filtrando por sus propiedades se realiza en Doctrine mediante un lenguaje de consulta sobre el modelo de objetos denominado DQL (Doctrine Query Language). Cuando se utiliza DQL es importante pensar en un repositorio de objetos en el que estamos filtrando un subconjunto en lugar de hacerlo en tablas y relaciones en una base de datos.

12.1 Anatomía de una consulta DQL

12.1.1 Consultas de selección de objetos

Ejemplo. "Dame una colección de objetos Film en las que participen actores cuyo nombre case con el patrón '%JOHN%' y cuyo año de producción sea posterior a 1980":

SELECT f FROM AppBundle:Film f JOIN p.actors a where a.firstName LIKE '%JOHN%' AND f.releaseYear > 1980

El resultado es un array de objetos (``FROM``)

FROM AcmeDemoBundle:Film f

El resultado de lanzar una consulta DQL sobre el repositorio de objetos, es un array de objetos del tipo especificado por la cláusula FROM . De manera que esta define la raíz de la consulta. Dicha cláusula FROM siempre va seguida del nombre de una entidad y del alias que le asignamos. En el ejemplo anterior, cuando ejecutemos la consulta, obtendremos un array de entidades AppBundle:Film.

Las asociaciones se especifican con ``JOIN``

JOIN f.actors a

Mediante la palabra clave ``JOIN`` podemos especificar las asociaciones que vayamos a necesitar en la definición del filtrado. ``JOIN`` va seguida de un atributo que es un objeto (o colección de objetos) asociado a otro objeto especificado por su alias y del alias que le

asignamos a este nuevo objeto.

En el ejemplo anterior f es el alias de la entidad AppBundle:Film, actors es una colección que está asociada a dicha entidad, y a es el alias que damos a los objetos de esta asociación, es decir, a los actores.

Establecemos filtros con ``WHERE``

WHERE a.name LIKE '%JOHN%' AND f.releaseYear > 1980

Una vez que tenemos alias para todos los objetos por cuyos atributos queremos filtrar, simplemente establecemos los filtros usando dichos alias para referirnos a los objetos.

Los objetos no se construyen completamente (lazzy loading y ``SELECT``)

SELECT f

Como ya hemos estudiado, los objetos pueden estar asociados con otros objetos de varias maneras (OneToOne, ManyToMany ...). Esto significa que construir completamente en memoria un sólo objeto significa construir todos los que estén asociados con él. Esto puede repetirse de una manera recursiva, pues a su vez un objeto asociado puede tener otros objetos asociados y así sucesivamente. La cantidad de objetos que se crearían dependerá de la complejidad del modelo de objetos.

Por tanto, la ejecución de una consulta DQL podría producir como resultado un array excesivamente grande si no fuese gracias a la aplicación del concepto de lazzy loading. Este consiste en construir parcialmente los objetos seleccionados por la consulta, de manera que los atributos correspondientes a las asociaciones con otros objetos se dejan vacíos. Si en un momento dado se piden a través de un getter del objeto, entonces se realiza una nueva consulta a la base de datos para construirlo.

Con la palabra reservada SELECT indicamos los objetos de la asociación que queremos recuperar de un tirón desde la base de datos (sin lazzy loading). En el ejemplo de DQL que encabeza esta sección, se está pidiendo únicamente los objetos películas. De manera que en la construcción en memoria de los mismos se rellenarán los atributos escalares pero no las asociaciones.

Si cambiamos la cláusula SELECT f por SELECT f,a, la consulta se realizaría de manera que en una única query a la base de datos se obtendrían los datos suficientes para construir en memoria los objetos película y los objetos actores asociados. De esta manera, cuando se utilice el método getActors() no se vuelve a consultar a la base de datos.

Cuando indicamos en el SELECT más de un tipo de objeto, se dice que estamos realizando un fetch join, mientras que cuando sólo especificamos uno, se dice que realizamos un regular join.

Por tanto la cláusula SELECT nos permite controlar hasta qué punto aplicar el lazzy loading en la construcción de objetos. Cuantos más objetos asociados queramos construir en memoria "del tirón", más necesidades de memoria tendremos. Pero si necesitamos acceder a muchos de los objetos asociados estaremos ahorrando en número de consultas (provocadas por el lazzy loading) y, por tanto en tiempo de ejecución, pues las operaciones de entrada/salida son costosas en tiempo.

Para decidir cuántos objetos hemos de recuperar de una vez (fetch), en una consulta, deberíamos analizar si, cuando dispongamos de los objetos solicitados, vamos a acceder algunos de sus objetos asociados con frecuencia. Si es así, la opción más eficiente es la de realizar un fetch join.

12.1.2 Consulta para la actualización y el borrado de objetos

También podemos utilizar DQL cuando queramos borrar o actualizar varios objetos de una sola vez, como alternativa a realizar un bucle en el que se llame al método remove() o persist() del entity manager.

Por otro lado, las consultas de inserción de nuevos objetos no se pueden hacer en DQL. Sólo se pueden hacer con el método persist.

12.2 Ejemplo de borrado en bloque

DELETE AppBundle:Film f WHERE f.title LIKE '%TIEMPO%'

12.3 Ejemplo de actualización en bloque

UPDATE AppBundle:Film f SET f.length=180 WHERE f.titulo LIKE '%METROPO%'

12.4 Consultas DQL en la acción

La forma más directa de realizar una consulta DQL es usando la función createQuery() del Entity Manager en la propia acción.

        

public function indexAction(){

           $em =
$this->getDoctrine()->getManager();
                   
           $dql = <<< DQL
select f,a from AppBundle:Film f JOIN f.actor a where a.firstName LIKE :firstname
AND f.releaseYear > :year
DQL;
           
           $query = $em->createQuery($dql)->setParameters([
               
'firstname' => '%JOHN%',
               
'year' => '1980'
           ]);
           

Usando el profiler de Symfony2 sobre dicha acción y modificando la consulta DQL puedes comprobar la diferencia entre realizar el fetch join y no hacerlo. La plantilla con la que se pinta dicha acción realiza un bucle para pintar todas las películas con sus actores obtenidas por la consulta $queryPeliculas2. Por ello, cuando se utiliza un regular join, es decir, cuando se usa el lazzy loading, se realizan más de 80 consultas a la base de datos. Sin embargo, si se utiliza un fetch join haciendo SELECT p,a, Se realiza sólo una consulta, disminuyendo considerablemente el tiempo de ejecución.

12.5 El constructor de consultas QueryBuilder

Doctrine incorpora un servicio denominado *QueryBuilder* que ofrece una elegante interfaz orientada a objetos para construir consultas DQL. Veamos cómo se realizaría la anterior consulta con el QueryBuilder:

        

public function indexAction(){

           $em =
$this->getDoctrine()->getManager();
           
           $queryActores = $em->createQueryBuilder()
               ->select(
'a')
               ->from(
'AppBundle:Actor', 'a')
               ->where(
'a.nombre LIKE :patron')
               ->setParameter(
'patron', 'A%')
               ->getQuery();
           
           $actores = $queryActores->getResult();

           
// Manejar el array $actores
           ...

        }

12.6 Los repositorios a medida

Aunque realizar consultas directamente en la acción funciona perfectamente, no es lo más adecuado. Entre otras cosas por que no podemos reutilizarlas en otras acciones a menos que las dupliquemos, lo cual atenta contra las buenas prácticas de programación.

El lugar correcto donde debemos construir las consultas DQL es el repositorio de cada entidad. Cada consulta que queramos hacer la encapsulamos en una nueva función del repositorio. Después podemos utilizar dichas funciones desde cualquier parte de la aplicación donde tengamos acceso al Entity Manager de Doctrine, por ejemplo desde una

acción.

Las clases repositorios se declaran en el mismo espacio de nombre que las entidades asociadas. Si se utiliza el generador de entidades del comando console, podemos generar el esqueleto de estas clases para rellenarlo cuando lo necesitemos.

A continuación mostramos un ejemplo de clase repositorio.

        

namespace Acme\DemoBundle\Entity;

use Doctrine\ORM\EntityRepository;

class ActorRepository extends EntityRepository {

           
public function findByNombreLike($patron) {
               
               
// Todos los actores cuyo nombre es como $patron
               $queryActores = $em->createQueryBuilder()
               ->select(
'a')
               ->from(
'AppBundle:Actor', 'a')
               ->where(
'a.nombre LIKE :patron')
               ->setParameter(
'patron', 'A%')
               ->getQuery();

               
return $queryActores->getResult();
           }          
}

Y así es como lo utilizamos en una acción:

        

public function indexAction(){
           $em =
$this->getDoctrine()->getManager();

           $repoActores =
$this->getDoctrine()
                         ->getRepository(
'AppBundle:Actor');

           $actores = $repoActores->findByNombreLike(
"%JOHN%");

           
// Manejar el array $actores
           ...

En la acción AppBundle:Default:dqlEnRepositorioAction, cuya ruta es doctrine/dql_en_repositorio, puedes comprobar como funcionan las consultas DQL en los repositorios.

13 Generación mediante ingeniería inversa de los metadatos de mapeo

En muchas ocasiones el desarrollo de una aplicación está condicionado a la existencia de una base de datos existente. En tal caso debemos confeccionar las entidades y sus metadatos de mapeo de manera que sean compatibles con dicha base de datos.

Doctrine ofrece una herramienta muy útil que, en más del 70% de los casos, ofrece una solución a este problema de ingeniería inversa. Aunque los metadatos generados por dicha herramienta no siempre sean completamente correctos, suponen un buen punto de partida que nos ahorra tiempo y neuronas.

# php app/console doctrine:mapping:import "AppBundle" annotation

Cuando utilicemos esta herramienta hemos de tener en cuenta que existen tipos propios de cada sistema gestor de base de datos que Doctrine no "sabe" como mapear a la inversa (desde la base de datos al metadato). En tal caso la herramienta de ingeniería inversa fallará. La solución a este problema es indicar en la configuración del servicio Doctrine como

deben mapearse dichos tipos.

Un ejemplo típico es el tipo ``enum`` de MySQL. Doctrine no "sabe" cómo debe mapearlo, pero nosotros podemos ayudarle añadiendo al fichero ``app/config/config.yml`` las siguientes líneas:

        doctrine:
           dbal:
               driver:  
%database_driver%
               
...
               mapping_types:
                         enum:
string             

14 Generación de un formulario asociado a una entidad de Doctrine

En muchas ocasiones necesitaremos generar formulario (tipos) asociados a las entidades de nuestra aplicación. Obviamente podemos generarlos manualmente según lo aprendido en el tema de los formularios. Pero la herramienta console nos ofrece un comando que nos asiste automáticamente en tal tarea:

# php app/console doctrine:generate:form AppBundle:Actor

Si fuese necesario siempre podemos "tunear" el tipo generado para adaptarlo a las necesidades de la aplicación.

15 Generación de módulos CRUD de Doctrine

Añadir (Create), recuperar (Retrieve), actualizar (Update) y borrar (Delete) entidades es un problema que surge con frecuencia en el desarrollo de aplicaciones que manejen datos persistentes.

Otro útil comando de la herramienta console nos genera automáticamente código totalmente funcional para realizar dichas operaciones, conocidas como CRUD (las primeras letras de cada operación).

# php app/console generate:doctrine:crud

Se trata de un comando interactivo que pregunta al usuario los datos que necesita para la construcción del código del módulo, que consiste en:

Si fuese necesario siempre podemos "tunear" el tipo generado para adaptarlo a las necesidades de la aplicación.

16 LifeCicle Events

Documemtación oficial Doctrine2

Documentación oficial Symflony3