Rekonsiliasi Data Impor: Entitas yang Hilang dari Sumber
Database masih 65, sumber tinggal 63. Penghapusan lewat importer yang sama, kunci idempotensi pindah ke nomor baris sumber.
Ringkasan
Database punya 65 entitas padahal JSON sumber cuma 63, dua dobel. Alih-alih hapus manual via SQL, penghapusan dijadiin bagian importer biar ada counter sama test, dan media ikut kehapus lewat CASCADE. Kunci idempotensi pindah dari slug ke nomor sumber, jadi run ulang aman, nol insert nol hapus.
Angka 65 di database, 63 baris di berkas JSON revisi. Itu pemandangan pertama ketika revisi data sanggar tiba dari klien. Dua entri hilang di sumber, nomor 27 duplikat dan nomor 40. Refleks pertama: buka konsol SQL, tulis dua pernyataan penghapusan, selesai dalam satu menit.
Pernyataan itu tidak pernah ditulis. Bukan karena malas, melainkan karena jalur itu tidak meninggalkan apa pun yang bisa diperiksa ulang. Penghapusan manual tidak tercatat di penghitung apa pun, tidak ikut teruji, dan kalau suatu hari jalur impor dijalankan ulang, dua entitas itu bisa muncul kembali tanpa seorang pun menyadari.
Rekonsiliasi Sebagai Bagian Importer
Keputusan yang diambil: penghapusan menjadi bagian dari kontrak importer. Entitas dalam kategori yang nilai attributes.no, nomor baris sumbernya, absen dari JSON sumber dihapus oleh eksekusi impor yang sama. Sebuah penghitung baru EntitiesDeleted mencatat berapa yang terhapus. Baris tanpa penanda no sama sekali tidak disentuh, sehingga entitas buatan manual tetap aman.
Bentuk sederhananya kira-kira begini:
package main
import (
"fmt"
"os"
)
// Reconcile: hapus entitas kategori yang attributes.no absen dari JSON sumber.
func (im *Importer) ReconcileSanggar(catID int64, present map[int]bool, dryRun bool) {
ids, err := im.sanggarOrphans(catID, present)
if err != nil {
fmt.Fprintln(os.Stderr, "reconcile:", err)
os.Exit(1)
}
for _, id := range ids {
if !dryRun {
if _, err := im.db.Exec(`DELETE FROM entities WHERE id = ?`, id); err != nil {
fmt.Fprintln(os.Stderr, "hapus entitas:", err)
os.Exit(1)
}
}
im.result.EntitiesDeleted++
}
}
Media yang menempel pada entitas terhapus ikut melalui ON DELETE CASCADE. Dokumentasi PostgreSQL menjelaskan bahwa CASCADE cocok untuk komponen yang tidak dapat berdiri sendiri, dan RESTRICT untuk dua objek yang independen [2]. Foto milik entitas memang komponen, bukan objek mandiri, jadi FK media memakai CASCADE. Praktik pembersihan database umumnya menemukan baris yatim justru karena tidak ada FK yang menolak hapusan; pola amannya adalah gladi bersih: jalankan dalam transaksi, bandingkan hitungan, batalkan bila tidak cocok [4]. Dry-run importer mengambil pola yang sama, kini ikut memprediksi jumlah skip dan hapus tanpa menulis apa pun.
Kunci Idempotensi Bergeser dari Slug ke Nomor Sumber
Perubahan yang paling tidak terduga justru soal kunci idempotensi. Importer lama melewati baris yang sudah ada dengan mencocokkan slug. Masalahnya muncul setelah rekonsiliasi: ketika duplikat dihapus, slug alaminya menjadi bebas. Eksekusi berikutnya yang masih mencocokkan slug bisa menyisipkan ulang baris melalui slug kosong itu. Karena itu pemeriksaan skip pindah ke nomor sumber, yang keberadaannya tidak terpengaruh oleh penghapusan mana pun. Run kedua setelah rekonsiliasi membuktikan: 0 insert, 0 skip baru, 0 hapus, murni idempoten.
Benchmark ORM ODB memberi catatan performa yang sejalan: CASCADE lebih cepat daripada rantai pernyataan DELETE eksplisit di semua mesin yang diuji kecuali SQLite, karena overhead per pernyataan lewat jaringan mendominasi [5]. Dokumentasi MySQL menambahkan dua detail yang relevan: aksi kaskade tidak memicu trigger, dan SET NULL menuntut kolom anak boleh NULL [6]. Detail kedua penting ketika memilih antara menghapus media sekaligus atau hanya melepas tautannya.
Angka yang Tertinggal dari Eksekusi
Verifikasi berjalan dengan angka yang pasti. Seed 65 entitas, satu run rekonsiliasi: 0 insert, 63 skip, 2 hapus, media yatim nol. Run berikutnya: 0, 0, 0. Catatan di papan kerja menambah satu temuan yang menggelitik: berkas revisi ternyata sudah 63 baris sejak awal, yang menyimpang adalah database lokal yang masih menyimpan 65 dari run lama. Rekonsiliasi pada dasarnya menyelaraskan database ke sumber, bukan sebaliknya.
Bedanya dengan dua pernyataan SQL manual terletak pada apa yang tertinggal setelah eksekusi. Pernyataan manual meninggalkan keputusan yang sudah lewat. Jalur importer meninggalkan penghitung, test integrasi yang memverifikasi 0/63/2 lalu 0/0/0, dan jaminan skema bahwa media tidak pernah jadi yatim. Ketika sumber berikutnya berubah lagi, jalur yang sama tinggal dijalankan ulang.