Curso Jakarta EE 9 (28). JPA con Hibernate (11): Relaciones: obtención eager\lazy, bytecode enhancement

logo Jakarta EE

En JPA, los atributos y relaciones que forman parte de una entidad se obtienen de la base de datos de dos maneras: en el momento en que la recuperemos (eager loading, carga inmediata o ansiosa) o bien cuando se accede a ellos por primera vez (lazy loading, carga perezosa), si es que esto llega a suceder.

>>>> ÍNDICE <<<<

Carga perezosa

La segunda técnica es un concepto de programación genérico. JPA lo propone -su funcionamiento no es el mismo en todas las implementaciones- para mejorar el rendimiento de la explotación de la base de datos siguiendo un principio que parece evidente (y que debemos seguir siempre): los datos que no necesitemos, mejor no obtenerlos. Tenemos que comprender el funcionamiento de este mecanismo en Hibernate para usarlo con responsabilidad. A ello está dedicado el presente capítulo.

Recordemos el ejemplo de las facturas del capítulo anterior.

@Entity
@Table(name = "invoices")
@Getter
@Setter
public class Invoice {
	
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, name = "date_time")
    private LocalDateTime dateTime;

    @OneToMany(mappedBy = "invoice", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<InvoiceItem> items;
@Entity
@Table(name = "invoices_items")
@Getter
@Setter
public class InvoiceItem {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;

	@ManyToOne(fetch = FetchType.LAZY)
	private Invoice invoice;
}

Veamos un código de lo más inocente que oculta un pequeño secreto.

 @Test
 void testLazyOneToMany() {
    Invoice invoice = em.find(Invoice.class, Datasets.INVOICE_ID);

    log.info("loading...");
    assertThat(invoice.getItems()).hasSize(3);
 }

Echemos un vistazo a la bitácora.

13:16:14,296 INFO  [stdout] (default task-2) Hibernate: select invoice0_.id as id1_7_0_ from invoices invoice0_ where invoice0_.id=?
13:16:14,299 INFO  [com.danielme.jakartaee.jpa.EntityProxyTest] (default task-2) loading...
13:16:14,344 INFO  [stdout] (default task-2) Hibernate: select items0_.invoice_id as invoice_2_8_0_, items0_.id as id1_8_0_, items0_.id as id1_8_1_, items0_.invoice_id as invoice_2_8_1_ from invoices_items items0_ where items0_.invoice_id=?

La primera sentencia SELECT, correspondiente a la operación find, obtiene la factura pero no sus líneas. La petición de un dato de la lista items (getItems().size()) provoca la ejecución de una segunda SELECT que recupera la relación. Este comportamiento se debe a que, de forma predeterminada, las relaciones one-to-many (y many-to-many) son perezosas. Y todo gracias a que Hibernate utiliza implementaciones propias de las colecciones de Java que permiten esta funcionalidad mediante el uso de proxys, técnica que vimos cuando estudiamos CDI. Por consiguiente, nos ahorramos el tiempo y la memoria que consume la obtención de la relación desde la base de datos cuando no la necesitamos.

Alternativamente, en Hibernate las relaciones perezosas pueden solicitarse sin acceder a ellas empleando el método estático Hibernate#initialize.

Hibernate.initialize(invoice.getItems());

Con el método PersistenceUnitUtil#isLoaded es posible averiguar si una relación fue recuperada .

 @Test
 void testLazyOneToMany() {
    Invoice invoice = em.find(Invoice.class, Datasets.INVOICE_ID);

    PersistenceUnitUtil pUnitUtil = em.getEntityManagerFactory().getPersistenceUnitUtil();

    assertThat(pUnitUtil.isLoaded(invoice, "items")).isFalse();
    log.info("loading...");
    assertThat(invoice.getItems()).hasSize(3);
    assertThat(pUnitUtil.isLoaded(invoice, "items")).isTrue();
}

¿Y si en vez de una colección tenemos un solo objeto? Es el caso de InvoiceItem. Las relaciones many-to-one (y one-to-one) se comportan por omisión en modo inmediato. Hay que activar el modo lazy en la anotación con al atributo fetch.

@ManyToOne(fetch = FetchType.LAZY)
private Invoice invoice;
@Test
void testLazyManyToOne() {
    InvoiceItem invoiceItem = em.find(InvoiceItem.class, Datasets.INVOICE_ID_ITEM_1);

    log.info("loading...");
    invoiceItem.getInvoice().getDateTime();

    assertThat(invoiceItem.getInvoice()).isInstanceOf(HibernateProxy.class);
}
13:37:42,368 INFO  [stdout] (default task-1) Hibernate: select invoiceite0_.id as id1_8_0_, invoiceite0_.invoice_id as invoice_2_8_0_ from invoices_items invoiceite0_ where invoiceite0_.id=?
13:37:42,397 INFO  [com.danielme.jakartaee.jpa.EntityProxyTest] (default task-1) loading...
13:37:42,398 INFO  [stdout] (default task-1) Hibernate: select invoice0_.id as id1_7_0_ from invoices invoice0_ where invoice0_.id=?

De nuevo, el atributo con la relación no es una instancia «real» sino un objeto proxy o representante gestionado por Hibernate con la herramienta Byte Buddy. Este proxy siempre contiene el identificador de la entidad. Por ello, los datos solo se obtendrán desde la base de datos cuando se acceda por primera vez a cualquier otro atributo (línea 6 de testLazyManyToOne).

Averiguamos si estamos en presencia de un proxy chequeando que el objeto implementa HibernateProxy.

  assertThat(invoiceItem.getInvoice()).isInstanceOf(HibernateProxy.class);

Como ya sabemos, el proxy mimetiza a un objeto de la clase real, pero incluye atributos y métodos inaccesibles desde el exterior para enriquecer a la clase con nuevas y asombrosas capacidades. Podemos verificarlo con el depurador de cualquier IDE.

El objeto proxy intimida un poco, aunque no es algo de lo que tengamos preocupar porque su uso en el código es transparente. Si fuera necesario, podemos obtener el objeto «real» que contiene.

@Test
void testUnproxyHibernate() {
    InvoiceItem invoiceItem = em.find(InvoiceItem.class, Datasets.INVOICE_ID_ITEM_1);

    assertThat(invoiceItem.getInvoice()).isInstanceOf(HibernateProxy.class);
    Invoice invoiceUnproxied = (Invoice) Hibernate.unproxy(invoiceItem.getInvoice());
    assertThat(invoiceUnproxied).isNotInstanceOf(HibernateProxy.class);
}

La carga perezosa solo está disponible en los objetos correspondientes a las entidades recuperadas de la base de datos por Hibernate, ya sea mediante consultas o con los métodos del gestor de entidades de JPA o de la interfaz Session de Hibernate. No obstante, y esto es muy importante tenerlo claro, deben encontrarse en estado managed (pertenecientes a un contexto de persistencia). Cuando estén en estado desligado o detached, los getters no devuelven null, sino que su invocación provoca una de las excepciones más «célebres» de Hibernate. La podemos forzar de este modo pasando una factura de managed a detached antes de usar el listado de sus items.

 Invoice invoice = em.find(Invoice.class, Datasets.INVOICE_ID);
 em.detach(invoice);
 invoice.getItems().size();
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.danielme.jakartaee.jpa.entities.cascade.Invoice.items, could not initialize proxy - no Session

El mensaje informa que la obtención de una relación perezosa no se ha realización dentro de una sesión de Hibernate, concepto que en la práctica equivale al de transacción.

Traslademos el fragmento de código anterior a una prueba del proyecto de ejemplo.

    @Test
    void testLazyInitializationException() {
        Invoice invoice = em.find(Invoice.class, Datasets.INVOICE_ID);
        em.detach(invoice);

        assertThatExceptionOfType(LazyInitializationException.class)
                .isThrownBy(() -> invoice.getItems().size());
    }

La manera en que funciona la carga perezosa tiene una consecuencia crucial en el diseño de nuestro código: toda la información de la base de datos que necesitemos fuera de los métodos transaccionales -en general, los que implementan la lógica de negocio \ casos de uso- debe obtenerse dentro de ellos. Además, esto se hará de la forma más eficiente posible, minimizando el número de consultas a la base de datos y el volumen de información que traemos a nuestro sistema. Trato este asunto al final del capítulo y en futuras entregas del curso. Cualquier otra solución, como recurrir al modo inmediato o a la técnica Open Session in View (*), debería ser descartada si queremos un sistema con un buen desempeño.

(*) A grandes rasgos, consiste en mantener abierta una sesión de Hibernate a lo largo de todo el procesamiento de una petición HTTP en una aplicación web. La idea es permitir que desde cualquier parte del código se puedan obtener las relaciones. Este enfoque viola la separación de responsabilidades (solo se debería acceder a la base de datos en los DAOs o, a lo sumo, en las clases de servicios) y tiene varias implicaciones negativas en el rendimiento. Por estos motivos, se considera un antipatrón y su empleo es cada vez más anecdótico.

Configuración de relaciones

Tal y como hemos visto, el tipo de obtención de las relaciones (y atributos) se establece con un valor del enumerado FetchType (EAGER o LAZY). Esta es la configuración predeterminada.

RelaciónModo
OneToManyLAZY
ManyToManyLAZY
OneToOneEAGER
ManyToOneEAGER

Ya lo comenté en la sección anterior: las relaciones múltiples (colecciones) se obtienen de forma perezosa, mientras que eager es el modo aplicado a las simples. Lo demuestro con más detalle en este tutorial, muy parecido al contenido del capítulo pero con otros ejemplos.

En teoría, todas las relaciones pueden configurarse a nuestro gusto. En la práctica, hay un par de casos en los que no es así.

La ilustración representa el ejemplo que usé para demostrar cómo crear relaciones one-to-one eficiente con @MapsId, compartiendo la clave primaria entre las dos entidades.

@Entity
@Table(name = "preferences")
@Getter
@Setter
public class Preferences {

	@Id
	private Long id;

	@OneToOne
	@MapsId
	private User user;

Al recuperarse la preferencia con find, también se obtiene su usuario porque one-to-one, de forma predeterminada, funciona en modo ansioso.

select preference0_.user_id as user_id3_9_0_, preference0_.enableNotifications as enableNo1_9_0_, preference0_.enableSharing as enableSh2_9_0_ 
from preferences preference0_ where preference0_.user_id=?

select user0_.id as id1_11_0_, user0_.name as name2_11_0_ from users user0_ where user0_.id=?

Configuremos la carga perezosa.

@OneToOne(fetch = FetchType.LAZY)
@MapsId
private User user;

Ahora, la siguiente prueba es válida y no se obtiene el usuario.

    @Test
    void testOneToOneUnidirectionalLazy() {
        Preferences preferences = em.find(Preferences.class, Datasets.USER_ID);

        assertThat(preferences.getUser()).isInstanceOf(HibernateProxy.class);
    }

De momento todo bien. Veamos una asociación bidireccional.

Marquemos los dos extremos con LAZY. No perdamos de vista que la clave ajena se ubica en Coupon.

public class Coupon { 
 @OneToOne(fetch = FetchType.LAZY)
 @JoinColumn(unique = true)
  private Expense expense;
public class Expense {
 @OneToOne(mappedBy = "expense", fetch = FetchType.LAZY)
  private Coupon coupon;

La obtención del cupón no recupera el gasto.

em.find(Coupon.class, 1L);
select
	coupon0_.id as id1_4_0_,
	coupon0_.amount as amount2_4_0_,
	coupon0_.expense_id as expense_5_4_0_,
	coupon0_.expiration as expirati3_4_0_,
	coupon0_.name as name4_4_0_
from
	coupons coupon0_
where
	coupon0_.id =?

Compárese con la SELECT generada si la carga fuera inmediata: se efectuaría la unión entre las tablas coupons e expenses. Esta unión es de tipo externa porque el gasto es opcional para el cupón.

 select
	coupon0_.id as id1_8_0_,
	coupon0_.amount as amount2_8_0_,
	coupon0_.expense_id as expense_5_8_0_,
	coupon0_.expiration as expirati3_8_0_,
	coupon0_.name as name4_8_0_,
	expense1_.id as id1_10_1_,
	expense1_.amount as amount2_10_1_,
	expense1_.category_id as category6_10_1_,
	expense1_.comments as comments3_10_1_,
	expense1_.concept as concept4_10_1_,
	expense1_.date as date5_10_1_
from
	coupons coupon0_
left outer join expenses expense1_ on
	coupon0_.expense_id = expense1_.id
where
	coupon0_.id =?

Todo correcto. Pero en el otro extremo el invento no funciona y se realizan dos consultas al ejecutarse find. La carga es, por tanto, inmediata.

em.find(Expense.class, 1L);
select expense0_.id as id1_6_0_, expense0_.amount as amount2_6_0_,...
select coupon0_.id as id1_4_0_, coupon0_.amount as amount2_4_0_, ...

Es razonable sentirse estafado, aunque hay un buen motivo para este comportamiento. Hibernate necesita saber si la relación existe para establecerla a null o bien crearle el proxy responsable de la carga perezosa. En el caso de la entidad Coupon, Hibernate sabe si tiene asociada un gasto simplemente comprobando la existencia de la clave ajena (columna expense_id), la cual conoce siempre porque recupera su valor con la SELECT que obtiene el cupón.

select 
    coupon0_.id as id1_4_0_,
	coupon0_.amount as amount2_4_0_,
	coupon0_.expense_id as expense_5_4_0_,

Dado que Expense no posee la clave ajena, la única forma que tiene Hibernate de saber qué gasto tiene un cupón es realizando una consulta a la base de datos. La consecuencia es la ejecución de una SELECT adicional a la que obtiene el gasto y que, como medida de optimización, se aprovecha para traer toda la entidad.

Una idea para sortear este comportamiento es indicar que la relación no es opcional (*): si no puede ser nula, Hibernate debería iniciar el proxy sin tener que revisar la base de datos.

@OneToOne(mappedBy = "expense", fetch = FetchType.LAZY, optional = false)
private Coupon coupon;

(*) En nuestro ejemplo, no es válido dado que un gasto puede pagarse sin aplicar el descuento de un cupón.

Esto no funciona porque hemos pasado por alto un detalle fundamental: el proxy debe tener el identificador de la entidad que representa, así que la consulta adicional es inevitable y, con ella, la carga inmediata.

A pesar de sonar paradójico, es una pena que esta prueba funcione. El cupón del gasto no es un proxy.

    @Test
    void testOneToOneBidirectional() {
        Coupon coupon = em.find(Coupon.class, Datasets.COUPON_ID_SUPER);
        assertThat(coupon.getExpense()).isInstanceOf(HibernateProxy.class);

        Expense expense = em.find(Expense.class, Datasets.EXPENSE_ID_1);
        assertThat(expense.getCoupon()).isNotInstanceOf(HibernateProxy.class);
    }

En consecuencia, podemos afirmar que en Hibernate las relaciones one-to-one bidireccionales no pueden ser perezosas en el extremo en el que no tenemos la clave ajena (el que usa mappedBy). Pero no todo está perdido y podemos acudir a una de las siguientes alternativas.

  • Definir la relación como unidireccional. Es la solución más sencilla y recomendable, suponiendo que esta configuración nos sirva.
  • Aplicar la mejora de bytecode de Hibernate. Lo veremos al final del capítulo.
  • Reemplazar la relación por otra que sí admita carga perezosa. En nuestro ejemplo, convertiremos el one-to-one de Expense#coupon en one-to-many y pasaremos a tener una colección. Ocultaremos este “apaño” escribiendo el set y get apropiado para asegurar que la colección tenga un único elemento y desde fuera de la clase parezca que estemos ante una relación one-to-one.

En Expense hacemos estos cambios. Se ha desactivado la generación del getter y setter con lombok para la lista, no queremos que pueda manipularse.

public class Expense {
    
    @Getter(AccessLevel.NONE)
    @Setter(AccessLevel.NONE)
    @OneToMany(mappedBy = "expense")
    private List<Coupon> coupons = new ArrayList<>(1);

    public void setCoupon(Coupon coupon) {
        if (coupon == null) {
            coupons.clear();
        } else {
            coupons.set(0, coupon);
        }
     }

    public Coupon getCoupon() {
        return coupons.isEmpty() ? null : coupons.get(0);
    }

En Coupon, cambiamos el tipo de relación a many-to-one para que todo resulte coherente.

public class Coupon {    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(unique = true)
    private Expense expense;

Hemos conseguido crear una relación bidireccional perezosa en sus dos extremos que se comporta como un one-to-one cuando la usamos en el código.

Atributos simples

La anotación @Basic de JPA permite configurar el modo de carga de los atributos simples de una entidad, es decir, aquellos que no son relaciones. Parece extraño que no queramos disponer siempre de sus valores, pero pensemos en el siguiente caso: la entidad UserWithPicture tiene una foto del usuario

@Entity
@Table(name = "users_pic")
public class UserWithPicture {   
    @Lob
    private byte[] picture;

Dejando al margen el polémico debate de si se deben guardan ficheros en una tabla o bien almacenar su ubicación en el sistema de archivos, con la obtención inmediata tenemos un problema. Cada vez que recuperemos un usuario, el atributo picture contendrá su imagen y esta acción puede penalizar el rendimiento. Así pues, activamos siempre el modo lazy.

    @Basic(fetch=FetchType.LAZY)
    @Lob
    private byte[] picture;

Este es el otro caso en el que Hibernate no permite carga perezosa, a menos que usemos la mejora de bytecode. No obstante, podemos seguir la misma praxis de la sección anterior y diseñar una relación alternativa. Repetimos la jugada de User y Preference: una relación unidireccional one-to-one exclusiva en las que los objetos asociados en ambos extremos comparten la clave primaria. No necesitamos que sea bidireccional porque recuperaremos la foto de un usuario con el identificador del propio usuario.

@Entity
@Table(name = "pictures")
@Getter
@Setter
public class Picture {

    @Id
    private Long id;

    @Lob
    private byte[] picture;

    @OneToOne(fetch = FetchType.LAZY, optional = false)
    @MapsId
    private UserWithPicture user;

}
@Entity
@Table(name = "users_pic")
@Getter
@Setter
public class UserWithPicture {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String name;

}

Ya no es un inconveniente que el binario se obtenga con carga inmediata porque el único propósito de la entidad Picture es recuperarlo.

UserWithPicture user = em.find(UserWithPicture.class, 1L);
byte[] pictureBinary = em.find(Picture.class, user.getId()).getPicture();

Mejora de bytecode (bytecode enhancement)

La mejora de bytecode (bytecode enhancement) es un mecanismo que incorpora nuevas capacidades a las entidades ya compiladas, esto es, modificando el bytecode de los .class en lugar de añadir código a la clase como hace lombok. Suena más complicado de lo que realmente es, pues todo se basa en aplicar un plugin a la fase de compilación en Maven.

Añadimos y configuramos el plugin del siguiente modo, habilitando el flag enableLazyInitialization. Necesitamos una versión de Hibernate compatible con el nombre de paquetes de Jakarta EE 9 (JPA 3.0), de lo contrario el plugin no hará nada. Son las dependencias con el sufijo -jakarta.

<plugin>
    <groupId>org.hibernate.orm.tooling</groupId>
    <artifactId>hibernate-enhance-maven-plugin</artifactId>
    <version>${hibernate.jakarta}</version>
    <executions>
        <execution>
            <goals>
                <goal>enhance</goal>
            </goals>
        </execution>
    </executions>
    <configuration>
        <enableLazyInitialization>true</enableLazyInitialization>
    </configuration>
    <dependencies>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-core-jakarta</artifactId>
            <version>${hibernate.jakarta}</version>
        </dependency>
    </dependencies>
</plugin>

El plugin se ejecuta al compilarse el proyecto con Maven o con esta orden.

mvn hibernate-enhance:enhance

En IntelliJ, no se aplica cuando el IDE compila. Se puede lanzar desde la vista de Maven…

…o automáticamente cuando el proyecto se despliegue en el servidor, añadiendo la acción a Before launch en la configuración de ejecución.

Al «decompilar» el fichero Expense.class aparecerán unos métodos con nombres extraños que no estaban presentes en el código fuente.

   public Coupon getCoupon() {
      return this.$$_hibernate_read_coupon();
   }

   public void setCoupon(Coupon coupon) {
      this.$$_hibernate_write_coupon(coupon);
   } 

public Coupon $$_hibernate_read_coupon() {
      if (this.$$_hibernate_getInterceptor() != null) {
         this.coupon = (Coupon)this.$$_hibernate_getInterceptor().readObject(this, "coupon", this.coupon);
      }

      return this.coupon;
   }

   public void $$_hibernate_write_coupon(Coupon var1) {
      if (this.$$_hibernate_getInterceptor() != null) {
         this.coupon = (Coupon)this.$$_hibernate_getInterceptor().writeObject(this, "coupon", this.coupon, var1);
      } else {
         this.coupon = (Coupon)var1;
      }
   }

Este «hechizo» sobre los getters y setters habilita la carga perezosa en los dos casos en problemáticos: el lado sin clave ajena del one-to-one bidireccional y los atributos. Eso sí, hay que acompañar @OneToOne con @LazyToOne para indicar que no se use el mecanismo de proxy de Hibernate.

@OneToOne(mappedBy = "expense", fetch = FetchType.LAZY)
@LazyToOne(LazyToOneOption.NO_PROXY)
private Coupon coupon;

En versiones de Hibernate anteriores a la 5.5 (como la que incluye WildFly 22 Preview), publicada en junio de 2021, el empleo de bytecode enhancement tiene un impacto importante en todas las relaciones one-to-one y many-to-one de todas las entidades.

  • Las relaciones queramos obtener de forma perezosa, con independencia de que requieran la mejora de bytecode, deben anotarse con @LazyToOne, además de tener su fetch a LAZY. Si no se cumplen los dos requisitos, pasarán a ser de carga inmediata.
  • La primera vez que se solicite una relación de estos tipos en una entidad, se obtendrán a la vez todas sus relaciones simples y perezosas. Para evitarlo, tenemos que recurrir a la anotación LazyGroup y organizar en grupos aquellas de tipo lazy que deban obtenerse conjuntamente al mismo tiempo. Si queremos que se obtengan de una en una a medida que se soliciten con sus métodos get (lo más habitual), cada relación tiene que constituir un grupo propio.

En Expense, aplicaremos las dos soluciones así para que sus relaciones sean LAZY en este escenario.

 @ManyToOne(optional = false, fetch = FetchType.LAZY)
 @LazyToOne(LazyToOneOption.NO_PROXY)
 @LazyGroup("category")
 private Category category;

 @OneToOne(mappedBy = "expense", fetch = FetchType.LAZY)
 @LazyToOne(LazyToOneOption.NO_PROXY)
 @LazyGroup("coupon")
 private Coupon coupon;

Los peligros de la carga perezosa

El mecanismo de carga perezosa parece una maravilla, pues asegura que solo obtendremos los datos en el momento de necesitarlos. Esto es cierto, pero tendremos problemas de rendimiento si lo usamos de forma indiscriminada como en el siguiente ejemplo.

List<Coupon> coupons = em.createQuery("SELECT c FROM Coupon c WHERE c.expense.category.id = 1", 
                                     Coupon.class)
                         .getResultList();
coupons.forEach(c -> c.getExpense().getAmount());

Aunque en este fragmento de código me he adelantado un poco, es fácil de comprender. Se ejecuta una consulta con el lenguaje JPQL de JPA, parecido a SQL pero basado en entidades en lugar de tablas. Recupera todos los cupones asociados a gastos de la categoría 1. Queremos que los cupones obtenidos contengan su gasto, y como la consulta no los trae porque respeta que la relación es LAZY, lo hacemos en la línea 4. En realidad, si el gasto existe no es nulo sino un proxy que contiene el identificador, por eso he invocado a un getter. Lo explico aquí.

¡Error! Este es el SQL generado.

select ... from coupons coupon0_ cross join expenses expense1_ 
               where coupon0_.expense_id=expense1_.id and  expense1_.category_id=1

select ... from expenses expense0_ where expense0_.id=?
select ... from expenses expense0_ where expense0_.id=?
select ... from expenses expense0_ where expense0_.id=?

Hemos topado con el conocido problema de rendimiento N + 1: para obtener una lista de elementos, se realiza la consulta que obtiene el listado y, al menos, una adicional por cada elemento del mismo. Y esto no es nada bueno; cuanto mayor sea el listado, más pobre será el desempeño de nuestro código.

Lo más eficiente es obtener todos los datos que necesitemos con el menor número de consultas SQL. Veremos cómo hacerlo con este mismo ejemplo cuando tratemos las reuniones de entidades en JPQL con FETCH JOIN.

La carga inmediata

A priori, no hay mucho que contar de este modo: obtiene las relaciones en el acto. Volvamos al caso en el que la relación del cupón con el gasto sea EAGER.

@Entity
@Table(name = "coupons")
@Getter
@Setter
public class Coupon {   
    @OneToOne
    @JoinColumn(unique = true)
    private Expense expense;

Si ejecutamos de nuevo la consulta JPQL, no hará falta el foreach. Pero el rendimiento sigue comprometido porque continúa el problema N + 1. Con EAGER indicamos cuándo queremos obtener la relación, pero no cómo. En Hibernate, esto último es posible gracias a la anotación @Fetch. En nuestro ejemplo, se está aplicando de forma predeterminada la estrategia SELECT, pero podemos usar FetchMode.JOIN para que se reúnan las tablas coupons y expenses y se obtenga el gasto en la misma consulta. Si la relación fuera múltiple, también estaría disponible la opción FetchMode.SUBSELECT.

    @OneToOne
    @Fetch(FetchMode.JOIN)
    @JoinColumn(unique = true)
    private Expense expense;

Si volvemos a ejecutar el ejemplo, veremos que nada cambia. La estrategia configurada con @Fetch solo se aplica cuando la entidad se obtiene directamente con el gestor de entidades.

em.find(Coupon.class, 2L)
SELECT... FROM coupons coupon0_LEFT OUTER JOIN expenses expense1_ ON coupon0_.expense_id = expense1_.id
            WHERE coupon0_.id =2

Así pues, el uso EAGER, al igual que LAZY, no está exento de problemas en lo que a rendimiento se refiere.

¿Eager o Lazy?

El modo EAGER solo debería emplearse cuando siempre queramos obtener una relación. Pero esta situación no será nada habitual, y en el momento en que se dé un caso en el que no la necesitamos, ya estamos incurriendo en una penalización en el rendimiento. Si a esto le añadimos que en algunas configurarciones puede causar el problema N + 1, lo más seguro es usar siempre LAZY y descartar la carga inmediata.

Con todo, hemos comprobado que la carga perezosa no es la panacea. Evitemos caer en el error de trabajar despreocupadamente con los getters, pues la consecuencia será un sistema con un desempeño pobre. Y, en todos los casos, tal y como indiqué en su momento, si la relación es una colección grande cargarla entera con su get (LAZY) o de forma inmediata es una barbaridad. Mejor conseguir los datos de manera paginada cuando la necesitemos.

El objetivo será obtener únicamente aquella información requerida en cada proceso de negocio con el menor número de consultas SQL. Para conseguirlo, no podemos ser «perezosos»: hagamos el esfuerzo de escribir consultas que además sean eficientes. JPA provee los mecanismos adecuados, y a ellos están dedicados los próximos capítulos del curso.

Código de ejemplo

El código de ejemplo de los capítulos dedicados a las relaciones se encuentra en GitHub (todos los proyectos son independientes pero están en un único repositorio). Para más información sobre cómo utilizar GitHub, consultar este artículo.

>>>> ÍNDICE <<<<

Deja una respuesta

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Salir /  Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Salir /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Salir /  Cambiar )

Conectando a %s

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.