55

我有 2 个具有一对多关系的类和一个有点奇怪的 HQL 查询。即使我已经阅读了一些已经发布的问题,我似乎也不清楚。

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

当我使用以下查询时,我得到重复的部门对象:

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

因此,我必须使用不同的:

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

这种行为是预期的吗?与 SQL 相比,我认为这很不寻常。

4

3 回答 3

99

这个问题在Hibernate FAQ上得到了彻底的解释:

首先,您需要了解 SQL 以及 OUTER JOIN 在 SQL 中的工作原理。如果您不完全理解和理解 SQL 中的外连接,请不要继续阅读此常见问题解答项目,而是查阅 SQL 手册或教程。否则你将无法理解下面的解释,你会在 Hibernate 论坛上抱怨这种行为。可能返回相同 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();  

所有这些示例都生成相同的 SQL 语句:

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

想知道为什么会有重复项吗?查看 SQL 结果集,Hibernate 并没有将这些重复项隐藏在外连接结果的左侧,而是返回驱动表的所有重复项。如果您在数据库中有 5 个订单,并且每个订单有 3 个订单项,则结果集将为 15 行。这些查询的 Java 结果列表将有 15 个元素,所有类型均为 Order。Hibernate 将只创建 5 个 Order 实例,但 SQL 结果集的副本被保留为对这 5 个实例的重复引用。如果您不理解最后一句话,则需要阅读 Java 以及 Java 堆上的实例与对此类实例的引用之间的区别。(为什么是左外连接?如果您有一个没有订单项的附加订单,结果集将是 16 行,右侧填充 NULL,其中行项目数据用于其他订单。即使没有订单项,您也想要订单,对吧?如果没有,请在您的 HQL 中使用内部连接提取)。
Hibernate 默认不会过滤掉这些重复的引用。有些人(不是你)实际上想要这个。你怎么能过滤掉它们?像这样:

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

LinkedHashSet 过滤掉重复的引用(它是一个集合)并保留插入顺序(结果中元素的顺序)。这太容易了,所以你可以用许多不同的和更困难的方式来做:

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

最后一个很特别。看起来您在这里使用的是 SQL DISTINCT 关键字。当然,这不是 SQL,这是 HQL。在这种情况下,这个 distinct 只是结果转换器的一个捷径。是的,在其他情况下,HQL distinct 将直接转换为 SQL DISTINCT。不是在这种情况下:您无法在 SQL 级别过滤掉重复项,产品/连接的本质禁止这样做 - 您想要重复项,或者您没有获得所需的所有数据。当结果集被编组为对象时,所有这些重复过滤都发生在内存中。为什么基于结果集行的“限制”操作(例如 setFirstResult(5) 和 setMaxResults(10))不适用于这些急切的获取查询,这也应该很明显。如果将结果集限制为一定数量的行,则会随机截断数据。有一天,Hibernate 可能足够聪明,知道如果您调用 setFirstResult() 或 setMaxResults(),它不应该使用连接,而是使用第二个 SQL SELECT。试试看,你的 Hibernate 版本可能已经足够聪明了。如果没有,请编写两个查询,一个用于限制内容,另一个用于急切获取。您想知道为什么带有 Criteria 查询的示例没有忽略映射中的 fetch="join" 设置但 HQL 不关心吗?阅读下一个常见问题解答项目。在映射中设置但 HQL 不在乎?阅读下一个常见问题解答项目。在映射中设置但 HQL 不在乎?阅读下一个常见问题解答项目。

于 2013-09-12T00:09:02.340 回答
0

这是我从 Vlad Mihalcea 先生那里学到的一个很好的技巧。更多提示: 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();
于 2020-09-12T01:04:35.623 回答
0

使用结果转换器Criteria.DISTINCT_ROOT_ENTITY

List result = session.createQuery("hql query")  
                        .setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY)  
                        .list();
于 2019-12-16T09:39:44.900 回答