Kategori Hapus Lunas: Jebakan State Machine di Layer Database
Menghapus kategori ternyata bukan sekadar ganti DELETE jadi UPDATE. Predikat, RowsAffected, scope guard restore, dan cache ikut jadi tanggung jawab.
Ringkasan
Kirain hapus kategori tinggal ganti DELETE jadi UPDATE doang ternyata ribet banget. Semua query lama harus ditambahin filter deleted_at IS NULL biar sampah nggak bocor ke dropdown atau list. Buat delete sama restore harus cek RowsAffected biar bisa kasih 404 dan jangan lupa flush cache biar datanya langsung muncul lagi.
Pas pertama kali ngetes endpoint tong sampah di project portal komunitas saya, saya yakin banget kodenya udah bener. Saya kirim request DELETE, cek database, kolom waktu penghapusan udah keisi timestamp. Aman. Tapi waktu saya panggil endpoint list kategori, data yang barusan saya buang itu masih nongol di response JSON.
Tebakan awal saya, ini cuma masalah caching atau bug kecil di serializer. Saya pikir implementasi kategori hapus lunas itu sesederhana ganti perintah SQL dari DELETE jadi UPDATE. Ternyata saya salah besar.
Masalah aslinya ada di cara kerja database itu sendiri. Database nggak tau mana data aktif dan mana data sampah. Konsep ini cuma state machine yang kita paksa ke sistem. Kalau kita nggak secara eksplisit nyaring data di setiap query, database bakal ngasih semua yang dia punya [1]. Di artikel sebelumnya yang bahas kontrak OpenAPI, saya udah nyiapin struktur response-nya. Contract-first development emang enak buat nentuin bentuk JSON-nya. Tapi waktu masuk ke lapisan implementasi database, realitanya jauh lebih rumit. Kontrak API cuma janji soal bentuk data, nggak ngatur gimana caranya nyaring data itu dari tabel yang isinya campur aduk antara data aktif dan sampah.
Predikat yang Wajib Menempel
Bagian paling memakan waktu dari commit ini bukan nulis fungsi delete yang baru. Justru ngubah query-query lama yang udah ada dan berjalan stabil.
Fungsi pengecekan eksistensi data, fungsi update, sampai query dropdown, semuanya harus dipaksa "setuju" sama status mesin ini. Saya harus nambahin predikat deleted_at IS NULL ke setiap klausa WHERE. Kalo ada satu aja yang kelupaan, data yang harusnya tersembunyi bakal bocor ke frontend [1][2]. Bayangin kalo user lagi milih kategori buat artikel baru, terus kategori yang udah mereka buang kemarin malah muncul lagi di dropdown.
Ini juga bikin jaminan foreign key dan unique constraint di database jadi agak melemah [2]. Kita nggak bisa lagi ngandelin database buat nolak duplikasi nama kategori yang udah dibuang, karena secara teknis baris itu masih ada di tabel. Semuanya sekarang jadi tanggung jawab logika aplikasi. Kalo ada user bikin kategori baru dengan nama yang sama persis kayak kategori yang ada di tong sampah, aplikasi harus ngecek manual dulu sebelum nge-throw error.
UPDATE categories
SET deleted_at = NOW()
WHERE id = ? AND deleted_at IS NULL;
RowsAffected sebagai Alarm 404
Terus gimana caranya tau kalo user nyoba ngapus kategori yang emang nggak ada, atau kategori yang udah ada di tong sampah?
Kalo pakai perintah DELETE biasa, MySQL bakal langsung ngasih tau berapa banyak baris yang beneran terhapus [4]. Tapi karena kita pakai UPDATE, kita butuh cara lain buat mastiin operasinya sukses. Di Go, satu-satunya cara resmi buat tau berapa baris yang kena dampak adalah lewat Result.RowsAffected() [3].
result, err := db.Exec(query, id)
if err != nil {
return err
}
rows, _ := result.RowsAffected()
if rows == 0 {
return ErrCategoryNotFound
}
Kalo nilainya nol, artinya nggak ada baris aktif yang di-update. Aplikasi langsung lempar error 404. Ini jadi alarm yang ngebedain antara "data nggak ada" dan "data udah ada di tong sampah". Tanpa cek ini, user bakal mikir operasinya sukses padahal mereka cuma nge-update baris yang udah mati.
Filter List dan Scope Guard Restore
Buat nampilin isi tong sampah, saya nambahin parameter boolean di fungsi list. Kalo parameternya true, query-nya berubah cari yang deleted_at IS NOT NULL. Ini kedengeran gampang, tapi interaksi sama modul lain lumayan bikin pusing. Waktu nge-list kategori berdasarkan modul, filter boolean ini harus digabung sama kondisi modul. Query-nya jadi panjang banget dan rawan salah taruh kurung. Saya sempet kena bug di mana tong sampah malah nampilin kategori dari modul yang salah, cuma gara-gara urutan operator AND dan OR-nya ketuker.
Fungsi restore juga punya jebakannya sendiri. Kita nggak bisa asal update kolom waktu jadi NULL. Kita butuh scope guard cerminan. Fungsi pencarian standar cuma mau baca data aktif. Jadi saya bikin guard terpisah khusus buat ngebaca view tong sampah. Fungsinya ngecek dulu apakah kategori itu emang lagi dalam status terhapus sebelum di-restore.
UPDATE categories
SET deleted_at = NULL
WHERE id = ? AND deleted_at IS NOT NULL;
Satu hal lagi yang sering kelewat: busting cache. Setelah data berhasil di-restore, cache publik wajib dibuang. Cache busting ini penting banget khususnya buat kategori. Soalnya kategori itu biasanya di-cache agresif di layer CDN atau Redis. Kalo kita restore kategori tapi cache-nya nggak di-flush, halaman yang tadinya kehilangan kategori itu bakal tetep nampilin data lama di mata user. Mereka harus nunggu cache kadaluarsa buat lihat perubahannya.
Implementasi fitur ini emang bikin kode jadi lebih berisik. Tapi ini trade-off yang masuk akal biar kita punya kontrol penuh atas riwayat data tanpa harus ngorbanin integritas referensial.