← Semua pembelajaran / Astro Nol → Portal Berita
Fase 9 · Migrasi dari CodeIgniter 3

Membersihkan data & konten warisan

Databasemu berumur bertahun-tahun dan sudah melewati beberapa sistem. Migrasi platform adalah kesempatan terbaik membersihkannya — dan satu-satunya waktu di mana biayanya bisa dibenarkan.

Sumber asli dev.mysql.com Resmi Rangkuman ~9 menit baca

Intisari

  • latin1 atau utf8mb3 di tabel lama berarti emoji dan sebagian karakter tidak bisa disimpan.
  • Konversi ke utf8mb4 menyalin seluruh tabel — jadwalkan, jangan selipkan di deploy biasa.
  • Bahaya terbesar: teks yang sudah rusak (mojibake) dari konversi sebelumnya yang tidak lengkap.
  • HTML badan artikel biasanya campuran dari beberapa generasi editor — normalkan bertahap, jangan sekaligus.
  • Inventarisasi dulu, ukur dulu, baru putuskan apa yang layak dibersihkan.

Inventarisasi charset

SELECT TABLE_NAME, TABLE_COLLATION,
       ROUND(DATA_LENGTH / 1024 / 1024) AS mb
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'portal'
ORDER BY DATA_LENGTH DESC;

-- Per kolom, karena kolom bisa berbeda dari tabelnya
SELECT TABLE_NAME, COLUMN_NAME, CHARACTER_SET_NAME, COLLATION_NAME
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = 'portal'
  AND CHARACTER_SET_NAME IS NOT NULL
  AND CHARACTER_SET_NAME <> 'utf8mb4';
CharsetMasalahnya
latin1Tidak bisa menyimpan karakter non-Latin maupun emoji
utf8 / utf8mb3Maksimal 3 byte per karakter — emoji gagal
utf8mb4Yang benar

utf8 di MySQL bukan UTF-8 yang lengkap. Ia alias untuk utf8mb3, yang maksimal tiga byte per karakter — cukup untuk sebagian besar bahasa, tapi tidak untuk emoji dan sebagian aksara. Gejalanya di portal berita: judul artikel yang mengandung emoji terpotong tepat di emoji itu, atau seluruh INSERT gagal dengan "Incorrect string value".

Mendeteksi teks yang sudah rusak

-- Mojibake khas: UTF-8 yang pernah dibaca sebagai latin1
SELECT id, judul FROM artikel
WHERE judul LIKE '%Ã%' OR judul LIKE '%â€%' OR judul LIKE '%Â%'
LIMIT 50;
Terlihat sebagaiSeharusnya
“" (kutip kiri)
’' (apostrof)
éé
 spasi tak terputus

Kalau kamu menemukan pola ini, jangan langsung ALTER TABLE. Konversi charset pada data yang sudah rusak akan mengunci kerusakannya — teks yang sebelumnya bisa dipulihkan jadi benar-benar hilang. Perbaiki dulu datanya, baru konversi tabelnya. Urutan terbalik adalah kesalahan yang tidak bisa dibatalkan tanpa memulihkan dari cadangan.

-- Perbaikan mojibake: baca ulang byte-nya sebagai latin1, tafsirkan sebagai utf8mb4.
-- UJI DI SALINAN DULU. Selalu.
UPDATE artikel
SET judul = CONVERT(BINARY(CONVERT(judul USING latin1)) USING utf8mb4)
WHERE judul LIKE '%Ã%' OR judul LIKE '%â€%';

Konversi charset

-- Cek dulu berapa lama dan apakah bisa tanpa mengunci
ALTER TABLE artikel
  CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci,
  ALGORITHM=INPLACE, LOCK=NONE;
-- Kalau ditolak: operasinya butuh menyalin tabel dan MENGUNCI.
Ukuran tabelPerkiraan waktuPendekatan
< 100 MBDetikLangsung saja
100 MB – 2 GBMenitJam sepi
> 2 GBPuluhan menit sampai jamgh-ost atau pt-online-schema-change

Ingat dampaknya ke replica (Fase 2). ALTER TABLE besar dijalankan ulang di replica, dan selama itu lag-nya melonjak. Kalau pembacaan portalmu sudah dialihkan ke replica, pembaca akan melihat data basi selama migrasi berjalan. Alihkan pembacaan ke writer dulu, atau lakukan sebelum memisahkan baca-tulis.

HTML dari beberapa generasi

-- Apa saja yang ada di badan artikel?
SELECT
  SUM(isi LIKE '%<font%')       AS tag_font,
  SUM(isi LIKE '%style="%')      AS style_sebaris,
  SUM(isi LIKE '%<table%')       AS tabel_layout,
  SUM(isi LIKE '%<center%')      AS tag_center,
  SUM(isi LIKE '%<iframe%')      AS iframe,
  SUM(isi LIKE '%mso-%')         AS tempel_dari_word,
  SUM(isi LIKE '%<script%')      AS script,
  SUM(isi LIKE '%src="http://%') AS gambar_http,
  COUNT(*)                       AS total
FROM artikel WHERE status = 'terbit';
TemuanPrioritasTindakan
<script> di badan artikelKritisSanitasi (Fase 7) — ini XSS tersimpan
src="http://"TinggiKonten campuran; gambar diblokir di halaman HTTPS
Tempelan dari Word (mso-)SedangBikin HTML membengkak berkali-kali lipat
<font>, <center>RendahKosmetik; sanitasi menghapusnya
Tabel untuk tata letakRendahRusak di ponsel, tapi memperbaikinya manual
-- Perbaikan gambar HTTP: cepat dan berdampak langsung
UPDATE artikel
SET isi = REPLACE(isi, 'src="http://portal.contoh.id', 'src="https://portal.contoh.id')
WHERE isi LIKE '%src="http://portal.contoh.id%';

Normalisasi bertahap

// Jangan bersihkan 200 ribu artikel dalam satu transaksi.
export async function bersihkanBatch(batas = 500) {
  const artikel = await dbBaca
    .selectFrom("artikel")
    .select(["id", "isi"])
    .where("disanitasi", "=", 0)
    .orderBy("id", "asc")
    .limit(batas)
    .execute();

  for (const a of artikel) {
    const bersih = bersihkan(a.isi);          // Fase 7

    await dbTulis.updateTable("artikel")
      .set({ isi: bersih, disanitasi: 1 })
      .where("id", "=", a.id)
      .execute();
  }
  return artikel.length;
}

Jalankan sebagai tugas berkala sampai habis. Sementara itu, artikel yang belum tersentuh tetap aman karena dibersihkan saat dirender (Fase 7). Tidak ada jendela pemeliharaan yang dibutuhkan.

Simpan versi aslinya sebelum menimpa. Satu kolom isi_asli atau satu tabel arsip berarti keputusan sanitasi yang ternyata terlalu agresif bisa dibatalkan. Tanpa itu, satu konfigurasi yang keliru bisa menghapus embed video dari sepuluh ribu artikel secara permanen.

Berkas yatim di uploads/

# Semua berkas yang ada
find /var/www/portal/uploads -type f -printf '%P\n' | sort > /tmp/ada.txt

# Yang dirujuk kolom database
mysql -N -e "SELECT gambar FROM artikel WHERE gambar IS NOT NULL
             UNION SELECT foto FROM member WHERE foto IS NOT NULL" portal \
  | sed 's|^/*uploads/||' | sort -u > /tmp/dirujuk-kolom.txt

# Yang dirujuk DI DALAM badan artikel — jangan lupa ini
mysql -N -e "SELECT isi FROM artikel" portal \
  | grep -oE 'uploads/[^"'"'"' )>]+' | sed 's|^uploads/||' | sort -u > /tmp/dirujuk-inline.txt

cat /tmp/dirujuk-kolom.txt /tmp/dirujuk-inline.txt | sort -u > /tmp/dirujuk.txt

comm -23 /tmp/ada.txt /tmp/dirujuk.txt > /tmp/yatim.txt
wc -l /tmp/ada.txt /tmp/dirujuk.txt /tmp/yatim.txt
du -ch $(sed 's|^|/var/www/portal/uploads/|' /tmp/yatim.txt) 2>/dev/null | tail -1

Jangan hapus apa pun sampai migrasi benar-benar selesai. Rujukan bisa ada di tempat yang tidak kamu kueri: tabel halaman statis, template email, arsip komentar, atau berkas konfigurasi. Pindahkan yang yatim ke penyimpanan arsip murah selama enam bulan, pantau 404, baru hapus.

Urutan yang disarankan

UrutanPekerjaanButuh jendela?
1Inventarisasi semuanya; jangan ubah apa punTidak
2Perbaiki mojibakeTidak
3Perbaiki gambar http://Tidak
4Konversi charset ke utf8mb4Ya
5Salin uploads/ ke R2/S3Tidak
6Sanitasi HTML bertahapTidak
7Arsipkan berkas yatimTidak — setelah beberapa bulan

Latihan: jalankan seluruh kueri inventarisasi di salinan database produksimu dan tulis hasilnya jadi satu dokumen: charset per tabel, jumlah baris ber-mojibake, statistik HTML, dan jumlah berkas yatim beserta ukurannya. Dokumen itu adalah lingkup pekerjaan migrasi datamu — dan angkanya biasanya cukup mengejutkan untuk mengubah jadwal proyek.

Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.