Пагінація — це поширена функція у веб-додатках. Практично кожен Laravel-додаток, над яким я працював, реалізував якусь форму пагінації.
Але що таке пагінація та навіщо вона потрібна? Як можна реалізувати пагінацію у наших Laravel-додатках? І як обрати метод пагінації?
У цій статті ми відповімо на ці питання та розглянемо, як використовувати пагінацію в Laravel як для Blade-видів, так і для API-ендпоінтів. Після прочитання статті ви будете впевнені в можливості впровадження пагінації у своїх проектах.
Пагінація — це техніка, яка дозволяє розділити великий набір даних на менші частини (сторінки). Вона дозволяє виводити підмножину даних, а не всі значення одночасно.
Уявіть, що у вас є сторінка, яка виводить імена всіх користувачів вашого додатка. Якщо у вас тисячі користувачів, то не буде практично виводити їх усіх на одній сторінці. Замість цього ви можете використовувати пагінацію для відображення, наприклад, 10 користувачів одночасно і надавати можливість користувачам переходити між сторінками для перегляду інших користувачів (наступні 10).
Використання пагінації дозволяє:
Зазвичай пагінацію поділяють на два види:
Laravel надає три методи для пагінації запитів Eloquent:
paginate
— Використовує пагінацію на основі зсуву та отримує загальну кількість записів у наборі.simplePaginate
— Використовує пагінацію на основі зсуву, але не отримує загальну кількість записів у наборі.cursorPaginate
— Використовує пагінацію на основі курсора та не отримує загальну кількість записів у наборі.Давайте розглянемо кожен із цих методів детальніше.
Метод paginate
дозволяє отримати підмножину даних із бази, використовуючи зсув і ліміт (ми розглянемо їх детальніше пізніше, коли будемо аналізувати SQL-запити).
Ви можете використовувати метод paginate
наступним чином:
use App\Models\User;
$users = User::query()->paginate();
Запустивши цей код, ви отримаєте $users
як екземпляр Illuminate\Contracts\Pagination\LengthAwarePaginator
, зазвичай це об'єкт Illuminate\Pagination\LengthAwarePaginator
. Цей екземпляр пагінатора містить усю інформацію, що необхідна для відображення пагінованих даних у вашому додатку.
Метод paginate
автоматично визначає запитуваний номер сторінки на основі параметра page
у URL. Наприклад, якщо ви відвідаєте https://my-app.com/users?page=2
, метод paginate
отримає дані для другої сторінки.
За замовчуванням усі методи пагінації в Laravel використовують 15 записів за раз. Однак це значення можна змінити (ми розглянемо, як це зробити пізніше).
Тепер розглянемо, як використовувати метод 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>Пагінація</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body>
<div class="max-w-5xl mx-auto py-8">
<h1 class="text-5xl">Пагінація</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-код для відображення пагінаційних посилань (напр., "Попередня", "Наступна" та номери сторінок). Вам не потрібно турбуватися про створення пагінаційних посилань самостійно, Laravel впорається з цим за вас.Також ми можемо бачити, що метод paginate
надає огляд даних пагінації. Ми можемо бачити, що переглядаємо з 16-го по 30-й записи, з загальної кількості 50. Також видно, що ми на другій сторінці і в нас всього 4 сторінки.
Варто зауважити, що метод links
повертає HTML, оформлений з використанням Tailwind CSS. Якщо ви хочете використовувати щось інше або самостійно стилізувати пагінаційні посилання, ви можете ознайомитися з документацією щодо кастомізації вигляду пагінації.
Також метод paginate
можна використовувати в 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 запиcами для зрозумілості):
{
"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": "« Попередня",
"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": "Далі »",
"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
- Масив посилань на різні сторінки даних. Це включає "Попередня" та "Наступна" посилання, а також номери сторінок.next_page_url
- URL до наступної сторінки даних.path
- Базовий URL ендпоінта.per_page
- Кількість записів, що повертаються за сторінку. Тут 15.prev_page_url
- URL до попередньої сторінки даних. Тут null
, оскільки ми на першій сторінці. Якщо б ми були на другій, це був би URL до першої.to
- Номер кінцевого запису, що повертається. Тут це 15-й запис. Якщо б ми були на другій сторінці, це було б 30.total
- Загальна кількість записів у наборі даних. Тут 50 записів.Використання методу paginate
в Laravel призводить до двох SQL-запитів:
Отже, якщо ми хотіли б отримати першу сторінку користувачів (з 15 користувачами на сторінку), будуть виконані наступні SQL-запити:
select count(*) as aggregate from `users`
і
select * from `users` limit 15 offset 0
У другому запиті ми бачимо, що значення limit
встановлено на 15. Це кількість записів, що повертаються за сторінку.
Значення offset
обчислюється так:
Offset = Розмір сторінки * (Сторінка - 1)
Тож, якщо ми хотіли б отримати третю сторінку користувачів, значення offset
обчислювалося б так:
Offset = 15 * (3 - 1)
Таким чином, значення offset
становитиме 30, і ми отримували б 31-ий до 45-го записи. Запити для третьої сторінки виглядатимуть так:
select count(*) as aggregate from `users`
і
select * from `users` limit 15 offset 30
Метод simplePaginate
дуже схожий на метод paginate
, але з однією ключовою відмінністю: simplePaginate
не отримує загальну кількість записів у наборі.
Як ми вже побачили, коли ми використовуємо метод paginate
, ми також отримуємо інформацію про загальну кількість записів і сторінок, що є у наборі даних. Цю інформацію можна використовувати для відображення таких даних, як загальна кількість сторінок у UI або 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
:
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>Проста пагінація</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body>
<div class="max-w-5xl mx-auto py-8">
<h1 class="text-5xl">Проста пагінація</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
не отримує загальну кількість записів, він не містить інформації про загальну кількість сторінок чи записів, лише про те, чи є наступна сторінка. Тому ми бачимо лише посилання "Попередня" та "Наступна" в пагінаційних посиланнях.
Метод 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
.
Метод simplePaginate
також опирається на значення limit
і offset
для отримання підмножини даних із бази. Однак він не виконує запит для отримання загальної кількості записів у наборі.
Значення offset
обчислюється так само, як і раніше:
Offset = Розмір сторінки * (Сторінка - 1)
Однак значення limit
розраховується трохи інакше, ніж у методі paginate
. Воно обчислюється так:
Limit = Розмір сторінки + 1
Це пов'язано з тим, що метод simplePaginate
потребує отримати один запис більше, ніж значення perPage
, щоб визначити, чи є ще записи для отримання. Припустимо, ми отримуємо 15 записів за сторінку. Значення limit
становитиме 16. Якщо нам буде повернено 16 записів, ми зрозуміємо, що доступна принаймні ще одна сторінка. Якщо повернеться менше ніж 16 записів, це означає, що ми на останній сторінці даних.
Отже, якщо ми хотіли б отримати першу сторінку користувачів (з 15 користувачами на сторінку), виконуються наступні SQL-запити:
select * from `users` limit 16 offset 0
Запит для другої сторінки виглядатиме так:
select * from `users` limit 16 offset 15
До цього часу ми розглянули методи paginate
і simplePaginate
, обидва з яких використовують пагінацію на основі зсуву. Тепер ми розглянемо метод cursorPaginate
, який використовує пагінацію на основі курсора.
Спочатку пагінація на основі курсора може здаватися дещо заплутаною, тому не турбуйтеся, якщо ви не зрозумієте це одразу. Сподіваюся, до кінця статті у вас буде краще розуміння, як це працює. Я також залишу чудове відео наприкінці статті, яке детально пояснює пагінацію на основі курсора.
Під час пагінації на основі зсуву ми використовуємо значення limit
і offset
для отримання підмножини даних з бази. Ми можемо сказати: "пропустити перші 10 записів та отримати наступні 10 записів". Це легко зрозуміти і реалізувати. У випадку курсорної пагінації ми використовуємо курсор (зазвичай унікальний ідентифікатор для конкретного запису в базі) як відправну точку для отримання попередніх/наступних наборів записів.
Наприклад, якщо ми виконуємо запит на отримання перших 15 користувачів, нехай ID 15-го користувача буде 20. Коли ми хочемо отримати наступних 15 користувачів, ми використаємо ID 15-го користувача (20) як курсор і скажемо: "отримати наступних 15 користувачів з ID більшим за 20".
Іноді ви можете бачити курсори, які називаються "токенами", "ключами", "наступними", "попередніми" тощо. Вони, по суті, є посиланнями на конкретний запис у базі даних. Ми розглянемо структуру курсорів пізніше в цій секції, коли аналізуватимемо основні SQL-запити.
Laravel дозволяє легко використовувати пагінацію на основі курсора за допомогою методу cursorPaginate
:
use App\Models\User;
$users = User::query()->cursorPaginate();
Запустивши цей код, ви отримаєте $users
як екземпляр Illuminate\Contracts\Pagination\CursorPaginator
, зазвичай це об'єкт Illuminate\Pagination\CursorPaginator
. Цей екземпляр пагінатора містить усю інформацію, що необхідна для відображення пагінованих даних у вашому додатку.
Як і в методі simplePaginate
, метод cursorPaginate
не отримує загальну кількість записів у наборі, а лише знає про поточну сторінку даних і чи є ще записи для отримання, тому ми не відразу знаємо загальну кількість сторінок або записів.
Розглянемо, як використовувати метод 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>Курсорна пагінація</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body>
<div class="max-w-5xl mx-auto py-8">
<h1 class="text-5xl">Курсорна пагінація</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
. Ми бачимо лише посилання "Попередня" та "Наступна".
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": "eyJ1c2Vycy5pZCI6MTUsIl9wb2ludHNUb05leHRJdGV0cyI6dHJ1ZX0",
"next_page_url": "http://example.com/users?cursor=eyJ1c2Vycy5pZCI6MTUsIl9wb2ludHNUb05leHRJdGV0cyI6dHJ1ZX0",
"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, що містить курсор для наступної сторінки даних. Якщо ми декодуємо це поле, ми отримаємо приблизно таке (для читабельності):
{
"users.id": 15,
"_pointsToNextItems": true
}
Курсор містить дві окремі частини інформації:
users.id
- ID останнього отриманого запису у наборі._pointsToNextItems
- логічне значення, яке вказує, чи курсор вказує на наступний чи попередній набір записів. Якщо значення true
, це означає, що курсор слід використовувати для отримання наступного набору записів з ID більше, ніж значення 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": "eyJ1c2Vycy5pZCI6MzAsIl9wb2ludHNUb05leHRJdGV0cyI6dHJ1ZX0",
"next_page_url": "http://example.com/users?cursor=eyJ1c2Vycy5pZCI6MzAsIl9wb2ludHNUb05leHRJdGV0cyI6dHJ1ZX0",
"prev_cursor": "eyJ1c2Vycy5pZCI6MTYsIl9wb2ludHNUb05leHRJdGV0cyI6ZmFsc2V9",
"prev_page_url": "http://example.com/users?cursor=eyJ1c2Vycy5pZCI6MTYsIl9wb2ludHNUb05leHRJdGV0cyI6ZmFsc2V9"
}
Ми бачимо, що поля prev_cursor
та prev_page_url
тепер заповнені, а next_cursor
та next_page_url
оновлені з курсором для наступної сторінки даних.
Щоб краще зрозуміти, як працює пагінація з курсором, давайте розглянемо основні SQL-запити, які виконуються під час використання методу cursorPaginate
.
На першій сторінці даних (що містить 15 записів) буде виконано наступний SQL-запит:
select * from `users` order by `users`.`id` asc limit 16
Ми бачимо, що ми отримуємо перші 16 записів з таблиці users
, упорядковані за стовпцем id
у порядку зростання. Подібно до методу simplePaginate
, ми отримуємо 16 рядків, оскільки ми хочемо визначити, чи є ще записи для отримання.
Уявімо, що ми потім переходимо на наступну сторінку елементів з наступним курсором:
eyJ1c2Vycy5pZCI6MTUsIl9wb2ludHNUb05leHRJdGV0cyI6dHJ1ZX0
Коли цей курсор декодується, ми отримуємо наступний 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 на попередній сторінці).
Тепер уявімо, що ID першого користувача на другій сторінці — це 16. Коли ми переходимо назад до першої сторінки даних з другої, ми будемо використовувати наступний курсор:
eyJ1c2Vycy5pZCI6MTYsIl9wb2ludHNUb05leHRJdGV0cyI6ZmFsc2V9
Коли це декодується, ми отримаємо наступний 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 на другій сторінці), а результати упорядковані у порядку спадання.
Дотепер у наших API прикладах ми просто повертали пагіновані дані безпосередньо з контролера. Однак у реальному додатку вам, ймовірно, знадобиться обробити дані перед їх поверненням користувачеві. Це може бути будь-що: від додавання або видалення полів, перетворення типів даних або навіть трансформації даних у зовсім інший формат. Ось чому ви, ймовірно, захочете використовувати API ресурси, оскільки вони дозволяють послідовно перетворювати ваші дані перед їх поверненням.
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": "« Попередня",
"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": "Далі »",
"active": false
}
],
"path": "http://example.com/users",
"per_page": 15,
"to": 15,
"total": 50
}
}
Як видно з JSON-наведень, Laravel визначає, що ми працюємо з пагінованим набором даних і повертає пагіновані дані в подібному форматі, як раніше. Однак цього разу користувачі в полі data
містять лише поля 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, давайте обговоримо, як вирішити, який із підходів використовувати у вашому додатку.
Якщо ви створюєте UI або API-ендпоінт, який вимагає відображати загальну кількість записів або сторінок, то метод paginate
буде логічним вибором.
Якщо ви не потребуєте жодної з цих функцій, тоді simplePaginate
або cursorPaginate
будуть більш ефективними, оскільки вони не виконують непотрібні запити для підрахунку загальної кількості записів.
Якщо вам потрібно мати можливість стрибати на конкретну сторінку даних, тоді пагінація на основі зсуву більше підходить. Оскільки курсорна пагінація є станом, вона покладається на попередню сторінку, щоб знати, куди йти далі. Тому стрибати на конкретну сторінку не так просто.
На відміну від цього, використовуючи пагінацію на основі зсуву, ви зазвичай просто передаєте номер сторінки в запиті (можливо, як параметр запиту) та стрибаєте на цю сторінку, не маючи жодного контексту попередньої сторінки.
Залежно від того, як бази даних обробляють значення 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 було б ID Користувача 5 (припустимо, це 5), оскільки це був останній запис на першій сторінці. Тому наш запит для сторінки 2 виглядатиме так:
select * from `users` where (`users`.`id` > 5) order by `users`.`id` asc limit 6
Виконання вказаного запиту поверне користувачів 6 до 10, як і очікувалося.
Це має підкреслити, як пагінація на основі зсуву може стати проблемою в разі зміни основних даних під час їх зчитування. Це стає менш передбачуваним і може призвести до непередбачуваних результатів.
Важливо пам'ятати, що ви не зобов'язані використовувати один тип пагінації у вашому додатку. У деяких місцях пагінація на основі зсуву може бути більш доречною (можливо, для UI), а в інших — пагінація на основі курсора буде ефективнішою (наприклад, при роботі з великим набором даних). Тому ви можете комбінувати різні методи пагінації у своєму додатку залежно від конкретного випадку.
Однак, якщо ви створюєте API, я б настійно радив використовувати одну й ту ж саму концепцію пагінації для всіх ваших ендпоінтів. Це полегшить розробникам зрозуміти, як користуватися вашим API, і уникнути плутанини.
Не хочете, щоб розробникам доводилося пам'ятати, які ендпоінти використовують пагінацію на основі зсуву, а які — пагінацію на основі курсора.
Звісно, це не суворе правило. Якщо вам дійсно потрібно застосувати інший метод пагінації в одному конкретному ендпоінті, будь ласка. Але просто переконайтеся, що це чітко вказано в документації, щоб полегшити розуміння для розробників.
Якщо ви більше вважаєте себе візуальним учнем, можливо, вам захочеться переглянути це чудове відео від Аарона Франсіса, яке детально пояснює різницю між пагінацією на основі зсуву та курсора:
У цій статті ми розглянули різні методи пагінації в Laravel та їх використання. Також ми проаналізували основні SQL-запити та розглянули, як вирішити, який метод пагінації вибрати у вашому додатку.
Сподіваюся, ви відчуєте себе більш впевненими у використанні пагінації у ваших Laravel-додатках.