Hibernate Criteria devuelve hijos varias veces con FetchType.EAGER

Tengo una clase de Order que tiene una lista de OrderTransactions y la OrderTransactions con un mapeo Hibernate de uno a muchos, así:

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

Estos Order s también tienen un orderStatus campo, que se usa para filtrar con los siguientes Criterios:

 public List 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 configuro explícitamente el tipo de búsqueda para EAGER , los Order s aparecen varias veces en la lista resultante?

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

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

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

Obtiene la misma instancia de Order en cualquiera de los resultados, pero desde ahora está haciendo una unión con OrderTransaction , tiene que devolver la misma cantidad de resultados que una unión sql regular devolverá

Entonces en realidad debería aparecer varias veces. esto lo explica muy bien el propio autor (Gavin King): ambos explican 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 recuperación de unión externa habilitada para una colección (incluso si uso la palabra clave distinta)? En primer lugar, debe comprender SQL y cómo funcionan las UNIONES EXTERNAS en SQL. Si no comprende y comprende completamente las uniones externas en SQL, no continúe leyendo este elemento de Preguntas frecuentes, sino consulte un manual de SQL o un tutorial. 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(); 

  ...  

 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 statement SQL:

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

¿Quieres saber por qué los duplicados están ahí? Mire el conjunto de resultados de SQL, Hibernate no oculta estos duplicados en el lado izquierdo del resultado externo unido pero devuelve todos los duplicados de la tabla de conducción. Si tiene 5 pedidos en la base de datos y cada orden tiene 3 líneas de pedido, el conjunto de resultados tendrá 15 filas. La lista de resultados de Java de estas consultas tendrá 15 elementos, todos de tipo Order. Solo 5 instancias de pedido serán creadas por Hibernate, pero los duplicados del conjunto de resultados de SQL se conservan como referencias duplicadas a estas 5 instancias. Si no entiende esta última oración, necesita 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 izquierda? Si tuviera una orden adicional sin líneas de pedido, el conjunto de resultados sería 16 filas con NULL llenando el lado derecho, donde los datos de línea de pedido son para otra orden. Desea pedidos incluso si no tienen líneas de pedido, ¿verdad? Si no, utilice una búsqueda de combinación interna en su HQL).

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

Me gusta esto:

 Collection result = new LinkedHashSet( session.create*(...).list() ); 

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

 criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY); 

tratar

 @Fetch (FetchMode.SELECT) 

por ejemplo

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

}

No use List y ArrayList sino Set y HashSet.

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

Usando Java 8 y Streams agrego en mi método de utilidad esta statement de retorno:

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

Las transmisiones eliminan el duplicado muy rápido. Utilizo la anotación en mi clase Entity de esta manera:

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

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

Tengo el mismo problema de 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 roles; @OneToMany(fetch = FetchType.LAZY, mappedBy = "user") @OrderBy("dateTime DESC") protected List 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 la lista a LinkedHashSet
  2. Utilice EntityGraph con solo campo “comida” y escriba LOAD, que cargan roles como declararon (EAGER y por BatchSize = 200 para evitar el problema de 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); 

ACTUALIZACIÓN: a menos que javadoc para org.springframework.data.jpa.repository.EntityGraph.EntityGraphType#FETCH

los atributos que se especifican mediante nodos de atributo del gráfico de entidad se tratan como FetchType.EAGER y los atributos que no se especifican se tratan como FetchType.LAZY

con este tipo de funciones también se obtienen.

No parece un gran comportamiento aplicar una combinación externa y traer resultados duplicados. La única solución que queda es filtrar nuestro resultado utilizando flujos. Gracias java8 dando una forma más fácil de filtrar.

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