Empat Baris Bernama Sama: Kunci Import yang Tidak Boleh Menebak
Kunci idempoten yang ikut memuat nama_panjang, supaya empat cabang FASI tidak melebur jadi satu baris.
Ringkasan
Saya menulis CLI kecil untuk memindahkan data. Kunci import awalnya hanya nama dan kategori. Kemudian saya tambahkan nama panjang untuk menghindari duplikat.
Saya menulis CLI kecil untuk memindahkan data struktural cabor dari file intake ke dua tabel, organizations dan organization_members. Identitas tiap baris kelihatan sederhana: nama cabor plus kategori, tiga kategori berbeda dengan total 54 + 2 + 5 baris (angka dari commit 7628acc). Yang tidak kelihatan di laporan jumlah itu: empat baris di file intake semuanya bernama "FASI", dan yang membedakan mereka hanya teks di dalam kurung.
Tebakan pertama saya jatuh ke kunci yang paling nyaman. nama + category_id sudah cukup, begitu bunyi komentar di kepala paketnya, dan memang kunci itulah yang saya pertimbangkan pertama kali karena paling mudah ditulis dan paling mudah dibaca. Kueri pendek, satu SELECT, selesai. Kalau sebuah nama sudah ada, bukankah barisnya berarti sudah pernah masuk?
Kunci yang Melebur
Masalahnya baru kelihatan ketika isi kurungnya dibuka. Keempat cabang FASI itu empat organisasi terpisah dengan nama panjang berbeda-beda; nama pendeknya sama karena induknya memang satu. Kalau kunci import hanya nama, run kedua akan melewatkan tiga cabang pertama dan menyisakan satu baris, lalu saya akan melihat "skipped (already exists)" dan mengira semuanya sudah benar. Tidak ada error, tidak ada duplikat, hanya tiga organisasi yang tidak pernah tersimpan. Itu bentuk kehilangan data yang paling sulit dikenali: ia terlihat seperti tidak terjadi apa-apa.
Di sisi skema, MySQL tidak menolak. Tabel organizations hanya punya primary key, foreign key ke kategori, dan dua index biasa, satu untuk kolom category_id dan satu lagi untuk kolom status, tidak ada UNIQUE yang menangkap kombinasi nama dan kategori. Jadi yang mengandalkan constraint untuk menahan duplikat tidak akan menemukannya. MySQL menyusun index dari gabungan kolom yang kita tentukan, dan gabungan itu yang dipakai sebagai nilai index-nya [2], dengan index yang boleh mencapai 16 kolom [3]. Karena tidak ada index uniknya, penjaga duplikatnya pindah ke kode. Kuncinya jadi tiga kolom:
SELECT COUNT(*) FROM organizations
WHERE nama = ? AND nama_panjang <=> ? AND category_id = ?
nama_panjang ikut masuk supaya empat cabang FASI tidak melebur jadi satu baris (commit 7628acc). Prinsipnya sama dengan yang dipakai sistem pesan untuk menahan duplikat: kunci harus mengidentifikasi secara unik dan konsisten unit kerja yang sama di setiap pengulangan [1]. Nama sama berarti unit kerja sama hanya kalau memang unit kerjanya satu.
NULL yang Diam
Kolom nama_panjang nullable, dan kebanyakan baris memang tidak mengisinya. Operator = berbahaya di sini: membandingkan NULL dengan tanda sama dengan tidak menghasilkan true, tapi NULL, jadi baris yang seharusnya cocok dianggap belum ada dan diinsert lagi. Operator <=> memperlakukan NULL sebagai nilai yang bisa dibandingkan, sehingga kolom kosong di kedua sisi tetap dianggap sama.
Alternatif yang lebih singkat memang ada: INSERT ... ON DUPLICATE KEY UPDATE. Tapi MySQL menjalankan itu hanya kalau baris baru bentrok dengan UNIQUE index atau primary key [4], dan kombinasi itu tidak punya index unik. Tanpa perubahan skema, jalurnya tetap cek dulu baru insert, pola yang harus disiplin karena tidak ada yang menegakkannya di database.
Guard Sebelum Koneksi
Satu baris paling atas main() menolak dijalankan kecuali APP_ENV persis development, sebelum sql.Open dipanggil sama sekali:
if os.Getenv("APP_ENV") != "development" {
fatal("guard APP_ENV: hanya boleh dijalankan dengan APP_ENV=development")
}
Env var sebagai saklar dipisahkan dari kode supaya nilai yang berbeda antar-deploy tidak ikut ter-bake ke dalam biner [5], dan karena itu satu biner yang sama bisa ditaruh di mana pun tanpa perlu dikompilasi ulang. Untuk CLI satu-file yang menulis puluhan baris ke tabel produksi, guard ini satu-satunya rem; tanpanya, satu lupa menyetel env sudah cukup.
Yang paling berguna justru temuan saat membaca ulang: komentar di kepala paket masih bilang idempoten berdasarkan nama + category_id, padahal kodenya sudah tiga kolom. Komentar itu tertinggal dari perbaikannya sendiri. Saya memperbaikinya, lalu berhenti menganggap komentar sebagai bukti: yang berlaku adalah query yang benar-benar dijalankan.
Sumber: