Buscar este blog

miércoles, 4 de julio de 2012

Hibernate, OneToOne y falsos Lazy

Hace poco estaba extrañada con la lentitud con que se ejecutaba un proceso en una aplicación que utiliza Hibernate, por lo que me puse a investigar un poco las consultas que se estaban ejecutando y como estaban hechos los mapeos, por ver si encontraba algo raro.
Comprobé que todas las relaciones estaban puestas con lazy (bien, primer paso comprobado) y entonces pasé a ver las sql que se estaban lanzando, y cual fue mi sorpresa al ver que, en una relación OneToOne, aunque estuviera especificado que la carga fuera lazy, Hibernate estaba lanzando una consulta para cargar esa entidad por cada registro encontrado.
Googleleando descubrí que Hibernate tiene un problema con las relaciones OneToOne y que si en la tabla hija de una relación OneToOne le dices que quieres carga lazy, él te dice que bien, que vale, que te da la opción de ponerlo, pero la ignora finamente.
Por poner un ejemplo, si tenemos estas dos clases:
@Entity
public class Persona {

 @OneToOne(optional = false)
 private Casa casa;

 public Casa getCasa() {
  return casa;
 }

 public void setCasa(Casa casa) {
  this.casa = casa;
 }
}

@Entity
public class Casa {

 @OneToOne(fetch = FetchType.LAZY, optional = true, mappedBy = "animal")
 private Persona persona;

 public Persona getPersona() {
  return persona;
 }

 public void setPersona(Persona persona) {
  this.persona = persona;
 }
 
}
Si hacemos una consulta sobre "Casa", la idea es que la persona no se cargue hasta que queramos acceder a ella, pero esto no es así, Hibernate realizará una consulta extra por cada registro de casa para cargar las personas (lo que, si se encadena en cascada, puede afectar gravemente al rendimiento) . Este comportamiento, según Hibernate, se basa en que, sin realizar la consulta, Hibernate no sabe si debe dejar la persona a null o con el proxy.
Y, ante esto, ¿que hacer? Pues la mejor solución que he encontrado es "engañar" a Hibernate para que crea que ha cargado la entidad, y que de esta forma no haga los select extra. Para ello, en el modelo donde se produce el problema (en nuestro ejemplo, Casa) se tiene que implementar la interfaz  FieldHandled,  añadir el atributo FieldHandler fieldHandler y realizar los siguiente cambios en la definición y en los getters/setters de las propiedades afectadas por el problema lazy:
@Entity
public class Casa implements FieldHandled {
 private FieldHandler fieldHandler;

 @OneToOne(fetch = FetchType.LAZY, optional = true, mappedBy = "animal")
 @LazyToOne(LazyToOneOption.NO_PROXY)
 private Persona persona;

 public Persona getPersona() {
  if (fieldHandler != null) {
            return (Persona) fieldHandler.readObject(this, "persona", persona);
  }
  return persona;
 }

 public void setPersona(Persona persona) {
  if (fieldHandler != null) {
            this.persona = (Persona) fieldHandler.writeObject(this, "persona", persona, persona);
            return;
        }
  this.persona = persona;
 }
 
}
De esta forma, hasta que no querramos a conciencia acceder a la Persona a través del get no se cargará el objeto (es decir, que será lazy de verdad :) ).
Enlace al artículo original en inglés de esta solución: Lazy one-to-one inverse relationships in Hibernate