我应该在此 JPQL 查询中包含 distinct 吗?

Should I include distinct in this JPQL query?

背景

我在 SO 和许多流行的博客上看到了多个关于 JPQL JOIN FETCH 查询中 distinct 关键字的必要性以及 PASS_DISTINCT_THROUGH 查询提示的答案和问题。

比如看这两道题

和这些博文

我错过了什么

现在我的问题是我无法完全理解 JPQL 查询中必须包含 distinct 关键字的确切时间。更具体地说,如果它取决于使用哪种方法来执行查询(getResultListgetSingleResult)。

下面举例说明我的意思。

从现在开始我写的所有东西都在 Ubuntu Linux 18.04 上测试过,使用 Java 8,Hibernate 5.4.13 和内存中的 H2 数据库(版本 1.4. 200).

假设我有一个 Department 实体,它与 DepartmentDirector 实体具有 lazy 双向一对多关系:

// Department.java
@Entity
public class Department {
    // ...
    private Set<DepartmentDirector> directors;

    @OneToMany(mappedBy = "department", fetch = FetchType.LAZY)
    public Set<DepartmentDirector> getDirectors() {
        return directors;
    }
    // ...
}

// DepartmentDirector.java
@Entity
public class DepartmentDirector {
    // ...
    private Department department

    @ManyToOne
    @JoinColumn(name = "department_fk")
    public Department getDepartment() {
        return department;
    }
    // ...
}

假设我的数据库当前包含一个部门 (department1) 和两个与之关联的主管。

现在我想通过 uuid(主键)检索部门及其所有主管。这可以通过以下 JOIN FETCH JPQL 查询来完成:

String query = "select department from Department department left join fetch "
             + "department.directors where department.uuid = :uuid";

由于前面的查询使用带有子集合的 join fetch,我预计它在发出时会 return 两个重复的部门:然而,这只发生在使用带有 [=18= 的查询时] 方法,而不是在使用 getSingleResult 方法时。这在某种程度上是合理的,但我发现 getSingleResult 的 Hibernate 实现在幕后使用了 getResultList,所以我预计会抛出 NonUniqueResultException

我也简要浏览了 JPA 2.2 规范,但没有提到两种方法在处理重复项方面的区别,并且涉及此问题的每个代码示例都使用 getResultList 方法。

结论

在我的示例中,我发现使用 getSingleResult 执行的 JOIN FETCH 查询不会遇到我在 背景 [=109] 部分链接的资源中解释的重复实体问题=].

如果上述说法是正确的,则意味着同一个 JOIN FETCH 查询如果使用 getResultList 执行则需要 distinct,但使用 getSingleResult.

如果这是预料之中的或者我误解了什么,我需要有人来解释我。


附录

两次查询结果:

  1. getResultList方法查询运行。我按预期得到了两个重复的部门(这只是为了测试查询的行为,应该使用 getSingleResult 来代替 ):

    List<Department> resultList = entityManager.createQuery(query, Department.class)
            .setParameter("uuid", department1.getUuid())
            .getResultList();
    
    assertThat(resultList).containsExactly(department1, department1); // passes
    
  2. getSingleResult方法查询运行。我希望检索到相同的重复部门,因此抛出 NonUniqueResultException。相反,检索到一个部门并且一切正常:

    Department singleResult = entityManager.createQuery(query, Department.class)
            .setParameter("uuid", department1.getUuid())
            .getSingleResult();
    
    assertThat(singleResult).isEqualTo(department1); // passes
    

有趣的问题。

首先让我指出,getSingleResult() 用于 由于其性质 总是 return 单个结果的查询(意思是:大部分聚合查询,如 SELECT SUM(e.id) FROM Entity e)。您 认为 的查询,基于某些业务 domain-specific 规则,应该 return 单个结果,并不真正符合条件。

也就是说,JPA 规范规定当查询 return 有多个结果时 getSingleResult() 应该抛出 NonUniqueResultException

The NonUniqueResultException is thrown by the persistence provider when Query.getSingleResult or TypedQuery.getSingleResult is invoked and there is more than one result from the query. This exception will not cause the current transaction, if one is active, to be marked for rollback.

但是,查看 Hibernate 实现:

    @Override
    public R getSingleResult() {
        try {
            final List<R> list = list();
            if ( list.size() == 0 ) {
                throw new NoResultException( "No entity found for query" );
            }
            return uniqueElement( list );
        }
        catch ( HibernateException e ) {
            if ( getProducer().getFactory().getSessionFactoryOptions().isJpaBootstrap() ) {
                throw getExceptionConverter().convert( e );
            }
            else {
                throw e;
            }
        }
    }

    public static <R> R uniqueElement(List<R> list) throws NonUniqueResultException {
        int size = list.size();
        if ( size == 0 ) {
            return null;
        }
        R first = list.get( 0 );
        for ( int i = 1; i < size; i++ ) {
            if ( list.get( i ) != first ) {
                throw new NonUniqueResultException( list.size() );
            }
        }
        return first;
    }

原来 Hibernate 对 'more than one result' 的解释似乎是 'more than one unique result'.

事实上,我使用所有 JPA 提供程序测试了您的场景,结果是:

  • Hibernate 确实 return 与 getResultList() 重复,但 不会 抛出异常,因为 getSingleResult() 的实现方式很特殊
  • EclipseLink 是唯一一个不受 getResultList() 中重复结果错误影响的链接,因此,getSingleResult() 不会 抛出异常,或者(对我来说,这种行为只是合乎逻辑的,但事实证明,这完全是一个解释问题)
  • OpenJPA 和 DataNucleus 都 return 从 getResultList() 复制结果并从 getSingleResult()
  • 抛出异常

Tl;DR

I need someone to explain me if this is expected or if I misunderstood something.

这实际上归结为您如何解释规范