Skip to content

Filter bidang tanpa menelan baris global

Adityo Guni Waluyo

Nambah parameter ?bidang= di endpoint publik bukan cuma soal satu klausa WHERE. Baris global harus tetap tayang di semua scope.

Ringkasan

Awalnya filter bidang cuma pakai WHERE biasa jadi konten global yang NULL malah kelewat nggak muncul. Akhirnya querynya dibenerin jadi ikut nampilin yang NULL sama ALL terus ditambah validasi 400 kalau bidangnya ngaco. Sekalian cache key ditambahin bidang dan teks intro pilar dipindah ke tabel settings biar gampang diatur.

Pas mulai nulis endpoint agenda buat halaman pilar kepemudaan, versi pertama yang saya tulis sederhana banget: ambil query parameter bidang, tempel satu klausa WHERE bidang = ?, beres. List-nya muncul, tapi ada yang aneh. Beberapa pengumuman yang jelas-jelas disiapkan buat tampil di semua pilar nggak ikut naik.

Tebakan pertama saya, datanya emang belum di-seed. Setelah ngecek migrasi dan isi tabel, baru ketahuan: kolom bidang di baris-baris itu NULL, dan itu bukan kekosongan yang nggak sengaja. Migration-nya bahkan ngasih komentar eksplisit: NULL = konten global. Baris tanpa scope memang dirancang buat ikut muncul di semua pilar, dan ada juga sentinel ALL buat kebutuhan yang sama tapi nilainya jelas.

Di MySQL, NULL itu konsepnya "a missing unknown value" [1], bukan string kosong dan bukan nol. Perbandingan apa pun yang menyentuh NULL hasilnya bukan true, jadi WHERE bidang = 'kepemudaan' secara matematis emang nggak akan pernah mengangkap baris NULL. Bukan bug, tapi cara NULL bekerja. Jadinya jelas: filter yang benar bukan cuma "cocokin scope", tapi juga neputusin baris mana yang wajib selalu lolos.

Gerbang 400 di handler

Sebelum nyentuh query, saya pasang gerbang dulu di handler. Nilai bidang cuma boleh kosong (tanpa filter) atau salah satu dari pariwisata, olahraga, budaya, kepemudaan. Selain itu, langsung 400 dengan kode INVALID_BIDANG, lengkap sama daftar nilai yang boleh di pesan error-nya:

bidang := q.Get("bidang")
switch bidang {
case "", scope.Pariwisata, scope.Olahraga, scope.Budaya, scope.Kepemudaan:
default:
    response.Error(w, http.StatusBadRequest, "INVALID_BIDANG",
        "unknown bidang; allowed: pariwisata, olahraga, budaya, kepemudaan")
    return
}

Kenapa nggak diam-diam abaikan aja nilai anehnya? Karena spec HTTP ngedefinisikan 400 buat request yang server nggak proses karena kesalahan klien [2], dan klien yang nerima 400 harus mengharapkan request yang sama bakal gagal terus selama nggak diubah [3]. Jauh lebih berguna daripada daftar kosong yang bikin frontend nebak-nebak, atau 500 yang muncul belakangan.

Predikat yang bikin baris global ikut

Jantung perubahan ini ada di repository. Versi naifnya cuma nambah AND bidang = ?, dan itu persis yang menelan baris global tadi. Versi finalnya gini:

if q.Bidang != "" {
    where += " AND (bidang = ? OR bidang IS NULL OR bidang = 'ALL')"
    args = append(args, q.Bidang)
}

Tiga cabang di dalam kurung itu kontraknya: baris milik scope yang diminta, baris global (NULL), dan baris sentinel ALL, semuanya tetap tayang di scope mana pun. Di sisi Go, kolom ini discan ke sql.NullString yang punya flag Valid; false berarti NULL beneran, bukan string kosong [4]. Satu baris komentar di model aja udah cukup buat ngejelasin kontraknya: NULL = konten global.

Cache key sama konten yang pindah ke CMS

Dua hal lain ikut ke-generate di commit ini. Pertama, cache key daftar upcoming nambah segmen :bidang:. Tanpa itu, permintaan scope olahraga bisa baca cache milik pariwisata karena key-nya kembar. Aturan mainnya sederhana: tiap dimensi query yang ngubah isi respons harus nongol di cache key, dan ini fan-out kedua setelah ?module= di endpoint sebelumnya.

Kedua, teks intro tiga pilar pindah dari const di halaman frontend ke tabel settings lewat migrasi 000062, group bidang, key bidang_intro_* dan bidang_program_*. Isinya di-seed verbatim dari teks lama, program sengaja kosong sampai katalognya siap, dan section-nya disembunyikan selama belum diisi admin. Ada satu detail yang kelihatan aneh tapi disengaja: halaman kebudayaan dipetakan ke scope budaya sesuai enum database, sementara prefix settings-nya tetap kebudayaan. Dua namespace, dua sejarah, dan mempertahankannya jauh lebih murah daripada ngerename setengah sistem demi estetika.

Pengalaman dari commit ini nyisain satu pegangan. Menyaring yang nggak boleh muncul itu bagian gampangnya; bagian susahnya ngeputusin apa yang wajib selalu muncul. Saya makin condong ke gaya gagal cepat kayak gerbang 400 di atas, daripada filter yang diam-diam ngasilin daftar kosong. Scope baru nanti tinggal nambah satu case di switch, sisanya udah diurusi predikat.

Sumber

Artikel terkait