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

4

54

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.

Hard answered 11/9, 2013 at 23:59 Comment(2)
why you make left join, just simple from Department also your fetch type is eager. What is the sql output?Sizeable
acctually the query has a where clause, I need to put a condition for each employee and a from Department wont help me in this case. select dep from Department as dep left join dep.employees emp where emp.enddate > current_date()Hard
S
98

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.

Scott answered 12/9, 2013 at 0:9 Comment(1)
I am having performance problems with this. I am thinking maybe Hibernate is trying to create Java objects for each of the duplicated items and THEN doing a distinct? So basically doing a lot of unnecessary object creation is causing a slow down, is this the case? Is there a more efficient solution?Rescue
B
0

Use the result transformer Criteria.DISTINCT_ROOT_ENTITY:

List result = session.createQuery("hql query")  
                        .setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY)  
                        .list();
Braze answered 16/12, 2019 at 9:39 Comment(0)
B
0

Try this way:

Query query = session.createQuery("queryStringWithEagerFetch").setHint(QueryHints.HINT_PASS_DISTINCT_THROUGH,false)
List result = query.list()
Busch answered 8/4, 2022 at 12:56 Comment(1)
As it’s currently written, your answer is unclear. Please edit to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers in the help center.There
B
-1

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();
Bateau answered 12/9, 2020 at 1:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.