¿Cómo puedo hacer que una relación JPA OneToOne sea perezosa?

212

En esta aplicación que estamos desarrollando, notamos que una vista era particularmente lenta. Perfilé la vista y noté que había una consulta ejecutada por hibernate que tardó 10 segundos incluso si solo había dos objetos en la base de datos para recuperar. Todo OneToManyy las ManyToManyrelaciones eran flojas, así que ese no era el problema. Al inspeccionar el SQL real que se está ejecutando, noté que había más de 80 uniones en la consulta.

Al inspeccionar aún más el problema, noté que el problema fue causado por la profunda jerarquía OneToOney las ManyToOnerelaciones entre las clases de entidad. Entonces, pensé, los haré ir perezosamente, eso debería resolver el problema. Pero anotar @OneToOne(fetch=FetchType.LAZY)o @ManyToOne(fetch=FetchType.LAZY)no parece funcionar. O recibo una excepción o no se reemplazan realmente con un objeto proxy y, por lo tanto, son flojos.

¿Alguna idea de cómo voy a hacer que esto funcione? Tenga en cuenta que no uso el persistence.xmlpara definir relaciones o detalles de configuración, todo se hace en código java.

Kim L
fuente

Respuestas:

218

En primer lugar, algunas aclaraciones a la respuesta de KLE :

  1. La asociación uno a uno sin restricciones (anulable) es la única que no se puede representar sin instrumentación de bytecode. La razón de esto es que la entidad propietaria DEBE saber si la propiedad de asociación debe contener un objeto proxy o NULL y no puede determinar eso mirando las columnas de su tabla base debido a que normalmente se asigna uno a uno a través de PK compartida, por lo que tiene que ser traído con entusiasmo de todos modos haciendo que el proxy no tenga sentido. Aquí hay una explicación más detallada .

  2. las asociaciones de muchos a uno (y uno a muchos, obviamente) no sufren este problema. La entidad propietaria puede verificar fácilmente su propio FK (y en el caso de uno a muchos, el proxy de colección vacío se crea inicialmente y se completa a pedido), por lo que la asociación puede ser perezosa.

  3. Reemplazar uno a uno por uno a muchos nunca es una buena idea. Puede reemplazarlo con muchos a uno únicos, pero hay otras opciones (posiblemente mejores).

Rob H. tiene un punto válido, sin embargo, es posible que no pueda implementarlo dependiendo de su modelo (por ejemplo, si su asociación uno a uno es anulable).

Ahora, en cuanto a la pregunta original:

A) @ManyToOne(fetch=FetchType.LAZY)debería funcionar bien. ¿Estás seguro de que no se sobrescribe en la consulta en sí? Es posible especificar join fetchen HQL y / o establecer explícitamente el modo de búsqueda a través de Criteria API, que tendría prioridad sobre la anotación de clase. Si ese no es el caso y todavía tiene problemas, publique sus clases, consultas y el SQL resultante para una conversación más precisa.

B) @OneToOnees más complicado. Si definitivamente no es anulable, vaya con la sugerencia de Rob H. y especifíquelo como tal:

@OneToOne(optional = false, fetch = FetchType.LAZY)

De lo contrario, si puede cambiar su base de datos (agregue una columna de clave externa a la tabla de propietarios), hágalo y asignela como "unida":

@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name="other_entity_fk")
public OtherEntity getOther()

y en OtherEntity:

@OneToOne(mappedBy = "other")
public OwnerEntity getOwner()

Si no puede hacer eso (y no puede vivir con una búsqueda ansiosa), la instrumentación de código de bytes es su única opción. Sin embargo, tengo que estar de acuerdo con CPerkins , ¡si tienes 80! se une debido a las ansiosas asociaciones OneToOne, tienes mayores problemas que esto :-)

ChssPly76
fuente
Tal vez haya otra opción, pero no la he probado personalmente: en el lado no restringido, use a one-to-onecon una fórmula como select other_entity.id from other_entity where id = other_entity.id. Por supuesto, esto no es ideal para realizar consultas.
Frédéric
1
opcional = falso, no funciona para mí. @OneToOne (fetch = FetchType.LAZY, mappedBy = "fundSeries", opcional = false) privado FundSeriesDetailEntity fundSeriesDetail;
Oleg Kuts el
21

Para que la carga diferida funcione en asignaciones uno a uno anulables, debe dejar que Hibernate compile instrumentación de tiempo y agregue un @LazyToOne(value = LazyToOneOption.NO_PROXY)a la relación uno a uno.

Ejemplo de mapeo:

@OneToOne(fetch = FetchType.LAZY)  
@JoinColumn(name="other_entity_fk")
@LazyToOne(value = LazyToOneOption.NO_PROXY)
public OtherEntity getOther()

Extensión de archivo Ant Build de ejemplo (para hacer la instrumentación de tiempo de compilación de Hibernate):

<property name="src" value="/your/src/directory"/><!-- path of the source files --> 
<property name="libs" value="/your/libs/directory"/><!-- path of your libraries --> 
<property name="destination" value="/your/build/directory"/><!-- path of your build directory --> 

<fileset id="applibs" dir="${libs}"> 
  <include name="hibernate3.jar" /> 
  <!-- include any other libraries you'll need here --> 
</fileset> 

<target name="compile"> 
  <javac srcdir="${src}" destdir="${destination}" debug="yes"> 
    <classpath> 
      <fileset refid="applibs"/> 
    </classpath> 
  </javac> 
</target> 

<target name="instrument" depends="compile"> 
  <taskdef name="instrument" classname="org.hibernate.tool.instrument.javassist.InstrumentTask"> 
    <classpath> 
      <fileset refid="applibs"/> 
    </classpath> 
  </taskdef> 

  <instrument verbose="true"> 
    <fileset dir="${destination}"> 
      <!-- substitute the package where you keep your domain objs --> 
      <include name="/com/mycompany/domainobjects/*.class"/> 
    </fileset> 
  </instrument> 
</target>
Kdeveloper
fuente
3
¿Por qué LazyToOneOption.NO_PROXYy no LazyToOneOption.PROXY?
Telmo Marques
Esto no responde al "por qué", pero este hecho también se afirma aquí (hacia el final de la sección "Mapeo típico"): vladmihalcea.com/…
DanielM
12

La idea básica detrás de los XToOnes en Hibernate es que no son flojos en la mayoría de los casos.

Una razón es que, cuando Hibernate tiene que decidir poner un proxy (con la identificación) o un valor nulo,
tiene que buscar en la otra tabla de todos modos para unirse. El costo de acceder a la otra tabla en la base de datos es significativo, por lo que también podría obtener los datos de esa tabla en ese momento (comportamiento no perezoso), en lugar de obtener eso en una solicitud posterior que requeriría un segundo acceso al misma mesa

Editado: para más detalles, consulte la respuesta de ChssPly76 . Este es menos preciso y detallado, no tiene nada que ofrecer. Gracias ChssPly76.

KLE
fuente
Hay varias cosas mal aquí - he proporcionado otra respuesta a continuación con una explicación (demasiadas cosas, no cabe en un comentario)
ChssPly76
8

Aquí hay algo que me ha funcionado (sin instrumentación):

En lugar de usar @OneToOneen ambos lados, uso @OneToManyen la parte inversa de la relación (la que tiene mappedBy). Eso hace que la propiedad sea una colección ( Listen el ejemplo a continuación), pero la traduzco en un elemento en el captador, haciéndola transparente para los clientes.

Esta configuración funciona de manera perezosa, es decir, las selecciones solo se realizan cuando getPrevious()o getNext()se llaman, y solo una selección para cada llamada.

La estructura de la mesa:

CREATE TABLE `TB_ISSUE` (
    `ID`            INT(9) NOT NULL AUTO_INCREMENT,
    `NAME`          VARCHAR(255) NULL,
    `PREVIOUS`      DECIMAL(9,2) NULL
    CONSTRAINT `PK_ISSUE` PRIMARY KEY (`ID`)
);
ALTER TABLE `TB_ISSUE` ADD CONSTRAINT `FK_ISSUE_ISSUE_PREVIOUS`
                 FOREIGN KEY (`PREVIOUS`) REFERENCES `TB_ISSUE` (`ID`);

La clase:

@Entity
@Table(name = "TB_ISSUE") 
public class Issue {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    protected Integer id;

    @Column
    private String name;

    @OneToOne(fetch=FetchType.LAZY)  // one to one, as expected
    @JoinColumn(name="previous")
    private Issue previous;

    // use @OneToMany instead of @OneToOne to "fake" the lazy loading
    @OneToMany(mappedBy="previous", fetch=FetchType.LAZY)
    // notice the type isnt Issue, but a collection (that will have 0 or 1 items)
    private List<Issue> next;

    public Integer getId() { return id; }
    public String getName() { return name; }

    public Issue getPrevious() { return previous; }
    // in the getter, transform the collection into an Issue for the clients
    public Issue getNext() { return next.isEmpty() ? null : next.get(0); }

}
acdcjunior
fuente
7

Como expliqué en este artículo , a menos que esté utilizando Bytecode Enhancement , no puede obtener perezosamente la asociación del lado primario @OneToOne.

Sin embargo, la mayoría de las veces, ni siquiera necesita la asociación del lado de los padres si usa @MapsIdel lado del cliente:

@Entity(name = "PostDetails")
@Table(name = "post_details")
public class PostDetails {

    @Id
    private Long id;

    @Column(name = "created_on")
    private Date createdOn;

    @Column(name = "created_by")
    private String createdBy;

    @OneToOne(fetch = FetchType.LAZY)
    @MapsId
    private Post post;

    public PostDetails() {}

    public PostDetails(String createdBy) {
        createdOn = new Date();
        this.createdBy = createdBy;
    }

    //Getters and setters omitted for brevity
}

Con @MapsId, la idpropiedad en la tabla secundaria sirve como clave primaria y clave externa para la clave primaria de la tabla primaria.

Por lo tanto, si tiene una referencia a la Postentidad principal , puede buscar fácilmente la entidad secundaria utilizando el identificador de la entidad principal:

PostDetails details = entityManager.find(
    PostDetails.class,
    post.getId()
);

De esta manera, no tendrá problemas de consulta N + 1 que podrían ser causados ​​por la mappedBy @OneToOneasociación en el lado primario.

Vlad Mihalcea
fuente
de esta manera no podemos seguir en cascada de operaciones de padres a hijos: /
Hamdi
Para persistir, es solo una llamada de persistencia adicional, para eliminar, puede usar la cascada DDL.
Vlad Mihalcea
6

En las asignaciones XML de Hibernate nativas, puede lograr esto declarando una asignación uno a uno con el atributo restringido establecido en verdadero. No estoy seguro de cuál es el equivalente de anotación de Hibernate / JPA de eso, y una búsqueda rápida del documento no proporcionó respuesta, pero espero que eso te dé una pista para continuar.

Rob H
fuente
55
+1 para una buena sugerencia; desafortunadamente no siempre es aplicable, ya que el modelo de dominio puede requerir nulabilidad La forma correcta de mapear esto a través de anotaciones es@OneToOne(optional=false,fetch=FetchMode.LAZY)
ChssPly76
Intenté esto y no vi ninguna mejora en el rendimiento. Todavía vi muchas consultas en la salida de hibernación a través del depurador.
P.Brian.Mackey
3

Como ya explica perfectamente ChssPly76, los proxies de Hibernate no ayudan con asociaciones uno a uno sin restricciones (anulables), PERO hay un truco explicado aquí para evitar configurar la instrumentación. La idea es engañar a Hibernate porque la clase de entidad que queremos usar ya ha sido instrumentada: lo instrumenta manualmente en el código fuente. ¡Es fácil! Lo implementé con CGLib como proveedor de código de bytes y funciona (asegúrese de configurar lazy = "no-proxy" y fetch = "select", no "join", en su HBM).

Creo que esta es una buena alternativa a la instrumentación real (me refiero a la automática) cuando solo tiene una relación anulable de uno a uno que desea hacer vago. El principal inconveniente es que la solución depende del proveedor de código de bytes que esté utilizando, así que comente su clase con precisión porque podría tener que cambiar el proveedor de código de bytes en el futuro; por supuesto, también está modificando su bean modelo por una razón técnica y esto no está bien.

Pino
fuente
1

Esta pregunta es bastante antigua, pero con Hibernate 5.1.10, hay una nueva solución más cómoda.

La carga diferida funciona a excepción del lado primario de una asociación @OneToOne. Esto se debe a que Hibernate no tiene otra forma de saber si asignar un valor nulo o un proxy a esta variable. Más detalles puedes encontrar en este artículo

  • Puede activar la mejora de código de bytes de carga diferida
  • O bien, puede eliminar el lado primario y usar el lado del cliente con @MapsId como se explica en el artículo anterior. De esta forma, descubrirá que realmente no necesita el lado primario, ya que el niño comparte la misma identificación con el padre para que pueda buscarlo fácilmente al conocer la identificación principal.
Toumi
fuente
0

Si la relación no debe ser bidireccional, una @ElementCollection podría ser más fácil que usar una colección perezosa de One2Many.

Stefan
fuente