Руководство по пагинации в Laravel 11
Пагинация — одна из востребованных функций в веб-приложениях. Практически в каждом приложении Laravel, с которым мне приходилось работать, была реализована та или иная форма пагинации.
Однако что же такое пагинация и почему мы так часто её используем? Как внедрить эту функциональность в Laravel-приложения? И как определиться с подходящим методом пагинации?
В статье постараемся ответить на эти вопросы и подробно рассмотрим, как использовать пагинацию в Laravel, как для представлений Blade, так и для конечных точек API. К концу нашего исследования вы сможете уверенно применять пагинацию в своих проектах.
Что такое пагинация
Пагинация — техника, используемая для разделения большого набора данных на более мелкие фрагменты (или страницы). Она позволяет отображать подмножество данных, а не все возможные значения сразу.
Представьте, что есть страница, на которой выводятся имена всех пользователей приложения. Если бы были тысячи пользователей, было бы нецелесообразно отображать их всех на одной странице. Вместо этого можно использовать пагинацию, отображая на каждой странице подмножество пользователей (скажем, 10 пользователей за раз) и позволяя пользователям переходить между страницами, чтобы просмотреть больше пользователей (следующие 10).
Используя пагинацию, можно:
- Улучшить производительности приложения — Поскольку вы получаете меньшее подмножество данных за раз, меньше данных нужно получить из базы данных, обработать/преобразовать, а затем возвращать.
- Улучшить пользовательский опыт — Скорее всего, пользователя будет интересовать только небольшое подмножество данных за один раз (обычно на первых нескольких страницах, особенно если используются фильтры и поисковые запросы). Используя пагинацию, можно избежать отображения данных, не интересующих пользователя.
- Улучшить время загрузки страницы — Получая только подмножество данных за один раз, можно уменьшить объем загружаемых на страницу данных, что может улучшить загрузку страницы и время обработки JavaScript.
Пагинацию можно разделить на два типа:
- Пагинация на основе смещения — Это наиболее распространённый тип пагинации, с которым вы, скорее всего, встречались в веб-приложениях, особенно в пользовательских интерфейсах (UI). Она включает выборку подмножества данных из базы данных на основе
смещения
иограничения
. Например, можно получить 10 записей, начиная с 20-й записи, для получения 3-й страницы данных. - Пагинация на основе курсора — Этот тип пагинации предполагает выборку подмножества данных на основе
курсора
. Курсор обычно представляет уникальный идентификатор записи в базе данных. Например, можно получить следующие 10 записей, начиная с записи с ID 20.
Laravel предоставляет три различных метода пагинации запросов Eloquent в приложениях:
paginate
— Использует пагинацию на основе смещения и получает общее количество записей в наборе данных.simplePaginate
— Использует пагинацию на основе смещения, но не получает общее количество записей в наборе данных.cursorPaginate
— Использует пагинацию на основе курсора и не получает общее количество записей в наборе данных.
Давайте рассмотрим каждый из этих методов более подробно.
Использование метода 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>
В результате страница будет выглядеть примерно так:
Давайте разберёмся, что происходит в представлении Blade:
- Перебираем всех пользователей, присутствующих в поле
$users
(объектIlluminate\Pagination\LengthAwarePaginator
), и выводим их имя. - Вызываем метод
links
на объекте$users
. Это очень удобный метод, возвращающий HTML, отображающий ссылки пагинации (например,Previous
,Next
и номера страниц). Это означает, что не нужно беспокоиться о создании ссылок пагинации самостоятельно, Laravel сделает всё это за вас.
Кроме того, видно, что метод 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": "« 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 »",
"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:
current_page
— Текущая страница, на которой находимся. В данном случае это первая страница.data
— Собственно возвращаемые данные. В данном случае это первые 15 пользователей (для краткости сокращённые до 3).first_page_url
— URL-адрес первой страницы данных.from
— Номер стартовой записи возвращаемых данных. В данном случае это первая запись. Если бы находились на второй странице, это было бы 16.last_page
— Номер последней страницы. В данном случае 4 страницы.last_page_url
— URL-адрес последней страницы данных.links
— Массив ссылок на различные страницы данных. Сюда входят ссылкиPrevious
иNext
, а также номера страниц.next_page_url
— URL-адрес следующей страницы данных.path
— Базовый URL конечной точки.per_page
— Количество записей, возвращаемых на одной странице. В данном случае это 15.prev_page_url
— URL-адрес предыдущей страницы данных. В данном случае этоnull
, потому что находимся на первой странице. Если бы находились на второй странице, это был бы URL-адрес первой страницы.to
— Номер конечной записи возвращаемых данных. В данном случае это 15-я запись. Если бы находились на второй странице, это было бы 30.total
— Общее количество записей в наборе данных. В данном случае 50 записей.
Основные 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>
Полученная страница будет выглядеть примерно так:
Как видно из примера, вывод $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
не получает общее количество записей в наборе данных, вывод $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
}
Курсор содержит две части информации:
users.id
— ID последней записи, полученной в наборе данных._pointsToNextItems
— Логическое значение, указывающее нам, на какой набор элементов указывает курсор: следующий или предыдущий. Если значениеtrue
, это означает, что курсор должен быть использован для получения следующего набора записей с идентификатором, большим, чем значениеusers.id
. Если значениеfalse
, это означает, что курсор должен быть использован для получения предыдущего набора записей с ID, меньшим, чем значениеusers.id
.
Давайте посмотрим, как может выглядеть вторая страница данных (для краткости снова сокращённая до 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": "« 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 »",
"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:
https://my-app.com/users
— Отображение первой страницы пользователей с 10 записями на странице.https://my-app.com/users?per_page=5
— Отображение первой страницы пользователей с 5 записями на странице.https://my-app.com/users?per_page=5&page=2
— Отображение второй страницы пользователей с 5 записями на странице.- И так далее...
Как решить, какой метод пагинации использовать
Теперь, когда мы рассмотрели различные типы пагинации и способы их использования в Laravel, давайте обсудим, как решить, какой из этих подходов использовать в приложении.
Нужен номер страницы или общее количество записей
Если для создания пользовательского интерфейса или конечной точки API требуется отображение общего количества записей или страниц, то, вероятно, разумным выбором будет метод paginate
.
Если вам не требуется ни то, ни другое, то simplePaginate
или cursorPaginate
будут более эффективными, поскольку они не выполняют лишних запросов для подсчёта общего количества записей.
Нужен переход на определённую страницу
Если необходимо иметь возможность перейти к определённой странице данных, то больше подойдёт пагинация на основе смещения. Поскольку пагинация на основе курсора является stateful, она полагается на предыдущую страницу, чтобы знать, куда переходить дальше. Поэтому перейти к конкретной странице не так просто.
В то время как при использовании пагинации со смещением можно просто передать номер страницы в запросе (возможно, в качестве параметра запроса) и перейти на эту страницу, без контекста предыдущей страницы.
Насколько большой набор данных
Из-за того, как базы данных обрабатывают значения offset
, пагинация на основе смещения становится менее эффективной по мере увеличения количества страниц. Это происходит потому, что при использовании смещения база данных всё равно должна просмотреть все записи до значения смещения. Они просто отбрасываются и не возвращаются в результатах запроса.
Вот отличная статья, объясняющая это более подробно: https://use-the-index-luke.com/no-offset.
Таким образом, по мере роста общего объёма данных в базе данных и увеличения количества страниц, пагинация на основе смещения может стать менее эффективной. В таких случаях пагинация на основе курсора оказывается более производительной, особенно если поле курсора проиндексировано, поскольку предыдущие записи не считываются. По этой причине, если предполагается использовать пагинацию в большом наборе данных, лучше предпочесть пагинацию по курсору, а не по смещению.
Часто ли меняется набор данных
Пагинация на основе смещения может быть сопряжена с проблемами, если базовый набор данных изменяется между запросами.
Давайте рассмотрим пример.
Допустим, в базе данных у нас есть следующие 10 пользователей:
- Пользователь 1
- Пользователь 2
- Пользователь 3
- Пользователь 4
- Пользователь 5
- Пользователь 6
- Пользователь 7
- Пользователь 8
- Пользователь 9
- Пользователь 10
Делаем запрос на получение первой страницы (содержащей 5 пользователей) и получаем следующих пользователей:
- Пользователь 1
- Пользователь 2
- Пользователь 3
- Пользователь 4
- Пользователь 5
Когда переходим на страницу 2, ожидаем получить пользователей с 6 по 10. Однако представим, что до загрузки страницы 2 (пока просматриваем страницу 1) Пользователь 1 удаляется из базы данных. Поскольку размер страницы равен 5, запрос для получения следующей страницы будет выглядеть следующим образом:
select * from `users` limit 5 offset 5
То есть пропускаем первые 5 записей и получаем следующие 5.
В результате на странице 2 окажутся следующие пользователи:
- Пользователь 7
- Пользователь 8
- Пользователь 9
- Пользователь 10
Как видим, Пользователь 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.