web-dev-qa-db-fra.com

La relation un-à-plusieurs obtient des objets en double sans utiliser "distinct". Pourquoi?

J'ai 2 classes dans une relation un-à-plusieurs et une requête HQL qui est un peu étrange. Même si j'ai lu quelques questions déjà postées, cela ne me semble pas clair.

Class Department{
   @OneToMany(fetch=FetchType.EAGER, mappedBy="department")
   Set<Employee> employees;
}
Class Employee{
   @ManyToOne
   @JoinColumn(name="id_department")
   Department department;
}

Lorsque j'utilise la requête suivante, j'obtiens des objets Department en double:

session.createQuery("select dep from Department as dep left join dep.employees");

Ainsi, je dois utiliser distinct:

session.createQuery("select distinct dep from Department as dep left join dep.employees");

Ce comportement est-il attendu? Je considère cela inhabituel, en le comparant à SQL.

47
herti

Cette question est expliquée en détail sur FAQ Hibernate :

Tout d'abord, vous devez comprendre SQL et comment les jointures externes fonctionnent en SQL. Si vous ne comprenez pas et ne comprenez pas complètement les jointures externes dans SQL, ne continuez pas à lire cet élément FAQ mais consultez un manuel ou un didacticiel SQL. Sinon, vous ne comprendrez pas l'explication suivante et vous vous plaindrez ce comportement sur le forum Hibernate. Exemples typiques qui peuvent renvoyer des références en double du même objet Order:

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

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

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 l'ensemble de résultats SQL, Hibernate ne masque 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 de 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 en double à ces 5 Si vous ne comprenez pas cette dernière phrase, vous devez lire sur Java et la différence entre une instance sur le Java tas et une référence à une telle instance. (Pourquoi une jointure externe gauche? Si vous aviez une commande supplémentaire sans éléments de campagne, le jeu 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 commande. Vous voulez des ordres même s'ils n'ont pas d'éléments de campagne, n'est-ce pas? Sinon, utilisez une extraction de jointure interne dans votre HQL).
Hibernate ne filtre pas ces références en double par défaut. Certaines personnes (pas vous) le veulent vraiment. Comment pouvez-vous les filtrer? Comme ça:

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

Un LinkedHashSet filtre les références en double (c'est un ensemble) et il préserve l'ordre d'insertion (ordre des éléments dans votre résultat). C'était trop facile, vous pouvez donc le faire de différentes manières, plus difficiles:

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


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

List result = session.createCriteria(Order.class)  
                        .setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY)  
                        .list();  

List result = session.createQuery("select o from Order o left join fetch o.lineItems")  
                      .setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY) // Yes, really!  
                      .list();  

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

Le dernier est spécial. Il semble que vous utilisiez le mot clé SQL DISTINCT ici. Bien sûr, ce n'est pas du SQL, c'est du HQL. Ce distinct n'est qu'un raccourci pour le transformateur de résultat, dans ce cas. Oui, dans d'autres cas, un HQL distinct se traduira directement en SQL DISTINCT. Pas dans ce cas: vous ne pouvez pas filtrer les doublons au niveau SQL, la nature même d'un produit/jointure l'interdit - vous voulez les doublons ou vous n'obtenez pas toutes les données dont vous avez besoin. Tout ce filtrage des doublons se produit en mémoire, lorsque l'ensemble de résultats est organisé en objets. Il devrait également être évident pourquoi les opérations de "limite" basées sur les lignes de l'ensemble de résultats, telles que setFirstResult (5) et setMaxResults (10), ne fonctionnent pas avec ce type de requêtes de récupération. Si vous limitez l'ensemble de résultats à un certain nombre de lignes, vous coupez les données de manière aléatoire. Un jour, Hibernate pourrait être suffisamment intelligent pour savoir que si vous appelez setFirstResult () ou setMaxResults (), il ne devrait pas utiliser de jointure, mais un deuxième SQL SELECT. Essayez-le, votre version d'Hibernate est peut-être déjà assez intelligente. Si ce n'est pas le cas, écrivez deux requêtes, l'une pour limiter les choses, l'autre pour la récupération rapide. Voulez-vous savoir pourquoi l'exemple avec la requête Critères n'a pas ignoré le paramètre fetch = "join" dans le mappage, mais HQL s'en fichait? Lisez l'article suivant FAQ.

88
gerrytan