Doctrine LAZY loading, EAGER y EXTRA_LAZY

Se explica el uso de las diferentes estrategias que tiene doctrine para obtener registros con asociaciones a través de un mini-proyecto Symfony2,que además de probar todo el código podemos ver todos los detalles de las consultas gracias al web profiler.

Doctrine tiene 3 estrategias para recuperar registros de entidades con asociaciones (Lazy, EAGER y Extra Lazy), los tipos se establecen en el valor del atributo fetch de la asociación.

doctrine fetch

Lazy

Al hacer una consulta de la entidad principal, los registros de la entidad asociada no serán cargarán hasta que sean llamados, una vez llamados generan automáticamente consultas extras para cargar los registros asociados. Lazy es usado por defecto.

Ejemplo:
Una asociación de User OneToOne InfoUser definida como LAZY

user-infouser

Entidad User

class User
{
   ...
   /**
   @ORM\OneToOne(targetEntity="InfoUser", fetch="LAZY")
   */
   private $infoUser;
   ...
}

Controlador User

/**
* @Route("/users-lazy/", name="listAllUserLazy")
*/
public function listAllUserLazyAction()
{
    $users = $this->getDoctrine()->getRepository('AppBundle:User')->findAll();
    return $this->render('user/users-lazy.html.twig', array(
       'users' => $users
    ));
}

Sin llamar ninguna propiedad de la entidad asociada

Vista de User

{% extends 'base.html.twig' %}
{% block body %}
    {% for user in users %}
        <p>{{ user.name }}</p>
    {% endfor %}
{% endblock %}

Resultado navegador [Atención al Web Profiler]:
Muestra a todos los usuarios [100 users en mi BD] con una sola consultalazy loading doctrine symfony

Llamando a una propiedad de la entidad asociada

Vista de User

{% extends 'base.html.twig' %}
{% block body %}
    {% for user in users %}
        <p>
            {{ user.name }}: {{ user.infoUser.description }}
        </p>
    {% endfor %}
{% endblock %}

Resultado [Atención al Web Profiler]:
Muestra a todos los usuarios  [100 users en mi BD] con una sola consulta, pero hace una consulta extra por cada usuario para obtener la descripción.

Captura de pantalla 2015-11-13 a las 14.32.51

A continuación vemos como la primera consulta es para obtener todos los usuarios de la entidad User y a partir de ahí, realiza una consulta por cada registro de User. [1 consulta de todo + 1 consulta por cada usuario (100)]

Captura de pantalla 2015-11-13 a las 14.38.06

EAGER

Al hacer una consulta de la entidad principal, los registros de la entidad asociada serán también cargarán (JOINs).

Ejemplo:
Entidad User

class User
{
   ...
   /**
   @ORM\OneToOne(targetEntity="InfoUser", fetch="EAGER")
   */
   private $infoUser;
   ...
}

Resultado [Atención al Web Profiler]:

Lista todos los usuarios con su descripción en una sola consulta.

doctrine - EAGER loading - symfony

A continuación vemos la única consulta realizada utilizando un LEFT JOIN a la entidad asociada InfoUser

Captura de pantalla 2015-11-13 a las 14.56.29

EXTRA_LAZY

Cargan los datos de la entidad principal y permite realizar algunas operaciones de Collection de la entidad asociada, además, no implica la carga total de la asociación. Esta estrategia “puede no reducir* el número de consultas a la base de datos pero si reduce la memoria utilizada por el sistema.

En los siguientes ejemplos vamos a explicar la frase anterior “Puede no reducir el número de consultas“:

A partir de la siguiente asociación de Article OneToMany Comment definida como EXTRA_LAZY

EXTRA-LAZY DIAGRAMA DOCTRINE

Tenemos un controlador que muestra todos los comentarios de un determinado artículo y además realiza una serie de operaciones como calcular el total de comentarios que tiene el articulo utilizando el método de collection, recorrer la collection y guarda los comentarios en un array, etc. [Leer comentarios en el código]

public function listArticleExtraLazyAction(Article $article)
{
    $article = $this->getDoctrine()->getRepository('AppBundle:Article')->find(array('id' => $article->getId()));

    $collection = $article->getComments(); // no hace nada

    $totalItems = $collection->count(); //Provoca una consulta COUNT(*) y no carga la colección
    $totalItems2 = $collection->count(); //Provoca una consulta COUNT(*) y no carga la colección
    //el foreach provoca una consulta y SI carga la colección, a partir de la carga, doctrine no necesita realizar ninguna consulta adicional.
    foreach($collection as $comment) {
        $comments[] = $comment; 
    }

    $totalItems3 = $collection->count(); //No genera una nueva consulta, por que cargó la colección

    return $this->render('articles/article.html.twig', array(
        'article' => $article,
        'totalComment' => $totalItems
    ));
}

Resultado [Atención al Web Profiler]:

extra_lazy1

A continuación vemos las 4 consultas que ha realizado que corresponden con lo mencionado en los comentarios del controlador anterior.

Captura de pantalla 2015-11-13 a las 17.34.54

Conclusión EXTRA_LAZY:

Si necesitamos algo como lo expuesto anteriormente, es importante realizarlo con un orden:

  1. Provocamos la consulta y que cargue la colección.
  2. Llamamos a los métodos cuantas veces necesitamos sin influir en consultas adicionales.

Podemos observarlo a continuación:

Controller

/**
 * @Route("/article/{id}", name="listArticleExtraLazyOrder")
 */
public function listArticleExtraLazyOrderAction(Article $article)
{
    $article = $this->getDoctrine()->getRepository('AppBundle:Article')->find(array('id' => $article->getId()));

    $collection = $article->getComments(); // no hace nada

    //el foreach provoca una consulta y SI carga la colección, a partir de la carga, doctrine no necesita realizar ninguna consulta adicional.
    foreach($collection as $comment) {
        $comments[] = $comment;
    }

    $totalItems = $collection->count(); //No genera una nueva consulta, porque cargó la colección
    $totalItems2 = $collection->count(); //No genera una nueva consulta, porque cargó la colección

    return $this->render('articles/article.html.twig', array(
        'article' => $article,
        'totalComment' => $totalItems
    ));
}

Resultado [Atención al Web Profiler]:

Captura de pantalla 2015-11-13 a las 17.48.07

extra-lazy-order

✔︎ Puedes acceder al código en Github – DoctrineLab

Recursos:

Agradecimientos:

Quiero agradecer a Javier y Manuel por la ayuda prestada a una consulta referente a EXTRA_LAZY en el foro de libros web. Si habeis llegado hasta aquí os recomiendo leer toda la conversación donde muestra algunas cosas muy interesantes. Y animo a seguir aportando más detalles.

Articulo añadido a “A week of Symfony”

Autor: Juan Luis García Borrego

Soy una persona responsable, activa, constante, con muchas ganas de aprender y crecer como profesional. Tengo un carácter positivo y agradable que me permite sentirme cómodo trabajando en equipo. Mis intereses profesionales se centran en torno a PHP, en concreto al framework Symfony y su ecosistema, en el cual me siento muy cómodo desarrollando y es un claro ejemplo de un software de calidad, con buenas prácticas y ‘Clean Code’.

7 opiniones en “Doctrine LAZY loading, EAGER y EXTRA_LAZY”

  1. Hola Juan Luis,

    He llegado aquí porque tengo un problema relacionado con esto. Lo sorprendente es que aquí he aprendido que doctrine tiene el fetch mode en LAZY, pero, en mi aplicación no hace demasiado caso, la verdad.

    Tengo un par de entidades:
    – Evento, además de otras cosas, un evento puede tener muchos Tickets, tiene una asociación así: * @ORM\OneToMany(targetEntity=”Ticket”, mappedBy=”event”, fetch=”LAZY”)
    – Ticket

    Estoy construyendo un API con FOSRestBundle y otras mandangas y para ello tengo un método súper sencillo:

    $event = $em->getRepository(‘AppBundle:Event’)->findOneBy([
    ‘id’ => $id
    ]);

    Según lo que yo esperaba, sólo debería devolverme la info de la entidad padre Event y nada de los hijos (Ticket), pero la API me devuelve los tickets:
    [
    {
    “id”: 1,
    “name”: “Sala1”,
    “slug”: “sala1”,
    “date”: “2017-01-31T22:00:00+0100”,
    “date_tms”: “1485733573”,
    “active”: true,
    “created_at”: “2017-01-29T17:33:00+0100”,
    “tickets”: [
    {
    “id”: 1,
    “locator”: “ram1”,
    “active”: true,
    “created_at”: “2017-01-29T17:34:00+0100”
    },

    Perdón el atraco!
    ¿lo que está pasando es normal? ¿Será cosa de FOSRestBundle?

    Mil gracias

  2. Buenas tardes,

    Yo tengo el mismo problema de rendimiento quiero crear asociaciones con EXTRA_LAZY y que solo me ejecute esas colecciones a petición, pero cuando ejecuto el getQuery()->getResult() me carga la colección del oneToMany, la única manera de que no me la cargue es ejecutándolo con Query::HYDRATE_ARRAY, pero no me vale yo necesito tener acceso a otros objetos que no son la colección.

    Ademas utilizo el bundle Pagerfanta para paginación y JMSSerializer para serialización.

    El caso es que hay una propiedad con una colección hija que solo quiero que se filtre mediante un queryparam del API Rest llamado fields y entonces es cuando lo devuelva el JSON.

    Si me puedes echar un cable te lo agradecería.

    Un saludo,

    1. Perdona ya esta solucionado es que tenia un postLoad que utilizaba para ordenar la colección y era lo que me fastidiaba el performance.
      Gracias.

  3. hola tengo una DB en MySQL y tengo dos tablas una edicion y otra archivos, como puedo hacer para contar el numero de archivos que pertenecen a cada edicion, utilizando EXTRA_LAZY?

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *