Efectiva carga de datos en JPA con Entity Graph

JPA 2.1 ha incluido la característica Entity Graph, y es de gran utilidad para cuando debemos de cargar los datos de una relación en nuestra consulta de una manera diferente a la mapeada en la entidad.

Entendamos las relaciones

Cuando hacemos un mapeo de una relación entre 2 tablas en JPA, hacemos uso de las anotaciones OneToOne, OneToMany, ManyToOney ManyToMany. Dichas anotaciones tienes un tipo de carga por defecto que puede ser Eagero Lazy, es decir.

AnotaciónFetch Type por defecto
OneToOneEager
ManyToOneEager
OneToManyLazy
ManyToManyLazy

El fetchType de los atributos que mapean una relación puede ser modificado, usando el atributo fetchType de la anotación y de esa manera dar un comportamiento diferente al por defecto. Sin embargo, no es recomendable cambiar dicha carga si no es estrictamente necesario ya que puede derivar en un rendimiento no ópitmo en las consultas a base de datos.

Por ejemplo, si tenemos una relación OneToMany entre dos clases

@Entity
public class Student {
   private long id;
   private String name;
   @ManyToOne
   @JoinColumn(name = "school_id")
   private School school;
}

@Entity
public class School {
   private long id;
   @OneToMany(mappedBy = "school")
   private List<Student> students;
}

En este caso, a obtener un Student JPA se va a encargar de obtener los respectivos datos de School para dicho estudiante de manera automática, esto debido a que la relación es del tipo ManyToOne. Por otro lado, al obtener un dato de School no vamos a cargar automáticamente todos sus estudiantes, estos serán obtenidos en una consulta por aparte mientras dicho elemento de School este asociado al “contexto” de JPA. Si el objeto ya no está asociado, entonces al llamar al método School.getStudents() vamos a obtener una excepción LazyInitializationException.

Debido a esa excepción, nos podemos ver tentados a cambiar el atributo fetchType de la anotación OneToMany por FetchType.Eager. De esta manera al obtener un registro School ya se obtiene la lista de estudiantes con datos, pero es aquí donde el rendimiento de la aplicación comienza a ser impactado, posiblemente con unos pocos datos no lo notaremos, pero si nos enfrentamos a cientos de School cada uno con miles de Student lo notaremos de inmediato.

Para cada registro de School JPA va a realizar una consulta extra para obtener el listado de Student, estaremos entonces realizando N+1 accesos a la base de datos. Ahora bien, este sería el comportamiento por defecto, no importaría si lo que se necesita es solamente un registro de Schoolo una lista con cientos de registros. Posiblemente en la mayoría de estos usos desde el punto de vista de lógica de negocio solo se necesite la información de la clase School y no la lista de Student. Es decir, se hizo una carga innecesaria y además ineficiente de datos.

Entity Graph al rescate

Entity Graph es una caracteristica agregada en JPA 2.1, básicamente nos permite sobreescribir el FetchType.LAZY de la relación para cargar tempranamente los dichos datos cuando el EntityGraphes aplicado en una consulta. Por defecto la entidad seguirá cargando datos de manera Lazy a no ser que la consulta explicitamente utilice el EntityGraph.

Definiendo el Entity Graph

Se puede hacer de dos maneras, programáticamente y por anotaciones. La manera más sencilla es por anotaciones, y en mi opinión la más eficiente, ya que dicho Entity Graph es creado una vez al iniciar la aplicación y queda disponible para futuros usos.

@Entity
@NamedEntityGraph( name = "School.students",
                   attributeNodes = {
                     @NamedAttributeNode("students")
                   })
public class School {
   private long id;
   @OneToMany(mappedBy = "school")
   private List<Student> students;
}

Hacemos uso entonces de la anotación @NamedEntityGraph , declaramos un nombre, y los atributos que queremos cargar de forma inmediata (en lugar de lazy) cuando la utilicemos.

Ahora bien, si lo queremos hacer de manera programada, entonces podemos usar lo siguiente:

EntityGraph<?> entityGraph = entityManager.createEntityGraph("School.students");
entityGraph.addAttributeNodes("students");

Usando el Entity Graph

Ya tenemos el EntityGraph definido, ahora es momento de utilizarlo en nuestra consulta, para lo que haremos uso del hint javax.persistence.loadgraph o javax.persistence.fetchgraph . Más adelante explicaré la diferencia entre ambas opciones.

EntityGraph<?> entityGraph = entityManager.getEntityGraph("School.students");

List<School> result = entityManager
  .createQuery("select s from School")
  .setHint("javax.persistence.fetchgraph", entityGraph)
  .getResultList();

En el caso de estar usando SpringData, se puede hacer uso de la anotación @EntityGraph , en donde se indica el nombre del entity graph creado previamente, y en tipo de cargar Fetch o Load.

@EntityGraph(type=EntityGraphType.FETCH, value="School.students")
public List<School> findBySchoolName(String name);

Fetch vs Load

Nos queda aún una duda, qué implica usar fetchgraph o usar loadgraph al realizar la consulta con un entity graph. Cabe indicar que en el caso de la anotación @EntityGraph utilizada en SpringData, tiene por defecto FETCH como tipo.

  • Fetch: Carga de manera inmediata solamente aquellos atributos indicados en la entidad. Es decir, que si la entidad tiene más atributos de relación que cargan de manera EAGER, estos serán cargados de manera LAZY.
  • Load: Mantiene el tipo carga existente en las relaciones de la entidad, y solamente modifica a EAGER aquellos atributos indicados en el entity graph.

Conclusión

Ahora que conocemos de la existencia de los Entity Graph, y entendemos el funcionamiento de los FetchType.EAGER y FetchType.LAZY, podemos emplearlos de manera adecuada para proporcionar un uso eficiente a nuestras consultas a base de datos, evitar los problemas N+1 en nuestras consultas cuando tenemos entidades que se relaciones con otras entidades.

Leave a Reply

Your email address will not be published. Required fields are marked *