Hibernate Criteria renvoie les enfants plusieurs fois avec FetchType.EAGER

115

J'ai une Orderclasse qui a une liste de OrderTransactionset je l'ai mappée avec un mappage Hibernate un-à-plusieurs comme ceci:

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

Ces derniers Orderont également un champ orderStatus, qui est utilisé pour le filtrage avec les critères suivants:

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

Cela fonctionne et le résultat est comme prévu.

Voici maintenant ma question : pourquoi, lorsque je définis explicitement le type de récupération sur EAGER, les Orders apparaissent-ils plusieurs fois dans la liste résultante?

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

Comment devrais-je changer mon code de critère pour atteindre le même résultat avec le nouveau paramètre?

Raoulsson
la source
1
Avez-vous essayé d'activer show_sql pour voir ce qui se passe en dessous?
Mirko N.
Veuillez également ajouter le code des classes OrderTransaction et Order. \
Eran Medan

Réponses:

115

C'est en fait le comportement attendu si j'ai bien compris votre configuration.

Vous obtenez la même Orderinstance dans n'importe lequel des résultats, mais puisque maintenant vous effectuez une jointure avec le OrderTransaction, il doit renvoyer le même nombre de résultats qu'une jointure SQL régulière retournera

Donc en fait, il devrait apparaître plusieurs fois. ceci est très bien expliqué par l'auteur (Gavin King) lui-même ici : cela explique à la fois pourquoi et comment obtenir encore des résultats distincts


Également mentionné dans la FAQ Hibernate :

Hibernate ne renvoie pas de résultats distincts pour une requête avec la récupération de jointure externe activée pour une collection (même si j'utilise le mot-clé distinct)? Tout d'abord, vous devez comprendre SQL et comment les OUTER JOIN fonctionnent dans SQL. Si vous ne comprenez pas et ne comprenez pas complètement les jointures externes dans SQL, ne continuez pas à lire cet article de FAQ mais consultez un manuel ou un didacticiel SQL. Sinon, vous ne comprendrez pas l'explication suivante et vous vous plaindrez de ce comportement sur le forum Hibernate.

Exemples typiques qui peuvent renvoyer des références dupliquées du même objet 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();

Tous ces exemples produisent la même instruction SQL:

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

Vous voulez savoir pourquoi les doublons sont là? Regardez le jeu de résultats SQL, Hibernate ne cache pas ces doublons sur le côté gauche du résultat joint externe mais renvoie tous les doublons de la table de pilotage. Si vous avez 5 commandes dans la base de données et que chaque commande comporte 3 éléments de campagne, l'ensemble de résultats comportera 15 lignes. La liste des résultats Java de ces requêtes comportera 15 éléments, tous de type Order. Seules 5 instances Order seront créées par Hibernate, mais les doublons du jeu de résultats SQL sont conservés en tant que références dupliquées à ces 5 instances. Si vous ne comprenez pas cette dernière phrase, vous devez lire sur Java et la différence entre une instance sur le tas Java et une référence à une telle instance.

(Pourquoi une jointure externe à gauche? Si vous avez une campagne supplémentaire sans élément de campagne, l'ensemble de résultats serait de 16 lignes avec NULL remplissant le côté droit, où les données d'élément de campagne sont pour une autre campagne. Vous voulez des campagnes même si ils n'ont pas d'éléments de campagne, n'est-ce pas? Sinon, utilisez une extraction par jointure interne dans votre HQL).

Hibernate ne filtre pas ces références dupliquées par défaut. Certaines personnes (pas vous) le souhaitent. Comment pouvez-vous les filtrer?

Comme ça:

Collection result = new LinkedHashSet( session.create*(...).list() );
Eran Medan
la source
121
Même si vous comprenez l'explication suivante, vous pourriez bien vous plaindre de ce comportement sur le forum Hibernate, car c'est un comportement stupide!
Tom Anderson
17
Tout à fait raison Tom, j'ai oublié l'attitude arrogante de Gavin Kings. Il dit également «Hibernate ne filtre pas ces références dupliquées par défaut. Certaines personnes (pas vous) veulent en fait ce `` Je serais intéressé quand les gens en fait une fourmi.
Paul Taylor
16
@TomAnderson oui exactement. pourquoi quelqu'un aurait-il besoin de ces doublons? Je demande par pure curiosité, car je n'en ai aucune idée ... Vous pouvez créer vous-même des doublons, autant que vous le souhaitez .. ;-)
Parobay
13
Soupir. C'est en fait un défaut Hibernate, à mon humble avis. Je souhaite optimiser mes requêtes, je passe donc de "sélectionner" à "rejoindre" dans mon fichier de cartographie. Soudain, mon code se casse partout. Ensuite, je cours et corrige tous mes DAO en ajoutant des transformateurs de résultats et ainsi de suite. Expérience utilisateur == très négative. Je comprends que certaines personnes aiment absolument avoir des doublons pour des raisons bizarres, mais pourquoi ne puis-je pas dire "récupérez ces objets PLUS RAPIDEMENT mais ne me dérangez pas avec des doublons" en spécifiant fetch = "justworkplease"?
Roman Zenka
@Eran: Je suis confronté à un problème similaire. Je n'obtiens pas d'objets parents en double, mais j'obtiens des enfants dans chaque objet parent répétés autant de fois qu'il y a de nombre d'objets parents dans la réponse. Une idée pourquoi ce problème?
mantri
93

En plus de ce qui est mentionné par Eran, un autre moyen d'obtenir le comportement souhaité est de définir le transformateur de résultat:

criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);
EJB
la source
8
Cela fonctionnera dans la plupart des cas ... sauf lorsque vous essayez d'utiliser des critères pour récupérer 2 collections / associations.
JamesD
42

essayer

@Fetch (FetchMode.SELECT) 

par exemple

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

}

mathi
la source
11
FetchMode.SELECT augmente le nombre de requêtes SQL déclenchées par Hibernate mais garantit une seule instance par enregistrement d'entité racine. Dans ce cas, Hibernate déclenchera une sélection pour chaque enregistrement enfant. Vous devez donc en tenir compte par rapport aux considérations de performances.
Bipul
1
@BipulKumar oui, mais c'est l'option lorsque nous ne pouvons pas utiliser la récupération paresseuse car nous devons maintenir une session pour la récupération paresseuse pour accéder aux sous-objets.
mathi
18

N'utilisez pas List et ArrayList mais Set et HashSet.

@OneToMany(targetEntity = OrderTransaction.class, cascade = CascadeType.ALL)
public Set<OrderTransaction> getOrderTransactions() {
    return orderTransactions;
}
Αλέκος
la source
2
S'agit-il d'une mention fortuite d'une meilleure pratique Hibernate ou est-il pertinent pour la question de récupération multi-enfants du PO?
Jacob Zwiers
Je l'ai. Secondaire à la question du PO. Cependant, l'article dzone devrait probablement être pris avec un grain de sel ... basé sur le propre aveu de l'auteur dans les commentaires.
Jacob Zwiers
2
C'est une très bonne réponse à l'OMI. Si vous ne voulez pas de doublons, il est fort probable que vous préfériez utiliser un ensemble plutôt qu'une liste - Utiliser un ensemble (et implémenter les méthodes equals / hascode correctes bien sûr) a résolu le problème pour moi. Attention lors de l'implémentation de hashcode / equals, comme indiqué dans la documentation redhat, de ne pas utiliser le champ id.
Mat
1
Merci pour votre IMO. De plus, ne vous embêtez pas à créer les méthodes equals () et hashCode (). Laissez votre IDE ou Lombok les générer pour vous.
Αλέκος
3

En utilisant Java 8 et Streams, j'ajoute dans ma méthode utilitaire cette déclaration de retour:

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

Les flux suppriment les doublons très rapidement. J'utilise l'annotation dans ma classe Entity comme ceci:

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

Je pense qu'il est bether dans mon application d'utiliser la session dans la méthode où j'ai besoin de données de la base de données. Closse session quand j'ai fait. Bien sûr, configurer ma classe Entity pour utiliser le type de récupération leasy. Je vais refactoriser.

easyScript
la source
3

J'ai le même problème pour récupérer 2 collections associées: l'utilisateur a 2 rôles (Set) et 2 repas (List) et les repas sont dupliqués.

@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'aide pas (requête 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);

Enfin j'ai trouvé 2 solutions:

  1. Changer la liste en LinkedHashSet
  2. Utilisez EntityGraph avec uniquement le champ "repas" et tapez LOAD, qui charge les rôles tels qu'ils ont été déclarés (EAGER et par BatchSize = 200 pour éviter les problèmes N + 1):

Solution finale:

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

Au lieu d'utiliser des hacks comme:

  • Set au lieu de List
  • criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);

qui ne modifient pas votre requête SQL, nous pouvons utiliser (en citant les spécifications JPA)

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

qui modifie la requête SQL résultante, ayant ainsi un DISTINCT.

Kaustubh
la source
0

Cela ne semble pas un bon comportement d'appliquer une jointure externe et d'apporter des résultats dupliqués. La seule solution qui reste est de filtrer notre résultat à l'aide de flux. Merci java8 qui donne un moyen plus simple de filtrer.

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