JPA e Hibernate: relaciones y atributos Lazy

logo hibernate

En JPA los atributos y relaciones que forman parte de una entidad pueden ser obtenidas desde la base de datos de dos formas distintas: en el momento en que obtengamos la entidad (EAGER) o bien bajo demanda (LAZY), esto es, la primera vez que se acceda a dichas propiedades o relaciones. Este modo LAZY es posible en Hibernate gracias al uso de objetos proxy que lanzan las consultas necesarias para la obtención de ese objeto, y su objetivo final es mejorar el rendimiento evitando obtener en nuestras aplicaciones datos que realmente no necesitamos (en ocasiones incluso podríamos terminar cargando toda la base de datos en memoria).

En este artículo vamos a revisar de forma práctica cómo hacer que Hibernate obtenga de forma LAZY las relaciones y atributos de nuestras entidades.

Master Pyhton, Java, Scala or Ruby

Proyecto de ejemplo

Para “jugar” con la definición de relaciones en Hibernate 5 vamos a utilizar una aplicación Maven estándar con Spring 4 basada en lo que hemos visto en otros tutoriales del blog, en concreto:

Se utiliza una base de datos MySQL cuyos parámetros se definen en el fichero /src/main/resources/db.properties. Ahí deberemos establecer la url de conexión completa, incluyendo el nombre de la base de datos de pruebas, el usuario y la contraseña. Esta base de datos, incluyendo datos de pruebas, se crea con el script birds.sql incluido en el directorio raíz del proyecto.

La configuración de Spring se realiza de forma programática en la clase ApplicationContext. El datasource utiliza como implementación la proporcionada por el pool de conexiones c3p0.

El proyecto tiene la siguiente estructura.

Modelo de entidades

Procedemos a crear un modelo de datos con entidades JPA que contengan a modo de ejemplo las relaciones más habituales.

Este es el mapeo de relaciones aplicado:

  • 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 define la 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 entidad Bird tenemos un atributo para almacenar un fichero, y queremos que este se obtenga de forma lazy cuando sea necesario.
    @Basic(fetch=FetchType.LAZY)
    @Lob
    private byte[] picture;
    

Probando las relaciones

El proyecto de ejemplo nos permitirá comprobar el comportamiento por defecto de las relaciones y atributos definidos en nuestras entidades, y modificar este comportamiento para que su obtención pueda ser LAZY si así lo necesitamos. Me adelanto al resultado ya que Hibernate respeta lo especificado por el estándar JPA:

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

Usaremos el siguiente test.

package com.danielme.blog.hibernatefetching;

import org.apache.log4j.Logger;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.transaction.annotation.Transactional;

import com.danielme.blog.hibernatefetching.ApplicationContext;
import com.danielme.blog.hibernatefetching.entities.Award;
import com.danielme.blog.hibernatefetching.entities.Bird;
import com.danielme.blog.hibernatefetching.entities.Breeder;
import com.danielme.blog.hibernatefetching.entities.Cage;
import com.danielme.blog.hibernatefetching.repositories.AwardRepository;
import com.danielme.blog.hibernatefetching.repositories.BirdRepository;
import com.danielme.blog.hibernatefetching.repositories.BreederRepository;
import com.danielme.blog.hibernatefetching.repositories.CageRepository;
import com.danielme.blog.hibernatefetching.repositories.TreatmentRepository;

/**
 * Some test cases.
 * 
 * @author danielme.com
 * 
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { ApplicationContext.class })
@Transactional
public class FetchTest {

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

	@Autowired
	private BirdRepository birdRepository;
	@Autowired
	private AwardRepository awardRepository;
	@Autowired
	private BreederRepository breederRepository;
	@Autowired
	private CageRepository cageRepository;
	@Autowired
	private TreatmentRepository treatmentRepository;

	@Test
	public void testBird() {
		log.info("--------> Bird");
		Bird bird = birdRepository.findOne(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 = awardRepository.findOne(1L);
		log.info("--------> Bird bidirectional @ManyToOne");
		award.getBird().getBand();
	}

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

	@Test
	public void testCage() {
		log.info("--------> Cage");
		Cage cage = cageRepository.findOne(1l);
		log.info("--------> Bird bidirectional OneToOne");
		cage.getBird().getBand();
	}

	@Test
	public void testTeatments() {
		treatmentRepository.findOne(1l).getBirds().size();
	}

}

Para cada entidad se obtiene una instancia de la base de datos gracias a Spring Data JPA, y a continuación vamos solicitando cada una de sus relaciones una a una (se presupone que todas las relaciones tienen datos). Para que la obtención de relaciones funcione necesitamos tener una sesión abierta en Hibernate, por este motivo el test se encuentra anotado con @Transactional. El comportamiento de Hibernate lo vamos a revisar en el log, configurado con log4j del siguiente modo para que sólo veamos las consultas ejecutadas contra la base de datos y los mensajes informativos que hemos puesto en el propio test.

<?xml version="1.0" encoding="UTF-8" ?>
 
<!DOCTYPE log4j:configuration PUBLIC
  "-//APACHE//DTD LOG4J 1.2//EN" "http://logging.apache.org/log4j/1.2/apidocs/org/apache/log4j/xml/doc-files/log4j.dtd">
 
<!-- http://wiki.apache.org/logging-log4j/Log4jXmlFormat -->
 
<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">
     
    <appender name="console" class="org.apache.log4j.ConsoleAppender">
        <param name="Target" value="System.out" />
        <layout class="org.apache.log4j.PatternLayout">
            <param name="ConversionPattern" value="%c{1}:%L - %m%n" />
        </layout>
    </appender>    

	<!-- prints the SQL (Prepared Statement) generated by Hibernate -->
   	<logger name="org.hibernate.SQL">
		<level value="debug" />
	</logger>
	
	<!-- prints the querys parameters -->
	<logger name="org.hibernate.type">
		<level value="debug" />
	</logger>  
	
	<logger name="com.danielme.blog.hibernatefetching">
		<level value="info" />
	</logger> 
 
    <root>
        <priority value="error" />
        <appender-ref ref="console" />
    </root>
 
</log4j:configuration>

Ejecutando los tests podemos observar el comportamiento de cada mapeo.

OneToOne

Todas las relaciones de este tipo se comportan por defecto de modo EAGER. Por ejemplo, al recuperarse un bird nos traemos mediante joins la especie y la 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=?

Esto es debido a que Hibernate necesita saber si existe un objeto en el otro extremo de la relación. De hecho, aunque marquemos la relación con Fetch Lazy, Hibernate seguirá trayendo la relación.

 @OneToOne(fetch=FetchType.LAZY)

La anotación @OneToOne admite el atributo optional que permite indicar si la relación debe existir siempre o puede ser nula. Si indicamos que la relación no puede ser nula, Hibernate ya no tiene que comprobar su existencia y la relación puede obtenerse de forma Lazy. Esto sólo funciona en el extremo de la relación en el que tenemos la clave ajena, es decir, donde no tenemos definido el atributo mappedBy.

En nuestro ejemplo, podemos hacer Lazy la relación unidireccional entre Bird y Specie con el siguiente mapeo.

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

Y podemos comprobarlo en el log.

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=?

Lo mismo es aplicable a la relación de Bird con Cage pero sólo en el lado de Bird.

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

Obsérvese que al traernos Cage de forma Lazy después de haber obtenido el Bird, Cage vuelve a traerse el Bird de la relació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=?

Para poder hacer Lazy el extremo Cage de la relación tenemos que recurrir a la anotación propia de Hibernate LazyToOne.

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

Además, tenemos que aplicar bytecode enhancement (lo veremos al final de este artículo) para hacer funcionar LazyToOne, aunque en versiones anteriores a Hibernate 5.1 se podía simplemente implementar en la entidad la interfaz FieldHandled. Aquí tenéis un ejemplo.

Como alternativa al uso del LazyToOne, debemos evaluar si realmente la relación debe ser bidireccional o si podemos hacer otro mapeo alternativo. Esta última solución podemos aplicarla en nuestro mapeo haciendo que el extremo de la relación en Cage sea un OneToMany y, por tanto, una colección que por defecto es LAZY. Desde fuera de la clase ocultaremos este “apaño” y trabajaremos con un 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 LinkedList<>();
        }
        birds.set(0, bird);
    }    

Con este mapeo tenemos una relación OneToOne bidireccional y LAZY en ambos extremos (recordad que en Bird hemos mapeado Cage como @OneToOne(optional=false, fetch=FetchType.LAZY) ):

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

Finalmente ¿qué pasaría si marcamos el atributo como obligatorio (optional=false) pero realmente no lo es (puede valer nulo)? Pues que si establecemos el atributo a null Hibernate lanzará una excepción.

org.hibernate.PropertyValueException: not-null property references a null or transient value

Por tanto, en este escenario un OneToOne nunca será LAZY si no usamos bytecode enhancement o recurrimos a un mapeo alternativo como el que hemos visto anteriormente con @OneToMany y/o @ManyToOne en función de si queremos que sea bidireccional o unidireccional. En nuestro ejemplo, podemos mapear la relación de Bird hacia Cage como sigue para que sea LAZY y opcional (recordad que nen el extremo Cage hemos utilizado un @OneToMany).

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

OneToMany

Se comporta por defecto como LAZY, podemos verlo 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 por defecto 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=?

A diferencia de las relaciones OneToOne, en este tipo de relación simplemente tenemos que definirla como LAZY.

    @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

Se obtienen por defecto en modo 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=?

Atributos

Aunque los atributos o propiedades pueden configurarse como LAZY con la anotación Basic, Hibernate los obtiene 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 generalmente sólo queremos obtener de forma explícita cuando sea estrictamente necesario. En nuestro ejemplo cada Bird puede tener una imagen, y si simplemente obtenemos un listado de los mismos estaremos obteniendo esos ficheros con el problema de rendimiento que esto puede suponer.

Para habilitar la obtención LAZY en los atributos, tenemos recurrir al bytecode enhancement. La alternativa es crear una entidad aparte para mapear los ficheros y obtenerlos con una relación que admita LAZY, como por ejemplo ManyToOne. Otra posibilidad es utilizar proyecciones y decidir en las consultas las propiedades que queremos traer.

A modo de ejemplo, vamos a crear una entidad Picture y 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;
	}	
	
}

Para relacionar la entidad Bird con Picture se puede utilizar un mapeo ManyToOne ya que según vimos anteriormente los OneToOne opcionales no puede ser LAZY.

 @ManyToOne(fetch=FetchType.LAZY)
 @JoinColumn(name = "picture_id")
 private Picture picture;

Bytecode Enhancement

Esta técnica permite procesar el bytecode generado por el compilador para añadir de forma automática nuevas funcionalidades al .class que finalmente ejecutaremos. Hibernate desde la versión 4.2.9 proporciona un plugin para Maven que permite aplicar automáticamente Bytecode enhancement a nuestro modelo mediante el uso de la librería javassist. Para utilizarlo tan sólo tenemos que añadirlo a nuestro pom activando la opción enableLazyInitialization que es la que necesitamos para poder realizar la carga Lazy de cualquier relación.

	         <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>

Si decompilamos nuestras entidades podemos ver los cambios realizados por este 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 nos 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 los demás mapeos:

  • Todas las relaciones que no sean colecciones y queramos cargar como LAZY deben anotarse con @LazyToOne con la opción NO_PROXY
  • 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 evitar esto tenemos que recurrir a la anotación LazyGroup que permite agrupar los atributos/relaciones LAZY que deben obtenerse la primera vez que se solicite obtener uno de los atributos de dicho grupo. Lamentablemente esta anotación sólo está disponible a partir de Hibernate 5.1 tal y recoge la entrada en JIRA.

  • Teniendo en cuenta estos dos puntos, podemos reescribir la entidad Bird para que todas sus relaciones sean LAZY si aplicamos bytecode enhancement con los siguientes mapeos. 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; 
    
    

    Conclusiones

    Gracias a bytecode enhancement podemos conseguir que cualquier relación y atributo se obtenga de forma LAZY aunque este no sea el comportamiento implementado por Hibernate. No obstante, esta técnica tiene efectos colaterales por lo que mi recomendación personal es intentar en la medida de lo posible utilizar los mapeos alternativos que hemos visto como por ejemplo simular las relaciones OneToOne como OneToMany y ManyToOne.

    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 con Hibernate y/o Spring

    JPA + Hibernate: Claves primarias

    Spring JDBC Template: simplificando el uso de SQL

    Persistencia en BD con Spring Data JPA

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

    Testing Spring con JUnit 4

    Ficheros .properties en Spring IoC

    Cursos aplicaciones móviles

    Responder

    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. Cerrar sesión / Cambiar )

    Imagen de Twitter

    Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

    Foto de Facebook

    Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

    Google+ photo

    Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

    Conectando a %s

    A %d blogueros les gusta esto: