JPA e Hibernate: relaciones y atributos Lazy. Bytecode enhancement.

Última actualización: 26/12/2021

logo hibernate

En JPA, los atributos y relaciones de una entidad pueden ser recuperados desde la base de datos de dos maneras distintas: en el momento en que obtengamos la entidad (modo Eager, «ansioso» o «inmediato») o bien bajo demanda (modo Lazy o «perezoso»), esto es, la primera vez que se acceda a dichas propiedades o relaciones. El objetivo final es mejorar el rendimiento, evitando obtener datos que realmente no necesitamos.

IMPORTANTE El contenido de este artículo ha sido mejorado y ampliado en Curso Jakarta EE 9 (28). JPA con Hibernate (11): Relaciones: obtención eager\lazy, bytecode enhancement 😉

Este modo Lazy es posible en Hibernate gracias al uso de objetos proxy que lanzan las consultas necesarias para la obtención del contenido del objeto «real». Solo está disponible en los objetos correspondientes a las entidades recuperadas de la base de datos por Hibernate, ya sea mediante consultas o bien directamente con los métodos del gestor de entidades (interfaz EntityManager de JPA) o la interfaz Session de Hibernate.

No obstante, y esto es muy importante tenerlo claro, estas entidades 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 las excepciones más «célebres» de Hibernate: LazyInitializationException. Su 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.

En este artículo veremos cómo conseguir que Hibernate obtenga de forma Lazy las relaciones y atributos de nuestras entidades. Tenemos que comprender el funcionamiento de este mecanismo para usarlo con responsabilidad.

Proyecto de ejemplo

Para «jugar» con la definición de relaciones en Hibernate -versión 5.4.21 de agosto de 2020- vamos a utilizar como banco de pruebas un proyecto Maven para Java 8 muy sencillo. Contendrá un modelo de clases de tipo entidad que veremos en breve.

La configuración de JPA con Hibernate se define en el fichero /META-INF/persistence.xml tal y como ordena la especificación (aunque en Spring no es necesario). Atención a los datos de conexión a la base de datos.

<persistence xmlns="http://java.sun.com/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
             version="2.0">
    <persistence-unit name="BirdsPersistence" transaction-type="RESOURCE_LOCAL">   

        <properties>
            <property name="javax.persistence.jdbc.url"
                      value="jdbc:mysql://localhost:3306/birds?autoReconnect=true&amp;useSSL=false&amp;serverTimezone=UTC"/>
            <property name="javax.persistence.jdbc.user" value="birds"/>
            <property name="javax.persistence.jdbc.password" value="password"/>
            <property name="hibernate.hbm2ddl.auto" value="update"/>
            <property name="hibernate.format_sql" value="true"/>
            <property name="hibernate.dialect" value="org.hibernate.dialect.MySQL55Dialect"/>
        </properties>

    </persistence-unit>
</persistence>

La base de datos que voy a usar es MySQL 5. La configuración, incluyendo el usuario y el juego de datos de pruebas, se encuentra en el script birds.sql. Para mayor comodidad, he incluido en la raíz del proyecto un fichero Dockerfile basado en la imagen oficial de MySQL. Al crearse un contenedor, se configura todo lo necesario porque se ejecutará el script birds.sql.

Con estas órdenes se contruye la imagen y arranca un contenedor de usar y tirar (la opción –rm borrará el contenedor cuando se detenga).

docker build -t hibernate-lazy-mysql .
docker run --rm -d -p 3306:3306 hibernate-lazy-mysql

Docker 1: introducción
Docker 2: imágenes y contenedores

Obsérvese que el contenedor expone el puerto 3306 de MySQL hacia fuera a través del puerto 3306. Si tenemos más de un MySQL en ejecución, cada uno deberá usar un puerto distinto. Si estás en esta situación, usa uno distinto al crear el contenedor y tenlo en cuenta al configurar los datos de conexión.

Usaremos la siguiente clase con pruebas automáticas basadas en JUnit 4. El único propósito de estas pruebas será imprimir en el log las consultas SQL que Hibernate vaya ejecutando. Se instancia para cada test un gestor de entidades correspondiente a la unidad de persistencia definida en el fichero persistence.xml. No necesitamos transacciones, lo que simplifica aún más el código.

package com.danielme.blog.hibernatefetching;

import com.danielme.blog.hibernatefetching.entities.*;
import org.apache.log4j.Logger;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;

public class FetchTest {

    private static final EntityManagerFactory entityManagerFactory = Persistence
            .createEntityManagerFactory("BirdsPersistence");
    private static EntityManager entityManager;

    private static final Logger log = Logger.getLogger(FetchTest.class);

    @Before
    public void setup() {
        entityManager = entityManagerFactory.createEntityManager();
    }

    @After
    public void shutdown() {
        entityManager.close();
    }

    @Test
    public void testBird() {
        log.info("--------> Bird");
        Bird bird = entityManager.find(Bird.class, 1L);;
        log.info("--------> Specie unidirectional OneToOne");
        bird.getSpecie().getName();
        log.info("--------> Picture - Lob Basic");
        bird.getPicture();
        log.info("--------> Awards bidirectional OneToMany");
        bird.getAwards().size();
        log.info("--------> Treatments Many to many");
        bird.getTreatments().size();
        log.info("--------> Breeder bidirectional ManyToOne");
        bird.getBreeder().getName();
        log.info("--------> Cage bidirectional OneToOne");
        bird.getCage().getName();
        log.info("--------> Notes unidirectional OneToMany");
        bird.getNotes().size();
    }

    @Test
    public void testAward() {
        log.info("--------> Award");
        Award award = entityManager.find(Award.class, 1L);
        log.info("--------> Bird bidirectional @ManyToOne");
        award.getBird().getBand();
    }

    @Test
    public void testBreeder() {
        log.info("--------> Breeder");
        Breeder breeder = entityManager.find(Breeder.class, 1L);
        log.info("--------> Birds bidirectional OneToMany");
        breeder.getBirds().size();
    }

    @Test
    public void testCage() {
        log.info("--------> Cage");
        Cage cage = entityManager.find(Cage.class, 1L);
        log.info("--------> Bird bidirectional OneToOne");
        cage.getBird().getBand();
    }

    @Test
    public void testTeatments() {
        entityManager.find(Treatment.class, 1L).getBirds().size();
    }

}

El log se ha configurado con log4j2.

<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
    <Appenders>
        <Console name="STDOUT" target="SYSTEM_OUT">
            <PatternLayout pattern="%c{1} %highlight{%level} - %msg%n"/>
        </Console>
    </Appenders>
    <Loggers>
        <Logger name="org.hibernate.SQL" level="debug"/>
        <Logger name="com.danielme" level="info"/>
        <Root level="warn">
            <AppenderRef ref="STDOUT"/>
        </Root>
    </Loggers>
</Configuration>

Modelo de pruebas

Este es el modelo de clases de tipo entidad.

Las clases contienen, a modo de ejemplo, las relaciones más comunes.

  • OneToOne unidireccional entre Bird y Specie.
    @OneToOne    
    @JoinColumn(name = "specie_id")
    private Specie specie;
    
  • OneToOne bidireccional entre Bird y Cage.
    @OneToOne
    @JoinColumn(name = "cage_id")
    private Cage cage;
    
    @OneToOne(mappedBy = "cage")
    private Bird bird;
    
  • ManyToOne bidireccional entre Bird y Breeder.
    @ManyToOne
    @JoinColumn(name = "breeder_id")
    private Breeder breeder;
    
    @OneToMany(mappedBy = "breeder")
    private List<Bird> birds;
    
  • OneToMany bidireccional entre Bird y Award (equivalente al anterior)
    @OneToMany(mappedBy = "bird")
    private List<Award> awards;
    
    @ManyToOne
    @JoinColumn(name = "bird_id")
    private Bird bird;
    
  • OneToMany unidireccional entre Bird y Note. En este mapeo, si no se utiliza @JoinColumn Hibernate crea una tabla intermedia.
    @OneToMany
    @JoinColumn(name = "bird_id")
    private List<Note> notes;
    
  • ManyToMany entre Bird y Treatment.
    @ManyToMany
    @JoinTable(name = "bird_treatment", joinColumns = @JoinColumn(name = "bird_id"), 
        inverseJoinColumns = @JoinColumn(name = "treatment_id"))
        private Set<Treatment> treatments;
    
    @ManyToMany(mappedBy = "treatments")
    private Set<Bird> birds;
    
  • Por último, en la clase Bird tenemos un atributo para contener un fichero, y queremos que este se obtenga de forma Lazy.
    @Basic(fetch=FetchType.LAZY)
    @Lob
    private byte[] picture;
    

Para más información sobre la configuración de relaciones en JPA con Hibernate, consultar:
JPA con Hibernate 7 – Relaciones: introducción y relaciones simples (many-to-one, one-to-one)
JPA con Hibernate 8 – Relaciones múltiples (one-to-many, many-to many)

Probando las relaciones

El proyecto de ejemplo nos permitirá comprobar el comportamiento predeterminado de las relaciones y atributos de nuestras entidades, y modificarlo para experimentar con él. Me adelanto al resultado: Hibernate respeta lo especificado por el estándar JPA.

Relación Obtención por defecto
OneToMany LAZY
ManyToMany LAZY
OneToOne EAGER
ManyToOne EAGER

Vemos que las relaciones múltiples (colecciones) son perezosas, mientras que las simples son inmediatas.

Ejecutando las pruebas podemos observar el comportamiento de cada relación.

OneToMany

Funcionan por omisión con el modo Lazy, lo vemos en la relación de Bird con Award y con Notas.

FetchTest:55 - --------> Awards bidirectional OneToMany
SQL:92 - 
    select
        awards0_.bird_id as bird_id3_0_0_,
        awards0_.id as id1_0_0_,
        awards0_.id as id1_0_1_,
        awards0_.bird_id as bird_id3_0_1_,
        awards0_.title as title2_0_1_ 
    from
        award awards0_ 
    where
        awards0_.bird_id=?
FetchTest:63 - --------> Notes unidirectional OneToMany
SQL:92 - 
    select
        notes0_.bird_id as bird_id3_5_0_,
        notes0_.id as id1_5_0_,
        notes0_.id as id1_5_1_,
        notes0_.name as name2_5_1_ 
    from
        note notes0_ 
    where
        notes0_.bird_id=?
ManyToOne

Estas relaciones se comportan de forma Eager y al obtenerse un Bird siempre se incluye el Breeder.

FetchTest:49 - --------> Bird
SQL:92 - 
    select
        bird0_.id as id1_1_0_,
        bird0_.band as band2_1_0_,
        bird0_.breeder_id as breeder_4_1_0_,
        bird0_.cage_id as cage_id5_1_0_,
        bird0_.picture as picture3_1_0_,
        bird0_.specie_id as specie_i6_1_0_,
        breeder1_.id as id1_3_1_,
        breeder1_.name as name2_3_1_ 
    from
        bird bird0_ 
    left outer join
        breeder breeder1_ 
            on bird0_.breeder_id=breeder1_.id 
    where
        bird0_.id=?

Tendremos carga perezosa simplemente indicándolo.

    @ManyToOne(fetch=FetchType.LAZY)
    @JoinColumn(name = "breeder_id")
    private Breeder breeder;
FetchTest:49 - --------> Bird
SQL:92 - 
    select
        bird0_.id as id1_1_0_,
        bird0_.band as band2_1_0_,
        bird0_.breeder_id as breeder_4_1_0_,
        bird0_.cage_id as cage_id5_1_0_,
        bird0_.picture as picture3_1_0_,
        bird0_.specie_id as specie_i6_1_0_ 
    from
        bird bird0_ 
    where
        bird0_.id=?
FetchTest:59 - --------> Breeder bidirectional ManyToOne
SQL:92 - 
    select
        breeder0_.id as id1_3_0_,
        breeder0_.name as name2_3_0_ 
    from
        breeder breeder0_ 
    where
        breeder0_.id=?

Lo mismo es aplicable a la relación entre Award y Bird.

@ManyToOne(fetch=FetchType.LAZY)
@JoinColumn(name = "bird_id")
private Bird bird;
ManyToMany

De nuevo, tenemos una relación que de forma predeterminada es Lazy.

FetchTest:57 - --------> Treatments Many to many
SQL:92 - 
    select
        treatments0_.bird_id as bird_id1_2_0_,
        treatments0_.treatment_id as treatmen2_2_0_,
        treatment1_.id as id1_7_1_,
        treatment1_.name as name2_7_1_ 
    from
        bird_treatment treatments0_ 
    inner join
        treatment treatment1_ 
            on treatments0_.treatment_id=treatment1_.id 
    where
        treatments0_.bird_id=?
OneToOne

Todas las relaciones de este tipo se comportan de modo Eager. Por ejemplo, al recuperarse un Bird (primera operación de FetchTest#testBird) nos traemos mediante reuniones (join) su especie y su jaula:

select
        bird0_.id as id1_1_0_,
        bird0_.band as band2_1_0_,
        bird0_.breeder_id as breeder_4_1_0_,
        bird0_.cage_id as cage_id5_1_0_,
        bird0_.picture as picture3_1_0_,
        bird0_.specie_id as specie_i6_1_0_,
        breeder1_.id as id1_3_1_,
        breeder1_.name as name2_3_1_,
        cage2_.id as id1_4_2_,
        cage2_.name as name2_4_2_,
        specie3_.id as id1_6_3_,
        specie3_.name as name2_6_3_ 
    from
        bird bird0_ 
    left outer join
        breeder breeder1_ 
            on bird0_.breeder_id=breeder1_.id 
    left outer join
        cage cage2_ 
            on bird0_.cage_id=cage2_.id 
    left outer join
        specie specie3_ 
            on bird0_.specie_id=specie3_.id 
    where
        bird0_.id=?

En nuestro ejemplo, podemos hacer Lazy la relación unidireccional entre Bird y Specie así

@OneToOne(optional=false, fetch=FetchType.LAZY)
@JoinColumn(name = "specie_id")
private Specie specie;

Podemos comprobar en el log que la reunión con la especie que aparecía en la consulta anterior ha desaparecido.

 select
        bird0_.id as id1_1_0_,
        bird0_.band as band2_1_0_,
        bird0_.breeder_id as breeder_4_1_0_,
        bird0_.cage_id as cage_id5_1_0_,
        bird0_.picture as picture3_1_0_,
        bird0_.specie_id as specie_i6_1_0_,
        breeder1_.id as id1_3_1_,
        breeder1_.name as name2_3_1_,
        cage2_.id as id1_4_2_,
        cage2_.name as name2_4_2_ 
    from
        bird bird0_ 
    left outer join
        breeder breeder1_ 
            on bird0_.breeder_id=breeder1_.id 
    left outer join
        cage cage2_ 
            on bird0_.cage_id=cage2_.id 
    where
        bird0_.id=?

Y que la especie se obtiene cuando se solicita uno de sus atributos.

FetchTest:51 - --------> Specie unidirectional OneToOne
SQL:92 - 
    select
        specie0_.id as id1_6_0_,
        specie0_.name as name2_6_0_ 
    from
        specie specie0_ 
    where
        specie0_.id=?

Ídem en relación de Bird con Cage del lado de Bird.

@OneToOne(optional=false, fetch=FetchType.LAZY)
@JoinColumn(name = "cage_id")
private Cage cage;

Ahora desaparece la reunión con la tabla cage y la jaula se recupera cuando la usemos.

--------> Cage bidirectional OneToOne
    select
        cage0_.id as id1_4_0_,
        cage0_.name as name2_4_0_ 
    from
        cage cage0_ 
    where
        cage0_.id=?

Obsérvese que al traernos Cage de forma Lazy después de haber obtenido el Bird, Cage vuelve a recuperar el Bird asociado. En este extremo de la relación, dado que no se configuró Lazy, se asume Eager por omisión.

FetchTest:61 - --------> Cage bidirectional OneToOne
SQL:92 - 
    select
        cage0_.id as id1_4_0_,
        cage0_.name as name2_4_0_,
        bird1_.id as id1_1_1_,
        bird1_.band as band2_1_1_,
        bird1_.breeder_id as breeder_4_1_1_,
        bird1_.cage_id as cage_id5_1_1_,
        bird1_.picture as picture3_1_1_,
        bird1_.specie_id as specie_i6_1_1_,
        breeder2_.id as id1_3_2_,
        breeder2_.name as name2_3_2_ 
    from
        cage cage0_ 
    left outer join
        bird bird1_ 
            on cage0_.id=bird1_.cage_id 
    left outer join
        breeder breeder2_ 
            on bird1_.breeder_id=breeder2_.id 
    where
        cage0_.id=?

¿Y si hacemos Lazy la relación en Cage?

@OneToOne(mappedBy = "cage", fetch = FetchType.LAZY)
private Bird bird;   

¡No funciona! Nada cambia y la obtención de la jaula en testCage conlleva la recuperación de su ave. Parece 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 un objeto proxy responsable de la carga perezosa y que contendrá el identificador del ave.

En el caso de una entidad de tipo Bird, Hibernate sabe si tiene asociada una jaula comprobando la existencia de la clave ajena (columna cage_id), la cual conoce siempre porque recupera su valor con la SELECT que obtiene el ave. Pero, debido a que Cage no posee la clave ajena, la única forma que tiene Hibernate de saber qué ave está en la jaula es realizando una consulta a la base de datos. La consecuencia es la ejecución de una SELECT adicional y que, como medida de optimización, se aprovecha para traer toda la entidad Bird en lugar de quedarse solo con su identificador.

A tenor de las pruebas, podemos afirmar que las relaciones one-to-one bidireccionales en la clase de entidad sin la clave ajena son Eager aunque se configuren como Lazy. No todo está perdido, pues contamos con a la anotación de Hibernate LazyToOne. Configura la manera de efectuar la carga Lazy en one-to-one. Lo que haremos es desactivar la precarga de la entidad con un proxy.

@OneToOne(fetch = FetchType.LAZY, optional = true, mappedBy = "cage")
@LazyToOne(LazyToOneOption.NO_PROXY)
private Bird bird;

Lamentablemente, tenemos que aplicar mejora de bytecode (lo veremos al final de este artículo) para hacer funcionar LazyToOne, aunque en versiones anteriores a Hibernate 5.1 era suficiente con implementar en la clase de tipo entidad la interfaz FieldHandled. Aquí dejo un ejemplo.

Como alternativa a LazyToOne, recomiendo evaluar si la relación debe ser bidireccional. En caso afirmativo, es posible configurar un mapeo alternativo convirtiendo la relación en un one-to-many bidireccional que ya sabemos que puede ser Lazy en los dos extremos sin problema alguno. Desde fuera de la clase ocultaremos este «apaño» y trabajaremos con un único objeto Bird si escribimos el set y get apropiado.

@OneToMany(mappedBy = "cage")
private List<Bird> birds;

public Bird getBird() {
 return birds != null && !birds.isEmpty() ? birds.get(0) : null;
}

public void setBird(Bird bird) {
 if (birds == null) {
    birds = new ArrayList<>();
  }
  birds.set(0, bird);
}    

En el otro extremo de la relación usamos @ManyToOne para mantener la coherencia

@ManyToOne(optional=false, fetch=FetchType.LAZY) 
@JoinColumn(name = "cage_id")
private Cage cage;

De este modo, hemos «simulado» una relación one-to-one bidireccional perezosa en sus dos extremos.

Atributos

Aunque los atributos que no representen a una relación pueden configurarse como Lazy con la anotación Basic -el campo Bird#picture-, Hibernate los recupera siempre.

FetchTest:49 - --------> Bird
SQL:92 - 
    select
        bird0_.id as id1_1_0_,
        bird0_.band as band2_1_0_,
        bird0_.breeder_id as breeder_4_1_0_,
        bird0_.cage_id as cage_id5_1_0_,
        bird0_.picture as picture3_1_0_,
        bird0_.specie_id as specie_i6_1_0_ 
    from
        bird bird0_ 
    where
        bird0_.id=?

Esto supone un problema si tenemos atributos que contengan binarios o textos muy grandes y que, por tanto, solo queremos obtener de forma explícita cuando sea estrictamente necesario. En el ejemplo, cada Bird puede tener una imagen, y si obtenemos un listado de aves estaremos recuperando esos ficheros con el problema de rendimiento que esto puede suponer.

Para habilitar la obtención perezosa en los atributos, de nuevo se requiere mejora de bytecode. Pero podemos repetir la jugada que hicimos antes y definir un mapeo alternativo. En esta ocasión, vamos a crear la clase de tipo entidad Picture (contendrá el atributo byte[] con la imagen) y a establecer una relación unidireccional, opcional y Lazy desde Bird hasta Picture.

package com.danielme.blog.hibernatefetching.entities;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Lob;
import javax.persistence.Table;

@Entity
@Table(name = "picture")
public class Picture {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;
	
	@Lob    
    private byte[] picture;

	public Long getId() {
		return id;
	}

	public void setId(Long id) {
		this.id = id;
	}

	public byte[] getPicture() {
		return picture;
	}

	public void setPicture(byte[] picture) {
		this.picture = picture;
	}	
	
}

Asociamos Bird con Picture. He llamado al atributo PictureEntity para distinguirlo del array de bytes Picture que ya tenemos en el ejemplo inicial.

 @OneToOne(fetch=FetchType.LAZY)
 @JoinColumn(name = "picture_id")
 private Picture pictureEntity;

Hemos cumplido nuestro objetivo, y podemos verificarlo con la salida de esta prueba.

    @Test
    public void testPictureEntity() {
        log.info("--------> Bird");
        Bird bird = entityManager.find(Bird.class, 1L);
        log.info("--------> Picture entity");
        bird.getPictureEntity().getPicture();
    }

Con todo, el ejemplo es mejorable. Obsérvese que una imagen siempre está asociada a una sola ave, por lo que el identificador de Picture puede coincidir con el de Bird. Esto implica que la columna cage_id es redundante

Podemos igualar clave ajena y primaria en la clase Bird con la anotación @MapsId, de tal modo que ambas claves usen la columna id.

@OneToOne(fetch = FetchType.LAZY)
@MapsId
@JoinColumn(name="id")
private Picture pictureEntity;

Mejor ahora. El identificador de Picture ya no debe ser autogenerado, le pondremos el de su ave.

public class Picture {
    @Id
    private Long id;

Bytecode Enhancement

Esta técnica permite procesar el bytecode generado por el compilador para añadir de forma automática nuevas funcionalidades. Hibernate, desde la versión 4.2.9, provee un plugin para Maven, también disponible para Gradle y Ant, que aplica automáticamente Bytecode enhancement a las clases de entidad gracias a la librería javassist. Para utilizarlo tan solo tenemos que añadirlo al pom activando la opción enableLazyInitialization, la que necesitamos para poder realizar la carga perezosa de cualquier relación o atributo.

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

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

mvn hibernate-enhance:enhance

Si «decompilamos» las clases veremos los cambios que hizo el plugin.

package com.danielme.blog.hibernatefetching.entities;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToOne;
import javax.persistence.Table;
import javax.persistence.Transient;
import org.hibernate.annotations.LazyToOne;
import org.hibernate.annotations.LazyToOneOption;
import org.hibernate.engine.spi.EntityEntry;
import org.hibernate.engine.spi.ManagedEntity;
import org.hibernate.engine.spi.PersistentAttributeInterceptable;
import org.hibernate.engine.spi.PersistentAttributeInterceptor;

@Entity
@Table(name="cage")
public class Cage
  implements ManagedEntity, PersistentAttributeInterceptable
{

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

  @Column(nullable=false, unique=true)
  private String name;

  @OneToOne(fetch=FetchType.LAZY, optional=true, mappedBy="cage")
  @LazyToOne(LazyToOneOption.NO_PROXY)
  private Bird bird;

  @Transient
  private transient EntityEntry $$_hibernate_entityEntryHolder;

  @Transient
  private transient ManagedEntity $$_hibernate_previousManagedEntity;

  @Transient
  private transient ManagedEntity $$_hibernate_nextManagedEntity;

  @Transient
  private transient PersistentAttributeInterceptor $$_hibernate_attributeInterceptor;

  public Long getId()
  {
    return $$_hibernate_read_id();
  }

  public void setId(Long id) {
    $$_hibernate_write_id(id);
  }

  public String getName() {
    return $$_hibernate_read_name();
  }

  public void setName(String name) {
    $$_hibernate_write_name(name);
  }

  public Bird getBird() {
    return $$_hibernate_read_bird();
  }

  public void setBird(Bird bird) {
    $$_hibernate_write_bird(bird);
  }

  public Object $$_hibernate_getEntityInstance()
  {
    return this;
  }

  public EntityEntry $$_hibernate_getEntityEntry()
  {
    return this.$$_hibernate_entityEntryHolder;
  }

  public void $$_hibernate_setEntityEntry(EntityEntry paramEntityEntry)
  {
    this.$$_hibernate_entityEntryHolder = paramEntityEntry;
  }

  public ManagedEntity $$_hibernate_getPreviousManagedEntity()
  {
    return this.$$_hibernate_previousManagedEntity;
  }

  public void $$_hibernate_setPreviousManagedEntity(ManagedEntity paramManagedEntity)
  {
    this.$$_hibernate_previousManagedEntity = paramManagedEntity;
  }

  public ManagedEntity $$_hibernate_getNextManagedEntity()
  {
    return this.$$_hibernate_nextManagedEntity;
  }

  public void $$_hibernate_setNextManagedEntity(ManagedEntity paramManagedEntity)
  {
    this.$$_hibernate_nextManagedEntity = paramManagedEntity;
  }

  public PersistentAttributeInterceptor $$_hibernate_getInterceptor()
  {
    return this.$$_hibernate_attributeInterceptor;
  }

  public void $$_hibernate_setInterceptor(PersistentAttributeInterceptor paramPersistentAttributeInterceptor)
  {
    this.$$_hibernate_attributeInterceptor = paramPersistentAttributeInterceptor;
  }

  public Long $$_hibernate_read_id()
  {
    if ($$_hibernate_getInterceptor() != null)
      this.id = ((Long)$$_hibernate_getInterceptor().readObject(this, "id", this.id));
    return this.id;
  }

  public void $$_hibernate_write_id(Long paramLong)
  {
    Long localLong = paramLong;
    if ($$_hibernate_getInterceptor() != null)
      localLong = (Long)$$_hibernate_getInterceptor().writeObject(this, "id", this.id, paramLong);
    this.id = localLong;
  }

  public String $$_hibernate_read_name()
  {
    if ($$_hibernate_getInterceptor() != null)
      this.name = ((String)$$_hibernate_getInterceptor().readObject(this, "name", this.name));
    return this.name;
  }

  public void $$_hibernate_write_name(String paramString)
  {
    String str = paramString;
    if ($$_hibernate_getInterceptor() != null)
      str = (String)$$_hibernate_getInterceptor().writeObject(this, "name", this.name, paramString);
    this.name = str;
  }

  public Bird $$_hibernate_read_bird()
  {
    if ($$_hibernate_getInterceptor() != null)
      this.bird = ((Bird)$$_hibernate_getInterceptor().readObject(this, "bird", this.bird));
    return this.bird;
  }

  public void $$_hibernate_write_bird(Bird paramBird)
  {
    Bird localBird = paramBird;
    if ($$_hibernate_getInterceptor() != null)
      localBird = (Bird)$$_hibernate_getInterceptor().writeObject(this, "bird", this.bird, paramBird);
    this.bird = localBird;
  }
}

Ahora ya funcionará @Basic(fetch=FetchType.LAZY)

FetchTest:48 - --------> Bird
SQL:92 - 
    select
        bird0_.id as id1_1_0_,
        bird0_.band as band2_1_0_,
        bird0_.breeder_id as breeder_4_1_0_,
        bird0_.cage_id as cage_id5_1_0_,
        bird0_.specie_id as specie_i6_1_0_ 
    from
        bird bird0_ 
    where
        bird0_.id=?
FetchTest:52 - --------> Picture - Lob Basic
SQL:92 - 
    select
        bird_.picture as picture3_1_ 
    from
        bird bird_ 
    where
        bird_.id=?

Y @LazyToOne(LazyToOneOption.NO_PROXY) en Cage.

FetchTest:84 - --------> Cage
SQL:92 - 
    select
        cage0_.id as id1_4_0_,
        cage0_.name as name2_4_0_ 
    from
        cage cage0_ 
    where
        cage0_.id=?
FetchTest:86 - --------> Bird bidirectional OneToOne
SQL:92 - 
    select
        bird0_.id as id1_1_0_,
        bird0_.band as band2_1_0_,
        bird0_.breeder_id as breeder_4_1_0_,
        bird0_.cage_id as cage_id5_1_0_,
        bird0_.specie_id as specie_i6_1_0_ 
    from
        bird bird0_ 
    where
        bird0_.cage_id=?

Sin embargo, la aplicación del bytecode enhancement tiene un impacto grande en todas las relaciones one-to-one y many-to-one de todas las clases de entidad si usamos una versión de Hibernate anterior a la 5.5 (junio 2021). Es la situación del proyecto de ejemplo. Debemos afrontar dos problemas.

  • Las relaciones simples que 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 propiedad Lazy de la entidad que no sea una colección, se traen todos los atributos Lazy. Por ejemplo, si la primera relación Lazy que obtenemos de Bird es Specie:
    FetchTest:50 - --------> Specie unidirectional OneToOne
    SQL:92 - 
        select
            bird_.breeder_id as breeder_4_1_,
            bird_.cage_id as cage_id5_1_,
            bird_.picture as picture3_1_,
            bird_.specie_id as specie_i6_1_ 
        from
            bird bird_ 
        where
            bird_.id=?
    SQL:92 - 
        select
            breeder0_.id as id1_3_0_,
            breeder0_.name as name2_3_0_ 
        from
            breeder breeder0_ 
        where
            breeder0_.id=?
    SQL:92 - 
        select
            cage0_.id as id1_4_0_,
            cage0_.name as name2_4_0_ 
        from
            cage cage0_ 
        where
            cage0_.id=?
    SQL:92 - 
        select
            specie0_.id as id1_6_0_,
            specie0_.name as name2_6_0_ 
        from
            specie specie0_ 
        where
            specie0_.id=?
    

    Para evitarlo, tenemos que recurrir a la anotación LazyGroup y organizar en grupos aquellos atributos de tipo lazy que deban obtenerse conjuntamente al mismo tiempo. Si queremos que se obtengan de uno en uno a medida que se soliciten con sus métodos get (lo más habitual), cada atributo tiene que constituir un grupo propio.

  • Teniendo en cuenta todo lo anterior, modifiquemos la clase Bird con el fin de que todas sus relaciones sean Lazy cuando aplicamos la mejora de bytecode. Para cada atributo se define un LazyGroup que permite obtener cada uno de forma independiente.

         // unidirectional
        @OneToOne(optional=false, fetch=FetchType.LAZY)   
        @LazyToOne(LazyToOneOption.NO_PROXY)
        @LazyGroup( "specie" )
        @JoinColumn(name = "specie_id")
        private Specie specie;
    
        // bidirectional
        @OneToOne(optional=false, fetch=FetchType.LAZY)   
        @LazyToOne(LazyToOneOption.NO_PROXY)
        @LazyGroup( "cage" )
        @JoinColumn(name = "cage_id")
        private Cage cage;
    
        // bidirectional
        @ManyToOne(fetch=FetchType.LAZY)  
        @LazyToOne(LazyToOneOption.NO_PROXY)
        @LazyGroup( "breeder" )
        @JoinColumn(name = "breeder_id")
        private Breeder breeder; 
    
    

    ¿Eager o Lazy?

    Como regla general, usaremos carga perezosa para evitar traernos de las tablas datos innecesarios. También parece razonable y cómodo recurrir el modo ansioso cuando queramos que la entidad siempre contenga la relación. No obstante, esta última situación no será nada frecuente, y aun así, tampoco es aconsejable el uso de relaciones Eager por los efectos colaterales que pueden tener en la ejecución de consultas JPQL o con Criteria API. También pueden causar un problema N + 1, tal y como explico aquí.

    Con todo, la carga perezosa no es la panacea. No caigamos en el error de trabajar despreocupadamente con los getters, pues la consecuencia será un sistema con un desempeño pobre. Esto se debe a dos motivos.

    • La carga progresiva de datos en modo Lazy invocando a getters va lanzando múltiples consultas que, probablemente, podrían ser reemplazadas por una sola que devuelva todo lo que vamos a necesitar.
    • Si la relación es una colección grande, cargarla entera con su get (o en modo Eager) es una barbaridad. En este caso, recomiendo no declararla en su lado one-to-many, salvo que la necesitemos para usarla en una consulta JPQL o Criteria API porque en ambos las reuniones o joins de entidades solo se pueden efectuar sobre relaciones. En Hibernate, con el lenguaje HQL no tenemos esta limitación.

    El objetivo será obtener únicamente aquella información que necesitemos en cada proceso de negocio con el menor número de consultas SQL y sorteando el conocido y temido problema «N + 1 consultas». Para conseguirlo, no podemos ser «perezosos»: hagamos el esfuerzo de escribir consultas que además sean eficientes. JPA provee los mecanismos adecuados (JPQL, Criteria API, SQL nativo), y puedes encontrarlos en los capítulos dedicados a JPA de mi curso Jakarta EE, entre otra mucha información imprescindible para sacar partido con eficiencia a JPA \ Hibernate.

    Código de ejemplo

    El proyecto completo se encuentra disponible en GitHub. Para más información sobre cómo utilizar GitHub, consultar este artículo.

    Otros tutoriales relacionados

    Persistencia en BD con Spring Data JPA

    SQL nativo con JPA e Hibernate

    JPA + Hibernate: Claves primarias

    Spring JDBC Template: simplificando el uso de SQL

    Persistencia en BD con Spring: Integrando JPA, c3p0, Hibernate y EHCache

    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 )

    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.