mention/fast-doctrine-paginator

适用于分批、无限滚动、Relay的快速Doctrine分页器

2.0.0 2023-12-06 17:55 UTC

This package is auto-updated.

Last update: 2024-09-06 20:29:51 UTC


README

此包提供了一个快速、无偏移的Doctrine分页器,适用于无限滚动、批处理作业和GraphQL/Relay分页。

Build Status Latest Version MIT License PHPStan Enabled

安装

composer require mention/fast-doctrine-paginator

为什么使用这个分页器?

它很快

使这个分页器快速的原因是它依赖于seek/keyset分页,而不是limit/offset分页。更多信息请访问https://use-the-index-luke.com/no-offset

除此之外,它还允许用户编写自己的优化查询。

它适用于批处理作业

使用Doctrine查询的批处理作业通常有两个问题

  • 查询一次返回太多结果并耗尽内存(即使使用iterate(),因为查询结果仍然在本地缓冲)
  • 实体在UnitOfWork中积累并耗尽内存

使用这个分页器可以解决这两个问题

  • 通过使用分页查询,我们避免了一次性获取太多项目
  • 通过在每个页面前后提供操作的机会,分页器提供了一个安全点,以便可以清除实体管理器,以保持受管理实体的数量在可控范围内

它适用于GraphQL/Relay

GraphQL/Relay风格的分页需要指向页面开始和结束以及页面中每个项目的精细粒度游标。此分页器提供这些。

它适用于无限滚动

每一页都将与上一页一样快。

它是类型安全的

分页器完全类型化,并支持phpstan-doctrine对查询结果的静态类型推断。

何时不使用此分页器?

此分页器基于游标,因此如果您需要通过编号访问页面,可能不适合您(尽管可以模拟)。

用法

以下是一个典型示例

<?php

$query = $entityManager->createQuery('
    SELECT   u.id, u.name
    FROM     Acme\\Entity\\Users u
    WHERE    u.id > :idCursor
    ORDER    BY u.id ASC
');

// Number of items per page
$query->setMaxResults(3);

$paginator = DoctrinePaginatorBuilder::fromQuery($query)
    ->setDiscriminators([
        new PageDiscriminator('idCursor', 'getId'),
    ])
    ->build();

foreach ($paginator as $page) {
    foreach ($page() as $user) {
        // ...
    }
    $em->clear();
}

使用用户定义的查询来获取一页结果,在获取多页时将多次执行。当查询返回无结果时,分页停止。

每页元素的数量是通过在查询本身上调用setMaxResults()来定义的。

分页是基于keyset的,而不是基于limit/offset:我们不是请求查询中的第N行,而是请求高于上一页最后一行最高的行。在比较行时,我们只考虑一个或几个称为区分符的列。我们使用一页最后一行区分符列的值来创建一个内部游标,该游标可以用于获取下一页。

为了有效地工作,以下条件必须成立

  • 用于区分的列必须是唯一的(如果不是,必须使用多个列的组合)
  • 查询必须按区分列排序
  • 查询必须有一个WHERE子句,该子句选择仅选择区分符高于上一页更高区分符的行。

关于查询

查询必须是一个Doctrine查询对象。它必须定义一个最大结果数(setMaxResults()),因为这定义了每页的项目数量。

它必须按区分器列排序。

它必须有一个WHERE子句,只选择上页的区分器更高的行。分页器通过调用setParameter()在查询中设置这些值。

示例

带有用户表的示例

如果我们按id排序,我们可以使用id作为区分器,因为它是唯一的。

注意ORDER子句,它按我们的区分器排序。

注意WHERE子句,它按我们的区分器区分。我们使用查询参数 :idCursor。此参数的值由分页器自动设置。默认情况下,当请求第一页时,它设置为0,然后它自动更新为最新获取的页的最后一行的值。

<?php

$query = $entityManager->createQuery('
    SELECT   u.id, u.name
    FROM     Users u
    WHERE    u.id > :idCursor
    ORDER    BY u.id ASC
');

// Max results per page
$query->setMaxResults(3);

$paginator = DoctrinePaginatorBuilder::fromQuery($query)
    ->setDiscriminators([
        new PageDiscriminator('idCursor', 'getId'),
    ])
    ->build();

foreach ($paginator as $page) {
    foreach ($page() as $result) {
        // ...
    }
    $em->clear();
}

第一页将返回这个

分页器将内部保留id=3作为游标。在请求下一页之前,分页器在查询上调用setParameter('idCursor', 3)。正如预期的那样,第二页返回这个

按名称排序

如果我们按名称排序,我们不能直接使用它作为区分器,因为它是非唯一的。如果我们按名称和id排序,我们可以使用名称和id作为区分器,因为它们在一起是唯一的。

注意我们如何使用u.id作为当前名称游标相等的后备。

<?php

$query = $entityManager->createQuery('
    SELECT   u.id, u.name
    FROM     Users u
    WHERE    u.name > :nameCursor
    OR       (u.name = :nameCursor AND u.id > :idCursor)
    ORDER    BY u.name ASC, u.id ASC
');

$paginator = DoctrinePaginatorBuilder::fromQuery($query)
    ->setDiscriminators([
        new PageDiscriminator('nameCursor', 'getName'),
        new PageDiscriminator('idCursor', 'getId'),
    ])
    ->build();

总结分页

我们可以通过显式设置游标来在新的DoctrinePaginator实例上恢复分页。这在通过多个请求分页时非常有用。

可以通过在PaginatorItem对象上调用getCursor()来检索页面的末尾游标。使用此游标将获取下一页。

可以通过在DoctrinePaginatorBuilder上调用setCursor()来构建一个在当前位置恢复的分页器。

批处理作业

分页器特别适合批处理,因为它允许控制内存使用。

  • 查询返回受控数量的行
  • 这提供了在每个页面前后采取行动的机会

例如,实体管理器可以在两个页面之间安全地清除

<?php

foreach ($paginator as $page) {
    foreach ($page() as $result) {
        // ...
    }
    $em->clear();
}

GraphQL/Relay

分页器特别适合GraphQL/Relay分页,因为它为页面和项目提供了游标。

  • 对于页面:在PaginatorPageInterface上使用firstCursor() / lastCursor()。
  • 对于项目:在PaginatorPageItemInterface上使用getCursor()。
<?php

foreach ($paginator as $page) {
    $startCursor = $page->firstCursor();
    $endCursor = $page->lastCursor();
    foreach ($page->items() as $item) {
        $itemCursor = $item->getCursor();
    }
}

作者

Mention 团队和贡献者