One-To-Many relationship gets duplicate objects without using "distinct". Why?

JavaHibernateHqlDistinctOne to-Many

Java Problem Overview


I have 2 classes in a one-to-many relationship and a HQL query that is a bit strange. Even if I have read some questions already posted, it does not seem clear to me.

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

When I use the following query I get duplicates Department objects:

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

Thus, I have to use distinct:

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

Is this behaviour an expected one? I consider this unusual, as comparing it with SQL.

Java Solutions


Solution 1 - Java

This question is thoroughly explained on Hibernate FAQ:

> First, you need to understand SQL and how OUTER JOINs work in SQL. If > you do not fully understand and comprehend outer joins in SQL, do not > continue reading this FAQ item but consult a SQL manual or tutorial. > Otherwise you will not understand the following explanation and you > will complain about this behavior on the Hibernate forum. Typical > examples that might return duplicate references of the same Order > object:

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();  

> All of these examples produce the same SQL statement:

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

> Want to know why the duplicates are there? Look at the SQL > resultset, Hibernate does not hide these duplicates on the left side > of the outer joined result but returns all the duplicates of the > driving table. If you have 5 orders in the database, and each order > has 3 line items, the resultset will be 15 rows. The Java result list > of these queries will have 15 elements, all of type Order. Only 5 > Order instances will be created by Hibernate, but duplicates of the > SQL resultset are preserved as duplicate references to these 5 > instances. If you do not understand this last sentence, you need to > read up on Java and the difference between an instance on the Java > heap and a reference to such an instance. (Why a left outer join? If > you'd have an additional order with no line items, the result set > would be 16 rows with NULL filling up the right side, where the line > item data is for other order. You want orders even if they don't have > line items, right? If not, use an inner join fetch in your HQL).
> Hibernate does not filter out these duplicate references by default. > Some people (not you) actually want this. How can you filter them out? > Like this: > > Collection result = new LinkedHashSet( session.create*(...).list() );
> > A LinkedHashSet filteres out duplicate references (it's a set) and > it preserves insertion order (order of elements in your result). That > was too easy, so you can do it in many different and more difficult > ways:

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();       

> The last one is special. It looks like you are using the SQL > DISTINCT keyword here. Of course, this is not SQL, this is HQL. This > distinct is just a shortcut for the result transformer, in this case. > Yes, in other cases an HQL distinct will translate straight into a SQL > DISTINCT. Not in this case: you can not filter out duplicates at the > SQL level, the very nature of a product/join forbids this - you want > the duplicates or you don't get all the data you need. All of this > filtering of duplicates happens in-memory, when the resultset is > marshalled into objects. It should be also obvious why resultset > row-based "limit" operations, such as setFirstResult(5) and > setMaxResults(10) do not work with these kind of eager fetch queries. > If you limit the resultset to a certain number of rows, you cut off > data randomly. One day Hibernate might be smart enough to know that if > you call setFirstResult() or setMaxResults() it should not use a join, > but a second SQL SELECT. Try it, your version of Hibernate might > already be smart enough. If not, write two queries, one for limiting > stuff, the other for eager fetching. Do you want to know why the > example with the Criteria query did not ignore the fetch="join" > setting in the mapping but HQL didn't care? Read the next FAQ item.

Solution 2 - Java

Use the result transformer Criteria.DISTINCT_ROOT_ENTITY:

List result = session.createQuery("hql query")  
                        .setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY)  
                        .list();

Solution 3 - Java

Here's a great tip I learned about from Mr. Vlad Mihalcea. For more tips: https://vladmihalcea.com/tutorials/hibernate/

    list = session.createQuery("SELECT DISTINCT c FROM Supplier c "
            +"LEFT JOIN FETCH c.salesList WHERE c.name LIKE :p1"
            , Supplier.class)
            .setHint(QueryHints.PASS_DISTINCT_THROUGH, false)
            .setParameter("p1", name + "%")
            .list();

Solution 4 - Java

Try this way:

Query query = session.createQuery("queryStringWithEagerFetch").setHint(QueryHints.HINT_PASS_DISTINCT_THROUGH,false)
List result = query.list()

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
Questionbogdan.hertiView Question on Stackoverflow
Solution 1 - JavagerrytanView Answer on Stackoverflow
Solution 2 - JavaJayram KumarView Answer on Stackoverflow
Solution 3 - JavaMuhammad AliView Answer on Stackoverflow
Solution 4 - JavaLiqun SunView Answer on Stackoverflow