Руководство по пагинации в Laravel 11

Источник: «A Guide to Pagination in Laravel»
Узнайте о различных типах пагинации, доступных в Laravel, и как их использовать. Также рассмотрим основные генерируемые SQL запросы и как решить, какой подход к пагинации использовать.

Пагинация — одна из востребованных функций в веб-приложениях. Практически в каждом приложении Laravel, с которым мне приходилось работать, была реализована та или иная форма пагинации.

Однако что же такое пагинация и почему мы так часто её используем? Как внедрить эту функциональность в Laravel-приложения? И как определиться с подходящим методом пагинации?

В статье постараемся ответить на эти вопросы и подробно рассмотрим, как использовать пагинацию в Laravel, как для представлений Blade, так и для конечных точек API. К концу нашего исследования вы сможете уверенно применять пагинацию в своих проектах.

Что такое пагинация

Пагинация — техника, используемая для разделения большого набора данных на более мелкие фрагменты (или страницы). Она позволяет отображать подмножество данных, а не все возможные значения сразу.

Представьте, что есть страница, на которой выводятся имена всех пользователей приложения. Если бы были тысячи пользователей, было бы нецелесообразно отображать их всех на одной странице. Вместо этого можно использовать пагинацию, отображая на каждой странице подмножество пользователей (скажем, 10 пользователей за раз) и позволяя пользователям переходить между страницами, чтобы просмотреть больше пользователей (следующие 10).

Используя пагинацию, можно:

Пагинацию можно разделить на два типа:

Laravel предоставляет три различных метода пагинации запросов Eloquent в приложениях:

Давайте рассмотрим каждый из этих методов более подробно.

Использование метода paginate

Метод paginate позволяет получить подмножество данных из базы данных на основе смещения и ограничения (рассмотрим их позже, когда будем изучать базовые SQL-запросы).

Использовать метод paginate можно следующим образом:

use App\Models\User;

$users = User::query()->paginate();

Выполнение приведённого выше кода приведёт к тому, что $users станет экземпляром Illuminate\Contracts\Pagination\LengthAwarePaginator, обычно это объект Illuminate\Pagination\LengthAwarePaginator. Этот экземпляр пагинатора содержит всю информацию, необходимую для постраничного отображения данных в приложении.

Метод paginate может автоматически определять номер запрашиваемой страницы на основе параметра запроса страницы в URL. Например, если посетить страницу https://my-app.com/users?page=2, метод paginate получит вторую страницу данных.

По умолчанию все методы пагинации в Laravel по умолчанию получают 15 записей за раз. Однако это значение можно изменить на другое (рассмотрим, как это сделать позже).

Использование paginate в представлениях Blade

Давайте рассмотрим, как использовать метод paginate при отображении данных в представлении Blade.

Представьте, что есть простой маршрут, извлекающий пользователей из базы данных в постраничном формате и передающий их в представление:

use App\Models\User;
use Illuminate\Support\Facades\Route;

Route::get('users', function () {
$users = User::query()->paginate();

return view('users.index', [
'users' => $users,
]);
});

Файл resources/views/users/index.blade.php может выглядеть так:

<html>
<head>
<title>Paginate</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>

<body>
<div class="max-w-5xl mx-auto py-8">
<h1 class="text-5xl">Paginate</h1>

<ul class="py-4">
@foreach ($users as $user)
<li class="py-1 border-b">{{ $user->name }}</li>
@endforeach
</ul>

{{ $users->links() }}
</div>
</body>
</html>

В результате страница будет выглядеть примерно так:

Пример страницы пагинации с использованием метода paginate

Пример страницы пагинации с использованием метода paginate

Давайте разберёмся, что происходит в представлении Blade:

Кроме того, видно, что метод paginate даёт обзор данных пагинации. Можно видеть, что просматриваются записи с 16-й по 30-ю из 50 записей. Также видно, что находимся на второй странице, а всего здесь 4 страницы.

Важно отметить, что метод links возвращает HTML, стилизованный с помощью Tailwind CSS. Если хотите использовать что-то другое, помимо Tailwind, или самостоятельно стилизовать ссылки пагинации, можете ознакомиться с документацией по настройке представлений пагинации.

Использование paginate в конечных точках API

Помимо использования метода paginate в представлениях Blade, его можно применять и в конечных точках API. Laravel упрощает этот процесс, автоматически преобразуя постраничные данные в JSON.

Например, можно создать конечную точку /api/users (добавив следующий маршрут в файл routes/api.php), возвращающую пагинацию пользователей в формате JSON:

use App\Models\User;
use Illuminate\Support\Facades\Route;

Route::get('paginate', function () {
return User::query()->paginate();
});

Обращение к конечной точке /api/users вернёт ответ в формате JSON, похожий на следующий (обратите внимание, что для краткости я ограничил поле data только 3 записями):

{
"current_page": 1,
"data": [
{
"id": 1,
"name": "Andy Runolfsson",
"email": "teresa.wiegand@example.net",
"email_verified_at": "2024-10-15T23:19:28.000000Z",
"created_at": "2024-10-15T23:19:29.000000Z",
"updated_at": "2024-10-15T23:19:29.000000Z"
},
{
"id": 2,
"name": "Rafael Cummings",
"email": "odessa54@example.org",
"email_verified_at": "2024-10-15T23:19:28.000000Z",
"created_at": "2024-10-15T23:19:29.000000Z",
"updated_at": "2024-10-15T23:19:29.000000Z"
},
{
"id": 3,
"name": "Reynold Lindgren",
"email": "juwan.johns@example.net",
"email_verified_at": "2024-10-15T23:19:28.000000Z",
"created_at": "2024-10-15T23:19:29.000000Z",
"updated_at": "2024-10-15T23:19:29.000000Z"
}
],
"first_page_url": "http://example.com/users?page=1",
"from": 1,
"last_page": 4,
"last_page_url": "http://example.com/users?page=4",
"links": [
{
"url": null,
"label": "&laquo; Previous",
"active": false
},
{
"url": "http://example.com/users?page=1",
"label": "1",
"active": true
},
{
"url": "http://example.com/users?page=2",
"label": "2",
"active": false
},
{
"url": "http://example.com/users?page=3",
"label": "3",
"active": false
},
{
"url": "http://example.com/users?page=4",
"label": "4",
"active": false
},
{
"url": "http://example.com/users?page=5",
"label": "5",
"active": false
},
{
"url": "http://example.com/users?page=2",
"label": "Next &raquo;",
"active": false
}
],
"next_page_url": "http://example.com/users?page=2",
"path": "http://example.com/users",
"per_page": 15,
"prev_page_url": null,
"to": 15,
"total": 50
}

Давайте разберём ответ JSON:

Основные SQL запросы метода paginate

Использование метода paginate в Laravel приводит к выполнению двух SQL запросов:

Если необходимо получить первую страницу пользователей (с 15 пользователями на странице), будут выполнены следующие SQL запросы:

select count(*) as aggregate from `users`

и

select * from `users` limit 15 offset 0

Во втором запросе видно, что значение limit равно 15. Это количество записей, возвращаемых на одну страницу.

Значение offset рассчитывается следующим образом:

Смещение = Размер страниц * (Страница - 1)

Таким образом, если хотим получить третью страницу пользователей, значение offset будет вычислено как:

Смещение = 15 * (3 - 1)

Поэтому значение offset будет равно 30, и получим записи с 31-й по 45-ю. Запросы для третьей страницы будут выглядеть следующим образом:

select count(*) as aggregate from `users`

и

select * from `users` limit 15 offset 30

Использование метода simplePaginate

Метод simplePaginate очень похож на paginate, но с одним ключевым отличием. Метод simplePaginate не получает общее количество записей в наборе данных.

Как было показано ранее, при использовании метода paginate получаем информацию об общем количестве записей и страниц, имеющихся в наборе данных. Затем эту информацию можно использовать для отображения таких вещей, как общее количество страниц, в пользовательском интерфейсе или в ответе API.

Но если вы не собираетесь отображать эти данные пользователю (или разработчику, использующему API), то можно избежать ненужного запроса к базе данных (подсчитывающего общее количество записей), используя метод simplePaginate.

Метод simplePaginate можно использовать так же, как и метод paginate:

use App\Models\User;

$users = User::query()->simplePaginate();

Выполнение приведённого выше кода приведёт к тому, что $users будет экземпляром Illuminate\Contracts\Pagination\Paginator, обычно это объект Illuminate\Pagination\Paginator.

В отличие от объекта Illuminate\Pagination\LengthAwarePaginator, возвращаемого методом paginate, объект Illuminate\Pagination\Paginator не содержит информации об общем количестве записей в наборе данных и не имеет представления о количестве страниц или общем количестве записей. Он знает только о текущей странице данных и о том, есть ли ещё записи, которые можно получить.

Использование simplePaginate в представлениях Blade

Давайте рассмотрим, как можно использовать метод simplePaginate с представлением Blade. Предположим, что у нас тот же маршрут, что и раньше, но на этот раз используется метод simplePaginate:

use App\Models\User;
use Illuminate\Support\Facades\Route;

Route::get('users', function () {
$users = User::query()->simplePaginate();

return view('users.index', [
'users' => $users,
]);
});

Создадим представление Blade аналогично предыдущему:

<html>
<head>
<title>Simple Paginate</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>

<body>
<div class="max-w-5xl mx-auto py-8">
<h1 class="text-5xl">Simple Paginate</h1>

<ul class="py-4">
@foreach ($users as $user)
<li class="py-1 border-b">{{ $user->name }}</li>
@endforeach
</ul>

{{ $users->links() }}
</div>
</body>
</html>

Полученная страница будет выглядеть примерно так:

Пример страницы пагинации с использованием метода simplePaginate

Пример страницы пагинации с использованием метода simplePaginate

Как видно из примера, вывод $users->links() отличается от результата использования метода paginate. Поскольку метод simplePaginate не получает общее количество записей, у него нет контекста общего количества страниц или записей, а только то, есть ли следующая страница или нет. Поэтому в ссылках пагинации отображаются только ссылки Previous и Next.

Использование simplePaginate в конечных точках API

Также можно использовать метод simplePaginate в конечных точках API. Laravel автоматически преобразует постраничные данные в JSON.

Давайте создадим конечную точку /api/users, возвращающую пользователей с постраничной разбивкой в формате JSON:

use App\Models\User;
use Illuminate\Support\Facades\Route;

Route::get('users', function () {
return User::query()->simplePaginate();
});

Если пройти по этому маршруту, получим ответ в формате JSON, похожий на следующий (для краткости я ограничил поле data только 3 записями):

{
"current_page": 1,
"data": [
{
"id": 1,
"name": "Andy Runolfsson",
"email": "teresa.wiegand@example.net",
"email_verified_at": "2024-10-15T23:19:28.000000Z",
"created_at": "2024-10-15T23:19:29.000000Z",
"updated_at": "2024-10-15T23:19:29.000000Z"
},
{
"id": 2,
"name": "Rafael Cummings",
"email": "odessa54@example.org",
"email_verified_at": "2024-10-15T23:19:28.000000Z",
"created_at": "2024-10-15T23:19:29.000000Z",
"updated_at": "2024-10-15T23:19:29.000000Z"
},
{
"id": 3,
"name": "Reynold Lindgren",
"email": "juwan.johns@example.net",
"email_verified_at": "2024-10-15T23:19:28.000000Z",
"created_at": "2024-10-15T23:19:29.000000Z",
"updated_at": "2024-10-15T23:19:29.000000Z"
}
],
"first_page_url": "http://example.com/users?page=1",
"from": 1,
"next_page_url": "http://example.com/users?page=2",
"path": "http://example.com/users",
"per_page": 15,
"prev_page_url": null,
"to": 15
}

Как видите, ответ в формате JSON очень похож на ответ, полученный при использовании метода paginate. Ключевое отличие заключается в том, что в ответе отсутствуют поля last_page, last_page_url, links и total.

Основные SQL запросы метода simplePaginate

Давайте рассмотрим базовые SQL запросы, выполняющиеся при использовании метода simplePaginate.

Метод simplePaginate по-прежнему опирается на значения limit и offset для получения подмножества данных из базы данных. Однако он не выполняет запрос для получения общего количества записей в наборе данных.

Значение offset вычисляется так же, как и раньше:

Смещение = Размер страницы * (Страница - 1)

Однако значение limit вычисляется несколько иначе, чем в методе paginate. Оно рассчитывается:

Ограничение = Размер страницы + 1

Это происходит потому, что метод simplePaginate должен получить на одну запись больше, чем значение perPage, чтобы определить, есть ли ещё записи. Допустим, мы получаем 15 записей на страницу. Значение limit будет равно 16. Таким образом, если будет возвращено 16 записей, мы будем знать, что есть ещё как минимум одна страница данных для получения. Если будет возвращено менее 16 записей, мы будем знать, что находимся на последней странице данных.

Таким образом, если необходимо получить первую страницу пользователей (с 15 пользователями на странице), будут выполнены следующие SQL запросы:

select * from `users` limit 16 offset 0

Запрос для второй страницы будет выглядеть следующим образом:

select * from `users` limit 16 offset 15

Использование метода cursorPaginate

До сих пор мы рассматривали методы paginate и simplePaginate, использующие пагинацию на основе смещения. Теперь рассмотрим метод cursorPaginate, использующий пагинацию на основе курсора.

Сразу предупреждаю, что пагинация на основе курсора может показаться немного запутанной, если сталкиваетесь с ней впервые. Поэтому не волнуйтесь, если не сразу всё поймёте. Надеюсь, к концу этой статьи будете лучше понимать, как она работает. В конце статьи я также размещу замечательное видео, более подробно объясняющее пагинацию на основе курсора.

При пагинации на основе смещения используются значения limit и offset для получения подмножества данных из базы данных. Таким образом, можно сказать: Пропустить первые 10 записей и получить следующие 10 записей. Это просто для понимания и легко для реализации. В то время как при пагинации на основе курсора используется курсор (обычно уникальный ID конкретной записи в базе данных) в качестве отправной точки для получения предыдущего/следующего набора записей.

Например, сделаем запрос на получение первых 15 пользователей. Предположим, что идентификатор 15-го пользователя равен 20. Когда нужно получить следующие 15 пользователей, будем использовать ID 15-го пользователя (20) в качестве курсора. Мы скажем: Найти следующие 15 пользователей с идентификатором больше 20.

Иногда курсоры называют "токенами"/"tokens", "ключами"/"keys", "следующий"/"next", "предыдущий"/"previous" и так далее. По сути, они представляют ссылку на конкретную запись в базе данных. Структуру курсоров рассмотрим позже в этом разделе, когда разберёмся с базовыми SQL-запросами.

Laravel позволяет с лёгкостью использовать пагинацию на основе курсоров с помощью метода cursorPaginate:

use App\Models\User;

$users = User::query()->cursorPaginate();

Выполнение приведённого выше кода приведёт к тому, что поле $users станет экземпляром Illuminate\Contracts\Pagination\CursorPaginator, обычно объектом Illuminate\Pagination\CursorPaginator. Этот экземпляр пагинатора содержит всю информацию, необходимую для отображения постраничных данных в приложении.

Как и метод simplePaginate, метод cursorPaginate не получает общее количество записей в наборе данных. Он знает только о текущей странице данных и о том, есть ли ещё записи для получения, поэтому общее количество страниц или записей не известно.

Использование cursorPaginate в представлениях Blade

Давайте рассмотрим, как использовать метод cursorPaginate при отображении данных в представлении Blade. Как и в предыдущих примерах, предположим, что есть простой маршрут, извлекающий пользователей из базы данных в постраничном формате и передающий их в представление:

use App\Models\User;
use Illuminate\Support\Facades\Route;

Route::get('users', function () {
$users = User::query()->cursorPaginate();

return view('users.index', [
'users' => $users,
]);
});

Представление Blade может выглядеть следующим образом:

<html>
<head>
<title>Cursor Paginate</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>

<body>
<div class="max-w-5xl mx-auto py-8">
<h1 class="text-5xl">Cursor Paginate</h1>

<ul class="py-4">
@foreach ($users as $user)
<li class="py-1 border-b">{{ $user->name }}</li>
@endforeach
</ul>

{{ $users->links() }}
</div>
</body>
</html>

В результате отобразится страница, похожая на представленную ниже:

Пример страницы пагинации с использованием метода cursorPaginate

Пример страницы пагинации с использованием метода cursorPaginate

Как видим, поскольку метод cursorPaginate не получает общее количество записей в наборе данных, вывод $users->links() похож на вывод, полученный при использовании метода SimplePaginate. В ссылках пагинации видны только ссылки Previous и Next.

Использование cursorPaginate в конечных точках API

Laravel также позволяет использовать метод cursorPaginate в конечных точках API и автоматически конвертирует постраничные данные в JSON.

Давайте создадим конечную точку /api/users, возвращающую пагинацию пользователей в формате JSON:

use App\Models\User;
use Illuminate\Support\Facades\Route;

Route::get('users', function () {
return User::query()->cursorPaginate();
});

Перейдя по этому маршруту, получим ответ в формате JSON, похожий на следующий (для краткости я ограничил поле data только 3 записями):

{
"data": [
{
"id": 1,
"name": "Andy Runolfsson",
"email": "teresa.wiegand@example.net",
"email_verified_at": "2024-10-15T23:19:28.000000Z",
"created_at": "2024-10-15T23:19:29.000000Z",
"updated_at": "2024-10-15T23:19:29.000000Z"
},
{
"id": 2,
"name": "Rafael Cummings",
"email": "odessa54@example.org",
"email_verified_at": "2024-10-15T23:19:28.000000Z",
"created_at": "2024-10-15T23:19:29.000000Z",
"updated_at": "2024-10-15T23:19:29.000000Z"
},
{
"id": 3,
"name": "Reynold Lindgren",
"email": "juwan.johns@example.net",
"email_verified_at": "2024-10-15T23:19:28.000000Z",
"created_at": "2024-10-15T23:19:29.000000Z",
"updated_at": "2024-10-15T23:19:29.000000Z"
}
],
"path": "http://example.com/users",
"per_page": 15,
"next_cursor": "eyJ1c2Vycy5pZCI6MTUsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0",
"next_page_url": "http://example.com/users?cursor=eyJ1c2Vycy5pZCI6MTUsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0",
"prev_cursor": null,
"prev_page_url": null
}

Как видим, ответ в формате JSON похож на предыдущие ответы, но с небольшими отличиями. Поскольку мы не получаем общее количество записей, в ответе нет полей last_page, last_page_url, links или total. Также можно заметить, что нет полей from и to.

Вместо них есть поля next_cursor и prev_cursor, содержащие курсор для следующей и предыдущей страниц данных. Поскольку мы находимся на первой странице, поля prev_cursor и prev_page_url равны null. Однако поля next_cursor и next_page_url установлены.

Поле next_cursor представляет собой закодированную в base-64 строку, содержащую курсор для следующей страницы данных. Если декодировать поле next_cursor, то получится что-то вроде этого (улучшено для удобства чтения):

{
"users.id": 15,
"_pointsToNextItems": true
}

Курсор содержит две части информации:

Давайте посмотрим, как может выглядеть вторая страница данных (для краткости снова сокращённая до 3 записей):

{
"data": [
{
"id": 16,
"name": "Durward Nikolaus",
"email": "xkuhic@example.com",
"email_verified_at": "2024-10-15T23:19:28.000000Z",
"created_at": "2024-10-15T23:19:29.000000Z",
"updated_at": "2024-10-15T23:19:29.000000Z"
},
{
"id": 17,
"name": "Dr. Glenda Cruickshank IV",
"email": "kristoffer.schiller@example.org",
"email_verified_at": "2024-10-15T23:19:28.000000Z",
"created_at": "2024-10-15T23:19:29.000000Z",
"updated_at": "2024-10-15T23:19:29.000000Z"
},
{
"id": 18,
"name": "Prof. Dolores Predovic",
"email": "frankie.schultz@example.net",
"email_verified_at": "2024-10-15T23:19:28.000000Z",
"created_at": "2024-10-15T23:19:29.000000Z",
"updated_at": "2024-10-15T23:19:29.000000Z"
}
],
"path": "http://example.com/users",
"per_page": 15,
"next_cursor": "eyJ1c2Vycy5pZCI6MzAsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0",
"next_page_url": "http://example.com/users?cursor=eyJ1c2Vycy5pZCI6MzAsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0",
"prev_cursor": "eyJ1c2Vycy5pZCI6MTYsIl9wb2ludHNUb05leHRJdGVtcyI6ZmFsc2V9",
"prev_page_url": "http://example.com/users?cursor=eyJ1c2Vycy5pZCI6MTYsIl9wb2ludHNUb05leHRJdGVtcyI6ZmFsc2V9"
}

Видно, что поля prev_cursor и prev_page_url установлены, а поля next_cursor и next_page_url обновлены курсором для следующей страницы данных.

Основные SQL запросы метода cursorPaginate

Чтобы лучше понять, как работает пагинация курсора, рассмотрим основные SQL запросы, выполняющиеся при использовании метода cursorPaginate.

На первой странице данных (содержащей 15 записей) будет выполнен следующий SQL запрос:

select * from `users` order by `users`.`id` asc limit 16

Как видно, извлекаются первые 16 записей из таблицы users и упорядочиваются по столбцу id в порядке возрастания. Как и в методе simplePaginate, при получении 16 строк необходимо определить, есть ли ещё записи для получения.

Представим, что переходим к следующей странице элементов с помощью соответствующего курсора:

eyJ1c2Vycy5pZCI6MTUsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0

При декодировании этого курсора получаем объект JSON следующего вида:

{
"users.id": 15,
"_pointsToNextItems": true
}

Затем Laravel выполнит следующий SQL запрос, для получения очередного набора записей:

select * from `users` where (`users`.`id` > 15) order by `users`.`id` asc limit 16

Как видите, мы получаем следующие 16 записей из таблицы users, имеющие id больше 15 (поскольку 15 был последним ID на предыдущей странице).

Теперь предположим, что идентификатор первого пользователя на странице 2 равен 16. При переходе к первой странице данных со второй страницы будет использоваться следующий курсор:

eyJ1c2Vycy5pZCI6MTYsIl9wb2ludHNUb05leHRJdGVtcyI6ZmFsc2V9

При его декодировании получаем следующий объект JSON:

{
"users.id": 16,
"_pointsToNextItems": false
}

Когда переходим к следующей странице результатов, в качестве курсора используется последняя найденная запись. Когда возвращаемся на предыдущую страницу результатов, в качестве курсора используется первая найденная запись. По этой причине мы видим, что значение users.id в курсоре установлено на 16. Также можно заметить, что значение _pointsToNextItems установлено в false, потому что переходим к предыдущему набору элементов.

В результате для получения предыдущего набора записей будет выполнен следующий SQL запрос:

select * from `users` where (`users`.`id` < 16) order by `users`.`id` desc limit 16

Как видим, ограничение where теперь проверяет записи с id меньше 16 (поскольку 16 был первым ID на странице 2), а результаты упорядочены по убыванию.

Использование ресурсов API с пагинацией

До сих пор в примерах API мы просто возвращали пагинацию данных непосредственно из контроллера. Однако в реальном приложении, скорее всего, понадобится обработать данные перед тем, как вернуть их пользователю. Это может быть что угодно: добавление или удаление полей, преобразование типов данных или даже преобразование данных в другой формат. По этой причине, скорее всего, придётся использовать API Resources, поскольку они позволяют последовательно преобразовывать данные перед отправкой.

Laravel позволяет использовать ресурсы API наряду с пагинацией. Давайте рассмотрим пример, как это сделать.

Представьте, что создали класс ресурса API App\Http\Resources\UserResource, преобразующий пользовательские данные перед отправкой. Это может выглядеть примерно так:

declare(strict_types=1);

namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

final class UserResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
];
}
}

В методе toArray определяем, что каждый раз, когда обрабатывается пользователь через этот ресурс, необходимо возвращать только поля id, name и email.

Давайте создадим простую конечную точку API /api/users в файле routes/api.php, возвращающую постранично пользователей с помощью App\Http\Resources\UserResource:

use App\Models\User;
use App\Http\Resources\UserResource;
use Illuminate\Support\Facades\Route;

Route::get('users', function () {
$users = User::query()->paginate();

return UserResource::collection(resource: $users);
});

В приведённом выше коде мы получаем из базы данных одну страницу пользователей (предположим, что это первая страница, содержащая 15 пользователей). Затем передаём поле $users (являющееся экземпляром Illuminate\Pagination\LengthAwarePaginator) в метод UserResource::collection. Этот метод преобразует постраничные данные, используя App\Http\Resources\UserResource, прежде чем вернуть их пользователю.

При обращении к конечной точке /api/users получим JSON-ответ, похожий на следующий (для краткости поле data ограничено всего 3 записями):

{
"data": [
{
"id": 1,
"name": "Andy Runolfsson",
"email": "teresa.wiegand@example.net"
},
{
"id": 2,
"name": "Rafael Cummings",
"email": "odessa54@example.org"
},
{
"id": 3,
"name": "Reynold Lindgren",
"email": "juwan.johns@example.net"
}
],
"links": {
"first": "http://example.com/users?page=1",
"last": "http://example.com/users?page=4",
"prev": null,
"next": "http://example.com/users?page=2"
},
"meta": {
"current_page": 1,
"from": 1,
"last_page": 4,
"links": [
{
"url": null,
"label": "&laquo; Previous",
"active": false
},
{
"url": "http://example.com/users?page=1",
"label": "1",
"active": true
},
{
"url": "http://example.com/users?page=2",
"label": "2",
"active": false
},
{
"url": "http://example.com/users?page=3",
"label": "3",
"active": false
},
{
"url": "http://example.com/users?page=4",
"label": "4",
"active": false
},
{
"url": "http://example.com/users?page=2",
"label": "Next &raquo;",
"active": false
}
],
"path": "http://example.com/users",
"per_page": 15,
"to": 15,
"total": 50
}
}

Как видно из приведённого JSON, Laravel определяет, что мы работаем с постраничным набором данных, и возвращает постраничные данные в том же формате, что и раньше. Однако на этот раз пользователи в поле данных содержат только поля id, name и email, которые указаны в классе ресурсов API. Другие поля (current_page, from, last_page, links, path, per_page, to и total) по-прежнему возвращаются, поскольку являются частью пагинации, но они были помещены в поле meta. Также есть поле links, содержащее first, last, prev и next ссылки на различные страницы данных.

Изменение количества записей на страницу

При создании представлений с постраничными данными может понадобиться дать пользователю возможность изменять количество записей, отображаемых на странице. Это может быть сделано с помощью выпадающего списка или поля ввода числа.

Laravel позволяет легко изменять количество записей, отображаемых на странице, передавая параметр perPage методам simplePaginate, paginate и cursorPaginate. Этот параметр позволяет указать количество записей, которые необходимо отображать на каждой странице.

Давайте рассмотрим простой пример, как считывать параметр запроса per_page и использовать его для изменения количества записей, загружаемых на страницу:

use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

Route::get('users', function (Request $request) {
$perPage = $request->integer('per_page', default: 10);

return User::query()->paginate(perPage: $perPage);
});

В приведённом выше примере берём значение параметра запроса per_page. Если значение не указано, по умолчанию устанавливается значение 10. Затем передаём это значение в параметр perPage метода paginate.

После этого можно получить доступ к разным URL:

Как решить, какой метод пагинации использовать

Теперь, когда мы рассмотрели различные типы пагинации и способы их использования в Laravel, давайте обсудим, как решить, какой из этих подходов использовать в приложении.

Нужен номер страницы или общее количество записей

Если для создания пользовательского интерфейса или конечной точки API требуется отображение общего количества записей или страниц, то, вероятно, разумным выбором будет метод paginate.

Если вам не требуется ни то, ни другое, то simplePaginate или cursorPaginate будут более эффективными, поскольку они не выполняют лишних запросов для подсчёта общего количества записей.

Нужен переход на определённую страницу

Если необходимо иметь возможность перейти к определённой странице данных, то больше подойдёт пагинация на основе смещения. Поскольку пагинация на основе курсора является stateful, она полагается на предыдущую страницу, чтобы знать, куда переходить дальше. Поэтому перейти к конкретной странице не так просто.

В то время как при использовании пагинации со смещением можно просто передать номер страницы в запросе (возможно, в качестве параметра запроса) и перейти на эту страницу, без контекста предыдущей страницы.

Насколько большой набор данных

Из-за того, как базы данных обрабатывают значения offset, пагинация на основе смещения становится менее эффективной по мере увеличения количества страниц. Это происходит потому, что при использовании смещения база данных всё равно должна просмотреть все записи до значения смещения. Они просто отбрасываются и не возвращаются в результатах запроса.

Вот отличная статья, объясняющая это более подробно: https://use-the-index-luke.com/no-offset.

Таким образом, по мере роста общего объёма данных в базе данных и увеличения количества страниц, пагинация на основе смещения может стать менее эффективной. В таких случаях пагинация на основе курсора оказывается более производительной, особенно если поле курсора проиндексировано, поскольку предыдущие записи не считываются. По этой причине, если предполагается использовать пагинацию в большом наборе данных, лучше предпочесть пагинацию по курсору, а не по смещению.

Часто ли меняется набор данных

Пагинация на основе смещения может быть сопряжена с проблемами, если базовый набор данных изменяется между запросами.

Давайте рассмотрим пример.

Допустим, в базе данных у нас есть следующие 10 пользователей:

Делаем запрос на получение первой страницы (содержащей 5 пользователей) и получаем следующих пользователей:

Когда переходим на страницу 2, ожидаем получить пользователей с 6 по 10. Однако представим, что до загрузки страницы 2 (пока просматриваем страницу 1) Пользователь 1 удаляется из базы данных. Поскольку размер страницы равен 5, запрос для получения следующей страницы будет выглядеть следующим образом:

select * from `users` limit 5 offset 5

То есть пропускаем первые 5 записей и получаем следующие 5.

В результате на странице 2 окажутся следующие пользователи:

Как видим, Пользователь 6 отсутствует в списке. Это потому, что Пользователь 6 теперь является 5-й записью в таблице, поэтому он фактически находится на первой странице.

У пагинации на основе курсора нет такой проблемы, потому что записи не пропускаются, просто выполняется выборка следующего набора записей на основе курсора. Представим, что в приведённом выше примере использовалась пагинация на основе курсора. Курсором для страницы 2 будет идентификатор Пользователя 5 (который, как предполагаем, равен 5), поскольку это была последняя запись на первой странице. Поэтому запрос страницы 2 может выглядеть следующим образом:

select * from `users` where (`users`.`id` > 5) order by `users`.`id` asc limit 6

Выполнение, приведённого выше запроса, вернёт пользователей с 6 по 10, как и ожидалось.

Надеюсь, это подчёркивает, что пагинация на основе смещения может стать проблематичной, если данные, лежащие в основе, изменяются, добавляются или удаляются в процессе чтения. Она становится менее предсказуемой и может привести к неожиданным результатам.

Создаёте API

Важно помнить, что не обязательно использовать один тип пагинации в приложении. В некоторых местах может быть более уместна пагинация со смещением (например, для UI), а в других случаях более эффективной может быть пагинация на основе курсора (например, при работе с большим набором данных). Таким образом, можно смешивать и сочетать методы пагинации в приложении в зависимости от конкретного случая использования.

Однако если создаёте API, я бы настоятельно рекомендовал быть последовательным и использовать единый подход к пагинации для всех конечных точек. Так разработчикам будет проще понять, как использовать ваш API, и избежать путаницы.

Вы же не хотите, чтобы им пришлось запоминать, какие конечные точки используют пагинацию по смещению, а какие — по курсору.

Конечно, это не является жёстким и непреложным правилом. Если действительно необходимо использовать другой метод пагинации в одной конкретной конечной точке, то действуйте. Но не забудьте указать это в документации, чтобы разработчикам было проще разобраться.

Если предпочитаете видео

Если вы больше склонны к визуальному восприятию, посмотрите замечательное видео (на английском) Аарона Фрэнсиса, в котором более подробно объясняется разница между пагинацией со смещением и пагинацией по курсору:

Заключение

В статье мы рассмотрели различные типы пагинации в Laravel и способы их использования. Также рассмотрели лежащие в их основе SQL запросы и то, как решить, какой метод пагинации использовать в приложении.

Надеюсь, теперь вы почувствуете себя более уверенно при использовании пагинации в приложениях Laravel.

Комментарии


Дополнительные материалы

Предыдущая Статья

Глубокое погружение в сессии Laravel

Следующая Статья

Новое в Symfony 7.2: Индикатор завершения работы консоли