SupportWriter space ↗
← Back to the journal
Laravel

Cursor Pagination di Laravel untuk Feed Audit Berskala Besar

Bangun feed audit dengan urutan deterministik, index gabungan, dan cursor yang tetap membawa filter.

DAdministrator · 13 Oct 2026 · 1 min read

This article is not yet available in your selected language. Showing the original.

Dashboard audit memiliki jutaan baris. Pada halaman jauh, pagination berbasis offset harus melewati banyak baris sebelum mengembalikan 50 record. Feed audit biasanya hanya membutuhkan tombol berikutnya dan sebelumnya, sehingga cursor pagination lebih sesuai daripada nomor halaman.

Contoh Laravel 13 ini memakai tabel audit_events dengan id, team_id, action, dan occurred_at. occurred_at wajib non-null dan dianggap tidak berubah setelah event dibuat. Cursor mencari posisi berdasarkan kolom pengurutan, bukan menghitung berapa baris harus dilewati.

Tentukan scope dan urutan

Pengguna hanya boleh melihat event tim yang menjadi anggotanya. Route contoh berada di balik middleware autentikasi. Model Team memiliki relasi members() ke pengguna, dan AuditEvent adalah model event audit aplikasi. Validasi filter sebelum membuat query, serta ambil ID tim dari route yang sudah diotorisasi.

<?php

namespace App\Http\Controllers;

use App\Models\AuditEvent;
use App\Models\Team;
use Illuminate\Http\Request;
use Illuminate\View\View;

class AuditFeedController extends Controller
{
    public function __invoke(Request $request, Team $team): View
    {
        abort_unless(
            $team->members()->whereKey($request->user()->id)->exists(),
            403,
        );

        $filters = $request->validate([
            'action' => ['nullable', 'in:created,updated,deleted'],
        ]);

        $events = AuditEvent::query()
            ->where('team_id', $team->id)
            ->when($filters['action'] ?? null, function ($query, string $action): void {
                $query->where('action', $action);
            })
            ->orderByDesc('occurred_at')
            ->orderByDesc('id')
            ->cursorPaginate(50, ['id', 'action', 'occurred_at'])
            ->withQueryString();

        return view('audit.index', compact('events', 'team'));
    }
}

View mengulang $events dan menampilkan {{ $events->links() }}. Jika menggunakan named route dengan tim, buat tautan awal melalui route('audit.index', $team). Setelah filter berubah, kembali ke tautan awal tanpa parameter cursor; cursor dari filter lama tidak mewakili posisi dalam hasil baru.

Timestamp membutuhkan tie-breaker

Banyak event bisa memiliki timestamp identik. Pengurutan hanya dengan occurred_at membuat posisi di dalam kelompok tersebut tidak unik. Penambahan id menghasilkan pasangan unik dan membuat cursor dapat membedakan setiap event. Kedua kolom harus ada pada select, seperti pada contoh.

Untuk urutan menurun, query berikutnya secara konsep mencari timestamp yang lebih kecil, atau timestamp sama dengan ID lebih kecil. Mesin database dapat memanfaatkan index untuk melanjutkan dari batas itu. Nilai null pada kolom pengurutan tidak didukung oleh cursor pagination Laravel, sehingga constraint non-null menjadi bagian dari kontrak data.

Index mengikuti filter yang sering dipakai

Tambahkan index team_id,occurred_at,id untuk feed seluruh aksi pada satu tim. Jika filter aksi sering dipakai, pertimbangkan index tambahan team_id,action,occurred_at,id. Buat melalui migration, lalu periksa execution plan di database uji berisi data representatif. Jangan otomatis menambahkan seluruh kombinasi index karena masing-masing menambah biaya penyimpanan dan insert.

Cursor menghindari query total halaman, tetapi bukan jaminan semua query menjadi cepat. Filter yang tidak memiliki index atau sort dengan ekspresi mahal tetap dapat mendominasi waktu respons. Ukur waktu query dan jumlah baris yang diperiksa sebelum serta sesudah perubahan.

Feed bergerak bukan snapshot

Event baru yang ditambahkan di bagian atas biasanya tidak menggeser batas halaman berikutnya seperti offset. Namun mengubah occurred_at pada event lama, menghapus event, atau menambahkan event dengan waktu historis dapat membuat pengalaman navigasi berubah. Cursor tidak membekukan seluruh dataset.

Jika ekspor audit harus merepresentasikan keadaan tertentu, tentukan batas snapshot terpisah, misalnya maksimal ID saat proses dimulai untuk tabel append-only, dan pertahankan batas itu di setiap query. Parameter cursor hanya alat navigasi; otorisasi tim tetap wajib diperiksa pada setiap request.

Skenario pengujian

Buat lebih dari 50 event dengan timestamp sama. Ikuti next cursor dan pastikan tidak ada ID berulang atau terlewat pada dataset tetap. Uji filter aksi, pengguna tim lain, cursor yang dibawa ke filter baru, serta event baru yang masuk setelah halaman pertama dibaca. UI sebaiknya menjelaskan bahwa feed tidak menyediakan lompatan langsung ke halaman 100.

Rujukan API: Cursor pagination.

Comments 0

Share your questions, experiences, or feedback about this article.

No comments yet. Be the first to share your feedback.

← Explore more notes