提示 HINT_PASS_DISTINCT_THROUGH 将 PageRequest 每页返回的实体数量减少到配置的页面大小以下 (PostgreSQL)

Est*_*hyl 4 java postgresql jpa repository spring-data

我正在设置一个基于 JPA 规范的存储库实现,该实现利用 jpa 规范(基于 RSQL 过滤器字符串构建)来过滤结果、定义结果排序并通过“不同”删除任何重复项,否则这些重复项将因连接表而返回。JPA 规范构建器方法连接多个表并设置“不同”标志:

final Join<Object, Object> rootJoinedTags = root.join("tags", JoinType.LEFT);
final Join<Object, Object> rootJoinedLocations = root.join("location", JoinType.LEFT);
...
query.distinct(true);
Run Code Online (Sandbox Code Playgroud)

为了允许按连接表列排序,我已将“HINT_PASS_DISTINCT_THROUGH”提示应​​用于相关存储库方法(否则,按连接表列排序会返回类似“排序列必须包含在 SELECT DISTINCT 查询中”的错误) 。

@QueryHints(value = {
        @QueryHint(name = org.hibernate.jpa.QueryHints.HINT_PASS_DISTINCT_THROUGH, value = "false")
    })
    Page<SomeEntity> findAll(@Nullable Specification<SomeEntity> spec, Pageable pageable);
Run Code Online (Sandbox Code Playgroud)

所述存储库方法的参数构造如下:

final Sort sort = getSort(searchFilter);
        final Specification spec = getSpecificationIfPresent(searchFilter);
        final PageRequest pageRequest = PageRequest.of(searchFilter.getPageNumber(), searchFilter.getLimit(), sort);

        return eventRepository.findAll(spec, pageRequest);
Run Code Online (Sandbox Code Playgroud)

经过这些更改后,过滤和排序似乎按预期工作。但是,该提示似乎会导致在构建结果页面后应用“不同”过滤,从而将页面中返回的实体数量从配置的“大小”PageRequest 参数减少到过滤重复项后留下的任何内容出去。例如,如果我们使用“page=0”和“pageSize=10”创建一个 PageRequest,那么生成的 Page 可能仅返回 5 个“SomeEntity”实例,尽管数据库包含更多条目(准确地说是 177 个实体)这个案例)。如果我删除提示,则返回的实体编号再次正确。

问题:有没有办法使相同的规范查询设置与正确大小的页面一起工作(可能会添加一些其他提示以在构造页面对象之前执行重复过滤)?如果没有,那么我是否可以使用另一种方法来实现所需的基于规范的过滤,以及与“不同”一样的连接列排序和重复删除?

PS:PostgreSQL 是相关应用程序背后的数据库

jcc*_*ero 8

您正在尝试的问题与您使用提示的方式有关HINT_PASS_DISTINCT_THROUGH

此提示允许您指示 HibernateDISTINCT不应在SELECT针对数据库发出的语句中使用该关键字。

您正在利用这一事实来允许您的查询按未包含在DISTINCT列列表中的字段进行排序。

但这不是应该如何使用此提示。

DISTINCT仅当您确定在 SQL 语句中应用或不应用关键字之间没有区别时才必须使用此提示SELECT,因为该语句本身SELECT已经获取了所有不同的值。这个想法是提高查询的性能,避免使用不必要的语句。DISTINCT

query.distinct当您在条件查询中使用该方法并且您是join fetching子关系时,通常会发生这种情况。@VladMihalcea 的这篇精彩文章详细解释了提示的工作原理。

另一方面,当您使用分页时,它将在针对数据库发出的 SQL 语句中设置OFFSETand LIMIT- 或类似的内容,具体取决于底层数据库SELECT,从而限制查询的最大结果数。

如前所述,如果您使用HINT_PASS_DISTINCT_THROUGH提示,则SELECT语句将不包含DISTINCT关键字,并且由于您的连接,它可能会给出主实体的重复记录。该记录将由 Hibernate 处理以区分重复项,因为您正在使用query.distinct,并且实际上如果需要,它会删除重复项。我认为这就是为什么您获得的记录可能少于您的Pageable.

如果你删除提示,由于DISTINCT关键字是在发送到数据库的SQL语句中传递的,只要你只投影主要实体的信息,它就会获取 指示的所有记录,LIMIT这就是为什么它会给你始终是请求的记录数。

您可以尝试使用fetch join您的子实体(而不是仅join使用它们)。它将消除无法使用关键字列中需要排序的字段的问题DISTINCT,此外,您现在将能够合法地应用提示。

但如果你这样做,就会出现另一个问题:如果你使用 join fetch 和分页来返回主要实体及其集合,Hibernate 将不再在数据库级别应用分页 - 它不会在SQL 语句中包含OFFSET或关键字,并且LIMIT它会尝试对内存中的结果进行分页。这是著名的 HibernateHHH000104警告:

HHH000104: firstResult/maxResults specified with collection fetch; applying in memory!
Run Code Online (Sandbox Code Playgroud)

@VladMihalcea 在本文的最后部分详细解释了这一点。

他还提出了一种可能的解决方案来解决您的问题,即Window Functions

在您的用例中,不使用Specifications,而是实现您自己的 DAO。这个 DAO 只需要访问EntityManager,这并不是什么大不了的事,因为你可以注入你的@PersistenceContext

HHH000104: firstResult/maxResults specified with collection fetch; applying in memory!
Run Code Online (Sandbox Code Playgroud)

一旦有了这个EntityManager,您就可以创建本机查询并使用窗口函数根据提供的Pageable信息构建将针对数据库发出的正确 SQL 语句。这将为您提供更多自由来决定使用哪些字段进行排序或您需要的任何内容。

正如最后引用的文章所示,窗口函数是所有市长数据库都支持的功能。

对于 PostgreSQL,您可以在官方文档中轻松找到它们。

最后,还有一个选项,实际上是由 @nickshoe 建议的,并在他引用的文章中进行了详细解释,是分两个阶段执行排序和分页过程:在第一阶段,您需要创建一个查询来引用您的子实体,您将在其中应用分页和排序。此查询将允许您识别将在流程的第二阶段中使用的主要实体的 ID,以获取主要实体本身。

您可以利用前面提到的自定义 DAO 来完成此过程。