Atribut Entitas Extensible: Kolom TEXT Berisi JSON
Menambahkan kolom attributes TEXT berisi JSON untuk atribut dinamis per modul, dan validasi pindah ke aplikasi.
Ringkasan
Daripada nambah kolom yang ujungnya banyak NULL, mending bikin satu kolom attributes tipe TEXT buat nampung JSON fleksibel. Soalnya di MySQL lama kolom TEXT ini nggak ngecek validasi JSON sama sekali, jadi harus divalidasi manual di aplikasi biar nggak ada data ngaco. Buat sekarang ini solusi paling simpel dan rapi, kalau nanti butuh filter serius baru dipromosi jadi kolom beneran.
Laporan modul wisata masuk: butuh kolom luas_lahan. Belum sempat beres, modul sanggar minta daftar atribut lain yang bentuknya beda sendiri. Kalau tiap permintaan saya jawab dengan migration tambah kolom, tabel entities bakal jadi kebun kolom yang setengahnya nilainya NULL dan nggak pernah kepakai lintas modul.
Tebakan Saya Soal JSON-TEXT Salah
Migrasi 000057 yang saya tulis menambahkan entities.attributes bertipe TEXT NULL. Di komentar migrasi saya menyebutnya pola JSON-TEXT, dan nama itu berhasil menipu diri sendiri. Saya sempat mengira kolom tersebut bakal divalidasi MySQL kayak tipe JSON asli: dokumen rusak otomatis ditolak.
Ternyata nggak. Ini TEXT biasa, dan bedanya bukan perkara teknis kecil. Database nggak peduli isinya JSON valid atau kurung kurawal acak, yang penting muat. Produksi kita memang masih MySQL 5.7, jadi sebelum ngecek dokumentasi saya nggak sadar bedanya sejauh apa. Tipe JSON asli di 5.7 itu beda cerita dan penting dibedakan: dokumentasinya menyebut validasi otomatis, dokumen invalid langsung kena error dari database [1].
Konsekuensi kedua dari teksturnya: ini bukan tipe JSON, jadi pembatasan index milik tipe JSON pun nggak nyambung ke kolom kita. Sekalian jadi catatan kalau nanti pindah ke tipe JSON asli: kolom JSON itu nggak bisa diindex langsung; butuh index berarti bikin generated column yang ngekstrak nilai scalar dari dokumennya [1].
Validasi Pindah ke Aplikasi
Karena database lepas tangan, validasi harus hidup di aplikasi. Di CMS, panel Atribut Tambahan nerima pasangan label dan nilai. Dua aturannya tegas: pasangan setengah jadi ditolak, pesannya apa adanya, label dan nilai atribut harus diisi bersama; label duplikat juga ditolak sebelum payload dikirim. Salah satu saja lolos, nanti bagian publik nerima data bocor yang bentuknya nggak jelas.
Sisanya mekanis. Nilai yang bukan string di-stringify biar konsisten, baris kosong dibuang, terus sisanya dirakit pakai Object.fromEntries() [2]. Fungsi ini ngebantu banget: array pasangan key-value berubah jadi object JavaScript dalam satu baris, tanpa loop manual yang rawan typo.
Pola Keempat, Bukan Pola Baru
Di backend Go, perubahannya minim. DTO nerima json.RawMessage, model pakai sql.NullString, INSERT dan UPDATE nambah satu placeholder. Go nggak nge-unmarshal apa-apa; teks apa adanya masuk kolom. Kontrak OpenAPI ikut kebawa karena skemanya cuma nambah satu field.
Ini sebenarnya pola keempat di repo. operating_hours, social_media, dan amenities udah lebih dulu pakai cara yang sama buat data dinamis per entitas. Kolom attributes cuma naikin polanya jadi wadah umum buat semua modul, termasuk modul yang belum terpikir bentuk atributnya kayak gimana.
Alternatif yang sempat saya tolak: tabel attributes generik ala entity-attribute-value, satu baris per pasangan. Denger pertama keren, karena tiap atribut jadi baris yang bisa diquery. Tapi biayanya nyata: nampilin form berarti pivot balik jadi object, ngesave berarti diff baris lama vs baru, dan query list yang tadinya satu SELECT sekarang nambah join per atribut. Buat atribut yang 90% hidupnya cuma ikut ke-render di halaman detail, kompleksitas itu nggak bayar dirinya sendiri. Satu kolom TEXT plus validasi di editor jadi pilihan yang lebih jujur buat skala kami.
Pendapat saya tetap: kolom TEXT berisi JSON itu escape hatch yang jujur buat MySQL 5.7. Asal validasinya kita gendong di aplikasi, ini jauh lebih rapi daripada menanam puluhan kolom nullable yang sebagian besar nggak kepakai. Kalau entar suatu saat naik ke MySQL 8 dan butuh validasi di database, kolomnya tinggal dimigrasi ke tipe JSON asli. Untuk sekarang, tiap permintaan atribut baru dari dinas selesai tanpa nyentuh skema sama sekali.
Kontrak kecilnya begini: kolom ini cuma buat kebutuhan tulis-baca sederhana. Begitu atribut butuh filter serius di list page atau join ke tabel lain, itu alarm buat promosi jadi kolom sungguhan lewat migration, bukan ditimbun lagi di TEXT. Dengan batas yang jelas sejak awal, wadah fleksibel dan skema terstruktur nggak rebutan tempat.