Hibernate Criteria devuelve hijos varias veces con FetchType.EAGER

115

Tengo una Orderclase que tiene una lista de OrderTransactionsy la asigné con un mapeo de Hibernate de uno a muchos así:

@OneToMany(targetEntity = OrderTransaction.class, cascade = CascadeType.ALL)
public List<OrderTransaction> getOrderTransactions() {
    return orderTransactions;
}

Estos Ordermensajes también tienen un campo orderStatus, que se utiliza para filtrar con los siguientes criterios:

public List<Order> getOrderForProduct(OrderFilter orderFilter) {
    Criteria criteria = getHibernateSession()
            .createCriteria(Order.class)
            .add(Restrictions.in("orderStatus", orderFilter.getStatusesToShow()));
    return criteria.list();
}

Esto funciona y el resultado es el esperado.

Ahora, aquí está mi pregunta : ¿Por qué, cuando establezco el tipo de búsqueda explícitamente en EAGER, aparecen las Orders varias veces en la lista resultante?

@OneToMany(targetEntity = OrderTransaction.class, fetch = FetchType.EAGER, cascade = CascadeType.ALL)
public List<OrderTransaction> getOrderTransactions() {
    return orderTransactions;
}

¿Cómo tendría que cambiar mi código de Criterios para alcanzar el mismo resultado con la nueva configuración?

Raoulsson
fuente
1
¿Ha intentado habilitar show_sql para ver lo que sucede debajo?
Mirko N.
Agregue también el código de clases OrderTransaction y Order. \
Eran Medan

Respuestas:

115

Este es realmente el comportamiento esperado si entendí su configuración correctamente.

Obtiene la misma Orderinstancia en cualquiera de los resultados, pero como ahora está haciendo una combinación con OrderTransaction, debe devolver la misma cantidad de resultados que una combinación SQL regular

Entonces, en realidad, debería aparecer varias veces. Esto se explica muy bien por el propio autor (Gavin King) aquí : explica por qué y cómo obtener resultados distintos


También se menciona en las preguntas frecuentes de Hibernate :

Hibernate no devuelve resultados distintos para una consulta con la búsqueda de combinación externa habilitada para una colección (incluso si uso la palabra clave distinta). Primero, debe comprender SQL y cómo funcionan las OUTER JOIN en SQL. Si no comprende y comprende completamente las combinaciones externas en SQL, no continúe leyendo este artículo de preguntas frecuentes, pero consulte un manual o tutorial de SQL. De lo contrario, no comprenderá la siguiente explicación y se quejará de este comportamiento en el foro de Hibernate.

Ejemplos típicos que pueden devolver referencias duplicadas del mismo objeto Order:

List result = session.createCriteria(Order.class)
                    .setFetchMode("lineItems", FetchMode.JOIN)
                    .list();

<class name="Order">
    ...
    <set name="lineItems" fetch="join">

List result = session.createCriteria(Order.class)
                       .list();
List result = session.createQuery("select o from Order o left join fetch o.lineItems").list();

Todos estos ejemplos producen la misma declaración SQL:

SELECT o.*, l.* from ORDER o LEFT OUTER JOIN LINE_ITEMS l ON o.ID = l.ORDER_ID

¿Quieres saber por qué hay duplicados? Mire el conjunto de resultados de SQL, Hibernate no oculta estos duplicados en el lado izquierdo del resultado combinado externo, pero devuelve todos los duplicados de la tabla inicial. Si tiene 5 pedidos en la base de datos y cada pedido tiene 3 elementos de línea, el conjunto de resultados será de 15 filas. La lista de resultados de Java de estas consultas tendrá 15 elementos, todos de tipo Order. Hibernate solo creará 5 instancias de Order, pero los duplicados del conjunto de resultados de SQL se conservan como referencias duplicadas a estas 5 instancias. Si no comprende esta última oración, debe leer sobre Java y la diferencia entre una instancia en el montón de Java y una referencia a dicha instancia.

(¿Por qué una combinación externa a la izquierda? Si tuviera un pedido adicional sin líneas de pedido, el conjunto de resultados sería 16 filas con NULL llenando el lado derecho, donde los datos de la línea de pedido son para otro pedido. Desea pedidos incluso si no tienen líneas de pedido, ¿verdad? De lo contrario, utilice una búsqueda de combinación interna en su HQL).

Hibernate no filtra estas referencias duplicadas de forma predeterminada. Algunas personas (no tú) realmente quieren esto. ¿Cómo puedes filtrarlos?

Me gusta esto:

Collection result = new LinkedHashSet( session.create*(...).list() );
Eran Medan
fuente
121
Incluso si comprende la siguiente explicación, es posible que se queje de este comportamiento en el foro de Hibernate, ¡porque es un comportamiento estúpido!
Tom Anderson
17
Muy bien Tom, me había olvidado de la actitud arrogante de Gavin Kings. También dice que 'Hibernate no filtra estas referencias duplicadas de forma predeterminada. Algunas personas (no tú) realmente quieren esto. Me interesaría cuando la gente realmente lo haga.
Paul Taylor
16
@TomAnderson sí exactamente. ¿Por qué alguien necesitaría esos duplicados? Lo pregunto por pura curiosidad, ya que no tengo ni idea ... Puedes crear duplicados tú mismo, tantos como desees .. ;-)
Parobay
13
Suspiro. En realidad, esto es un defecto de Hibernate, en mi humilde opinión. Quiero optimizar mis consultas, así que paso de "seleccionar" a "unir" en mi archivo de mapeo. De repente, mi código se ROMPE por todos lados. Luego corro y arreglo todos mis DAO agregando transformadores de resultados y todo eso. Experiencia de usuario == muy negativa. Entiendo que a algunas personas les encanta tener duplicados por razones extrañas, pero ¿por qué no puedo decir "busca estos objetos MÁS RÁPIDO pero no me molestes con duplicados" especificando fetch = "justworkplease"?
Roman Zenka
@Eran: Estoy enfrentando un tipo de problema similar. No obtengo objetos principales duplicados, pero obtengo elementos secundarios en cada objeto principal repetidos tantas veces como haya una cantidad de objetos principales en la respuesta. ¿Alguna idea de por qué este problema?
mantri
93

Además de lo mencionado por Eran, otra forma de obtener el comportamiento que desea es configurar el transformador de resultado:

criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);
EJB
fuente
8
Esto funcionará para la mayoría de los casos ... excepto cuando intente utilizar Criterios para obtener 2 colecciones / asociaciones.
JamesD
42

tratar

@Fetch (FetchMode.SELECT) 

por ejemplo

@OneToMany(targetEntity = OrderTransaction.class, fetch = FetchType.EAGER, cascade = CascadeType.ALL)
@Fetch (FetchMode.SELECT)
public List<OrderTransaction> getOrderTransactions() {
return orderTransactions;

}

Mathi
fuente
11
FetchMode.SELECT aumenta el número de consultas SQL lanzadas por Hibernate pero asegura solo una instancia por registro de entidad raíz. Hibernate disparará una selección para cada registro secundario en este caso. Por lo tanto, debe tenerlo en cuenta con respecto a las consideraciones de rendimiento.
Bipul
1
@BipulKumar sí, pero esta es la opción cuando no podemos usar la recuperación diferida porque necesitamos mantener una sesión de recuperación diferida para acceder a los objetos secundarios.
mathi
18

No use List y ArrayList, sino Set y HashSet.

@OneToMany(targetEntity = OrderTransaction.class, cascade = CascadeType.ALL)
public Set<OrderTransaction> getOrderTransactions() {
    return orderTransactions;
}
Αλέκος
fuente
2
¿Es esta una mención incidental de una mejor práctica de Hibernate o es relevante para la pregunta de recuperación de varios niños del OP?
Jacob Zwiers
Entendido. Secundario a la pregunta del OP. Sin embargo, el artículo de dzone probablemente debería tomarse con un grano de sal ... basado en la propia admisión del autor en los comentarios.
Jacob Zwiers
2
Esta es una muy buena respuesta en mi opinión. Si no desea duplicados, es muy probable que prefiera usar un conjunto que una lista: usar un conjunto (e implementar métodos correctos iguales / hascode, por supuesto) resolvió el problema por mí. Tenga cuidado al implementar hashcode / equals, como se indica en el documento de redhat, no usar el campo id.
Mat
1
Gracias por tu OMI. Además, no se meta en problemas creando métodos equals () y hashCode (). Deje que su IDE o Lombok los generen por usted.
Αλέκος
3

Usando Java 8 y Streams agrego en mi método de utilidad esta declaración de devolución:

return results.stream().distinct().collect(Collectors.toList());

Las secuencias eliminan los duplicados muy rápido. Yo uso la anotación en mi clase Entity así:

@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
@JoinTable(name = "STUDENT_COURSES")
private List<Course> courses;

Creo que es mejor en mi aplicación usar la sesión en el método donde necesito datos de la base de datos. Cierre la sesión cuando termine. Por supuesto, configuré mi clase Entity para usar el tipo de búsqueda leasy. Voy a refactorizar.

easyScript
fuente
3

Tengo el mismo problema para buscar 2 colecciones asociadas: el usuario tiene 2 roles (Conjunto) y 2 comidas (Lista) y las comidas están duplicadas.

@Table(name = "users")
public class User extends AbstractNamedEntity {

   @CollectionTable(name = "user_roles", joinColumns = @JoinColumn(name = "user_id"))
   @Column(name = "role")
   @ElementCollection(fetch = FetchType.EAGER)
   @BatchSize(size = 200)
   private Set<Role> roles;

   @OneToMany(fetch = FetchType.LAZY, mappedBy = "user")
   @OrderBy("dateTime DESC")
   protected List<Meal> meals;
   ...
}

DISTINCT no ayuda (consulta DATA-JPA):

@EntityGraph(attributePaths={"meals", "roles"})
@QueryHints({@QueryHint(name= org.hibernate.jpa.QueryHints.HINT_PASS_DISTINCT_THROUGH, value = "false")}) // remove unnecessary distinct from select
@Query("SELECT DISTINCT u FROM User u WHERE u.id=?1")
User getWithMeals(int id);

Por fin he encontrado 2 soluciones:

  1. Cambiar lista a LinkedHashSet
  2. Use EntityGraph con solo el campo "comida" y escriba LOAD, que cargan los roles como lo declararon (EAGER y por BatchSize = 200 para evitar el problema N + 1):

Solución final:

@EntityGraph(attributePaths = {"meals"}, type = EntityGraph.EntityGraphType.LOAD)
@Query("SELECT u FROM User u WHERE u.id=?1")
User getWithMeals(int id);
Grigory Kislin
fuente
1

En lugar de usar trucos como:

  • Set en vez de List
  • criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);

que no modifican su consulta sql, podemos usar (citando especificaciones JPA)

q.select(emp).distinct(true);

que modifica la consulta sql resultante, por lo que tiene un DISTINCTarchivo.

Kaustubh
fuente
0

No parece un buen comportamiento aplicar una combinación externa y generar resultados duplicados. La única solución que queda es filtrar nuestro resultado usando streams. Gracias java8 por dar una forma más fácil de filtrar.

return results.stream().distinct().collect(Collectors.toList());
Pravin
fuente