Os critérios do Hibernate retornam filhos várias vezes com FetchType.EAGER

115

Eu tenho uma Orderclasse que tem uma lista de OrderTransactionse mapeei com um mapeamento de Hibernate um-para-muitos assim:

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

Estes Orders também têm um campo orderStatus, que é usado para filtrar com os seguintes critérios:

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

Isso funciona e o resultado é o esperado.

Agora, aqui está minha pergunta : Por que, quando eu defino o tipo de busca explicitamente como EAGER, os Orders aparecem várias vezes na lista resultante?

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

Como eu teria que alterar meu código de critérios para chegar ao mesmo resultado com a nova configuração?

Raoulsson
fonte
1
Você tentou habilitar show_sql para ver o que está acontecendo por baixo?
Mirko N.
Adicione também o código das classes OrderTransaction e Order. \
Eran Medan

Respostas:

115

Este é realmente o comportamento esperado se eu entendi sua configuração corretamente.

Você obtém a mesma Orderinstância em qualquer um dos resultados, mas como agora você está fazendo uma junção com o OrderTransaction, ela deve retornar a mesma quantidade de resultados que uma junção sql regular retornará

Então, na verdade, deve aparecer várias vezes. isso é muito bem explicado pelo próprio autor (Gavin King) aqui : Ele explica por que e como ainda obter resultados distintos


Também mencionado no FAQ do Hibernate :

O Hibernate não retorna resultados distintos para uma consulta com a busca de junção externa habilitada para uma coleção (mesmo se eu usar a palavra-chave distinta)? Primeiro, você precisa entender o SQL e como OUTER JOINs funcionam no SQL. Se você não entende e compreende totalmente as junções externas em SQL, não continue lendo este item da FAQ, mas consulte um manual ou tutorial de SQL. Caso contrário, você não entenderá a explicação a seguir e reclamará sobre esse comportamento no fórum do Hibernate.

Exemplos típicos que podem retornar referências duplicadas do mesmo 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 esses exemplos produzem a mesma instrução SQL:

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

Quer saber por que as duplicatas estão lá? Observe o conjunto de resultados SQL, o Hibernate não esconde essas duplicatas no lado esquerdo do resultado da junção externa, mas retorna todas as duplicatas da tabela de controle. Se você tiver 5 pedidos no banco de dados e cada pedido tiver 3 itens de linha, o conjunto de resultados terá 15 linhas. A lista de resultados Java dessas consultas terá 15 elementos, todos do tipo Order. Apenas 5 instâncias de Order serão criadas pelo Hibernate, mas duplicatas do conjunto de resultados SQL são preservadas como referências duplicadas a essas 5 instâncias. Se você não entende esta última frase, você precisa ler sobre Java e a diferença entre uma instância no heap Java e uma referência a tal instância.

(Por que uma junção externa à esquerda? Se você tivesse um pedido adicional sem itens de linha, o conjunto de resultados seria de 16 linhas com NULL preenchendo o lado direito, onde os dados do item de linha são para outro pedido. Você deseja pedidos mesmo que eles não têm itens de linha, certo? Caso contrário, use uma busca de junção interna em seu HQL).

O Hibernate não filtra essas referências duplicadas por padrão. Algumas pessoas (não você) realmente querem isso. Como você pode filtrá-los?

Como isso:

Collection result = new LinkedHashSet( session.create*(...).list() );
Eran Medan
fonte
121
Mesmo que você entenda a explicação a seguir, você pode reclamar sobre esse comportamento no fórum do Hibernate, porque ele está mudando um comportamento estúpido!
Tom Anderson,
17
Muito certo Tom, eu tinha esquecido da atitude arrogante de Gavin Kings. Ele também diz 'O Hibernate não filtra essas referências duplicadas por padrão. Algumas pessoas (não você) realmente querem isso. Eu ficaria interessado quando as pessoas realmente formularem isso.
Paul Taylor
16
@TomAnderson sim exatamente. por que alguém precisaria dessas duplicatas? Estou perguntando por pura curiosidade, pois não tenho ideia ... Você mesmo pode criar duplicatas, quantas quiser .. ;-)
Parobay
13
Suspiro. Esta é realmente uma falha do Hibernate, IMHO. Quero otimizar minhas consultas, então vou de "selecionar" para "juntar" no meu arquivo de mapeamento. De repente, meu código QUEBRA em todo o lugar. Então corro e conserto todos os meus DAOs acrescentando transformadores de resultado e outros enfeites. Experiência do usuário == muito negativa. Eu entendo que algumas pessoas adoram ter duplicatas por motivos bizarros, mas por que não posso dizer "buscar esses objetos MAIS RAPIDAMENTE, mas não me incomode com duplicatas" especificando fetch = "justworkplease"?
Roman Zenka,
@Eran: Estou enfrentando um tipo de problema semelhante. Não estou obtendo objetos-pai duplicados, mas estou obtendo filhos em cada objeto-pai repetidos tantas vezes quanto houver número de objetos-pai na resposta. Alguma ideia do porquê desse problema?
mantri
93

Além do que é mencionado por Eran, outra forma de obter o comportamento desejado, é definir o transformador de resultado:

criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);
EJB
fonte
8
Isso funcionará na maioria dos casos ... exceto quando você tentar usar os critérios para buscar 2 coleções / associações.
JamesD
42

experimentar

@Fetch (FetchMode.SELECT) 

por exemplo

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

}

matemática
fonte
11
FetchMode.SELECT aumenta o número de consultas SQL disparadas pelo Hibernate, mas garante apenas uma instância por registro de entidade raiz. O Hibernate irá disparar um select para cada registro filho neste caso. Portanto, você deve levar isso em consideração com relação às considerações de desempenho.
Bipul
1
@BipulKumar sim, mas esta é a opção quando não podemos usar o lazy fetch porque precisamos manter uma sessão para lazy fetch para acessar subobjetos.
mathi
18

Não use List e ArrayList, mas Set e HashSet.

@OneToMany(targetEntity = OrderTransaction.class, cascade = CascadeType.ALL)
public Set<OrderTransaction> getOrderTransactions() {
    return orderTransactions;
}
Αλέκος
fonte
2
Esta é uma menção acidental de uma prática recomendada do Hibernate ou relevante para a pergunta de recuperação de vários filhos do OP?
Jacob Zwiers,
Entendi. Secundário à pergunta do OP. Porém, o artigo dzone provavelmente deve ser visto com um grão de sal ... com base na própria admissão do autor nos comentários.
Jacob Zwiers,
2
Esta é uma resposta muito boa IMO. Se você não quiser duplicatas, é altamente provável que prefira usar um conjunto do que uma lista - usar um conjunto (e implementar métodos equals / hascode corretos, é claro) resolveu o problema para mim. Cuidado ao implementar hashcode / equals, conforme declarado no documento redhat, para não usar o campo id.
Mat
1
Obrigado pelo seu IMO. Além disso, não tenha problemas para criar métodos equals () e hashCode (). Deixe seu IDE ou Lombok gerá-los para você.
Αλέκος
3

Usando Java 8 e Streams, adiciono ao meu método de utilitário esta declaração de retorno:

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

Streams removem duplicatas muito rápido. Eu uso anotações em minha classe Entity assim:

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

Acho que é melhor no meu aplicativo usar sessão no método onde preciso de dados de banco de dados. Sessão de Closse quando eu terminar. Claro, configurei minha classe Entity para usar o tipo de busca leasy. Eu vou refatorar.

easyScript
fonte
3

Tenho o mesmo problema para buscar 2 coleções associadas: o usuário possui 2 funções (Conjunto) e 2 refeições (Lista) e as refeições estão 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 não ajuda (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);

Finalmente encontrei 2 soluções:

  1. Alterar lista para LinkedHashSet
  2. Use EntityGraph apenas com o campo "refeição" e digite LOAD, que carregam os papéis conforme declarados (EAGER e por BatchSize = 200 para evitar o problema N + 1):

Solução final:

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

Em vez de usar hacks como:

  • Set ao invés de List
  • criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);

que não modificam sua consulta sql, podemos usar (citando especificações JPA)

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

que modifica a consulta sql resultante, tendo, portanto, um DISTINCTnele.

Kaustubh
fonte
0

Não parece um bom comportamento aplicar uma junção externa e trazer resultados duplicados. A única solução que resta é filtrar nosso resultado usando fluxos. Obrigado java8 dando uma maneira mais fácil de filtrar.

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