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&useSSL=false&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.
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