โ† Semua pembelajaran / Laravel Nol โ†’ Enterprise
Fase 5 ยท API & Frontend

Paginasi

Paginasi terlihat seperti detail sepele sampai tabelmu berisi jutaan baris. Saat itu, pilihan antara offset dan cursor menentukan apakah halaman terakhir bisa dibuka sama sekali.

Sumber asli laravel.com Resmi Rangkuman ~6 menit baca

Intisari

  • paginate() menjalankan dua query: data, plus COUNT(*) untuk total.
  • simplePaginate() membuang COUNT โ€” hanya "sebelumnya/berikutnya", jauh lebih murah.
  • cursorPaginate() memakai WHERE pada kolom terurut. Biayanya tetap di halaman mana pun.
  • OFFSET besar itu lambat: database tetap membaca lalu membuang semua baris sebelumnya.
  • Selalu ->withQueryString() supaya filter tidak hilang saat pindah halaman.

Tiga bentuk

// 1. Lengkap โ€” ada nomor halaman dan total
$artikel = Artikel::latest()->paginate(20)->withQueryString();

// 2. Sederhana โ€” hanya sebelumnya/berikutnya, tanpa COUNT
$artikel = Artikel::latest()->simplePaginate(20);

// 3. Cursor โ€” untuk daftar panjang dan umpan tanpa batas
$artikel = Artikel::latest('id')->cursorPaginate(20);
paginatesimplePaginatecursorPaginate
Query per halaman211
Tahu jumlah totalYaTidakTidak
Lompat ke halaman 500BisaTidakTidak
Biaya di halaman jauhNaik terusNaik terusTetap
Tahan terhadap data baru masukTidak โ€” baris bisa terlewat atau dobelTidakYa
Cocok untukTabel adminDaftar panjang biasaUmpan, API, infinite scroll

Kenapa OFFSET jadi mahal

-- Halaman 1: cepat
SELECT * FROM artikel ORDER BY id DESC LIMIT 20 OFFSET 0;

-- Halaman 5.000: database membaca 100.000 baris lalu MEMBUANG 99.980 di antaranya
SELECT * FROM artikel ORDER BY id DESC LIMIT 20 OFFSET 99980;
-- cursorPaginate: selalu membaca 20 baris, di halaman mana pun
SELECT * FROM artikel WHERE id < 883421 ORDER BY id DESC LIMIT 20;

Dan COUNT(*) yang sering lebih mahal daripada datanya. Pada tabel besar dengan filter, menghitung total bisa memindai jauh lebih banyak baris daripada mengambil dua puluh baris pertamanya. Pertanyaan yang layak diajukan: apakah pengguna benar-benar butuh angka "1.847.203 hasil"? Kalau tidak, simplePaginate() menghapus separuh biaya halaman itu dalam satu perubahan kata.

Masalah yang cuma dipecahkan cursor

Pengguna membuka halaman 1 dari daftar artikel terbaru. Sepuluh detik kemudian, tiga artikel baru terbit. Pengguna menekan "halaman 2" โ€” dan tiga artikel yang tadi ada di bawah halaman 1 kini tergeser ke halaman 2, sehingga dilihat dua kali, sementara tiga lainnya terlewat.

Dengan cursor, halaman berikutnya berarti "yang lebih lama daripada baris terakhir yang sudah kamu lihat". Data baru yang masuk di atas tidak menggeser apa pun. Untuk umpan berita, ini bukan penyempurnaan โ€” ini kebenaran.

Menampilkan

@foreach ($artikel as $a)
    <article>{{ $a->judul }}</article>
@endforeach

{{ $artikel->links() }}
{{ $artikel->onEachSide(1)->links() }}      {{-- lebih sedikit nomor halaman --}}
php artisan vendor:publish --tag=laravel-pagination    # untuk mengubah tampilannya

withQueryString() hampir selalu perlu. Tanpa itu, tautan halaman 2 kehilangan seluruh parameter pencarian dan filter โ€” pengguna yang menyaring lalu pindah halaman tiba-tiba melihat seluruh katalog. Ini bug yang sangat sering lolos ke produksi karena hanya muncul saat filter dipakai bersama paginasi.

Paginasi di API

return ArtikelResource::collection(
    Artikel::with('penulis')->latest('id')->cursorPaginate(20)
);
{
  "data": [ โ€ฆ ],
  "links": { "next": "https://โ€ฆ/api/artikel?cursor=eyJpZCI6ODgzNDIxfQ" },
  "meta":  { "per_page": 20, "next_cursor": "eyJpZCI6ODgzNDIxfQ" }
}

Cursor punya syarat: kolom urutannya harus unik dan stabil. Mengurutkan berdasarkan created_at saja berisiko kalau ada dua baris dengan detik yang sama โ€” barisnya bisa terlewat. Pakai ->orderBy('terbit_pada', 'desc')->orderBy('id', 'desc') supaya urutannya selalu punya pemecah seri, dan pastikan ada index gabungan untuk kedua kolom itu.

Pengaman untuk parameter dari pengguna

$perHalaman = min($request->integer('per_page', 20), 100);   // batasi

$artikel = Artikel::latest()->paginate($perHalaman);

Tanpa batas atas, siapa pun bisa meminta ?per_page=100000 dan memaksa servermu membangun seratus ribu objek Eloquent dalam satu permintaan. Ini vektor penolakan layanan yang murah bagi penyerang dan mahal bagimu.

Latihan: buat 100.000 baris dengan factory, lalu ukur waktu paginate() di halaman 1 dan di halaman 4.000. Bandingkan dengan cursorPaginate() pada kedalaman yang setara. Simpan angkanya โ€” ini argumen paling meyakinkan saat suatu hari kamu perlu menjelaskan pilihan ini kepada orang lain.

Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.