Menulis Migrasi Database yang Tidak Akan Rusak Jika Dijalankan Dua Kali

Anda sedang men-deploy fitur baru yang membutuhkan kolom tambahan di tabel users. Anda menulis skrip migrasi, menjalankannya di staging, semuanya terlihat baik. Kemudian seseorang di tim menjalankan skrip yang sama secara tidak sengaja saat pipeline di-retry. Sekarang Anda mendapatkan error: "column already exists." Deployment gagal, seseorang harus memperbaiki database secara manual, dan rilis menjadi tertunda.

Skenario ini lebih sering terjadi daripada yang diakui kebanyakan tim. Perbaikannya tidak rumit, tetapi membutuhkan perubahan cara berpikir tentang skrip migrasi. Anda perlu menulisnya agar bisa dijalankan berkali-kali tanpa menimbulkan masalah.

Apa yang Membuat Migrasi Aman

Prinsip utamanya adalah idempotensi. Sebuah operasi bersifat idempoten ketika menjalankannya sekali atau seratus kali menghasilkan keadaan akhir yang sama. Ini bukan tentang mencegah skrip dijalankan dua kali. Ini tentang memastikan bahwa menjalankannya dua kali tidak menimbulkan kerusakan.

Pikirkan mengapa sebuah skrip mungkin dijalankan lebih dari sekali. Sebuah pipeline gagal di tengah jalan dan di-retry. Seseorang menjalankan migrasi di staging, lupa, dan menjalankannya lagi nanti. Dua developer menerapkan perubahan yang sama di lingkungan yang berbeda pada waktu yang berbeda. Tanpa idempotensi, skenario mana pun dapat merusak data Anda atau menghentikan deployment Anda.

Pola Sederhana: Periksa Sebelum Bertindak

Cara paling langsung untuk membuat migrasi idempoten adalah memeriksa apakah perubahan sudah ada sebelum menerapkannya. Database SQL memudahkan ini dengan pernyataan kondisional.

Berikut contoh konkretnya. Misalkan Anda perlu menambahkan kolom last_login_at untuk melacak aktivitas pengguna:

-- Non-idempoten: gagal jika kolom sudah ada
ALTER TABLE users ADD COLUMN last_login_at TIMESTAMP;

-- Idempoten: berhasil apakah kolom sudah ada atau belum
ALTER TABLE users ADD COLUMN IF NOT EXISTS last_login_at TIMESTAMP;

Versi pertama akan melempar error jika kolom sudah ada. Versi kedua berjalan aman setiap saat.

Alih-alih menulis:

ALTER TABLE users ADD COLUMN phone_number VARCHAR(20);

Tulislah:

ALTER TABLE users ADD COLUMN IF NOT EXISTS phone_number VARCHAR(20);

Klausa IF NOT EXISTS berarti skrip berhasil baik kolom sudah ada atau belum. Pendekatan yang sama berlaku untuk indeks, constraint, dan tabel baru. PostgreSQL, MySQL, dan sebagian besar database modern mendukung konstruksi kondisional ini.

Untuk menghapus sesuatu, logika yang sama berlaku. Menghapus kolom yang tidak ada akan gagal. Gunakan IF EXISTS untuk membuat operasi aman:

ALTER TABLE users DROP COLUMN IF EXISTS old_phone_number;

Menangani Migrasi Data

Penambahan dan penghapusan kolom adalah kasus yang mudah. Kompleksitas sesungguhnya muncul ketika Anda perlu memindahkan atau mentransformasi data yang sudah ada. Misalkan Anda memisahkan kolom full_name menjadi first_name dan last_name. Migrasi perlu:

  1. Menambahkan dua kolom baru
  2. Mengisinya dari data yang sudah ada
  3. Menangani kasus ketika skrip dijalankan lagi

Pendekatan naif akan menyalin data tanpa memeriksa apakah sudah disalin. Menjalankannya dua kali membuat data duplikat atau menimpa nilai yang valid. Pola yang lebih aman terlihat seperti ini:

-- Tambahkan kolom jika belum ada
ALTER TABLE users ADD COLUMN IF NOT EXISTS first_name VARCHAR(100);
ALTER TABLE users ADD COLUMN IF NOT EXISTS last_name VARCHAR(100);

-- Hanya isi jika kolom target kosong
UPDATE users 
SET first_name = SPLIT_PART(full_name, ' ', 1),
    last_name = SPLIT_PART(full_name, ' ', 2)
WHERE first_name IS NULL AND last_name IS NULL;

Klausa WHERE memastikan pembaruan hanya berjalan pada baris yang belum diproses. Jika skrip dijalankan lagi, baris-baris tersebut sudah terisi dan pembaruan tidak melakukan apa-apa.

Untuk skenario yang lebih kompleks, Anda mungkin perlu membandingkan data sumber dan target, atau menggunakan checksum untuk memverifikasi bahwa transformasi menghasilkan hasil yang benar. Prinsipnya tetap sama: jangan pernah berasumsi data dalam keadaan aslinya.

Penghapusan Bertahap Mengurangi Risiko

Menghapus kolom atau tabel berisiko karena Anda tidak bisa mengembalikannya dengan mudah. Pendekatan yang lebih aman adalah melakukannya secara bertahap:

  1. Migrasi pertama: ganti nama kolom menjadi column_name_deprecated
  2. Tunggu beberapa siklus rilis untuk memastikan tidak ada yang rusak
  3. Migrasi kedua: hapus kolom yang sudah tidak dipakai

Pola ini memberi waktu bagi tim Anda untuk menangkap kode yang masih mereferensi kolom lama. Jika ada yang salah, mengganti nama bersifat reversibel. Menghapus tidak.

Catat Riwayat Eksekusi

Idempotensi menangani kasus ketika skrip dijalankan beberapa kali. Tetapi Anda juga perlu tahu skrip mana yang sudah dijalankan, kapan dijalankan, dan apakah berhasil. Ini adalah jejak audit Anda.

Sebagian besar framework migrasi seperti Flyway atau Liquibase menangani ini secara otomatis. Mereka membuat tabel yang melacak setiap skrip migrasi berdasarkan nama, checksum, dan timestamp eksekusi. Jika Anda menulis skrip SQL mentah tanpa framework, buat tabel pelacakan Anda sendiri:

CREATE TABLE IF NOT EXISTS migration_log (
    script_name VARCHAR(255) PRIMARY KEY,
    started_at TIMESTAMP,
    completed_at TIMESTAMP,
    status VARCHAR(20),
    script_hash VARCHAR(64)
);

Sebelum menjalankan migrasi apa pun, masukkan baris dengan nama skrip dan status "running." Setelah selesai, perbarui status menjadi "success" atau "failed." Jika skrip gagal, pipeline dapat di-retry, dan pelari migrasi dapat memeriksa apakah skrip sudah selesai dengan sukses.

Log ini tidak hanya untuk debugging. Ini adalah bukti Anda untuk kepatuhan dan tata kelola. Ketika seseorang bertanya "siapa yang mengubah tabel users dan kapan," jawabannya harus datang dari log, bukan dari ingatan seseorang.

Daftar Periksa Praktis untuk Menulis Skrip Migrasi

Sebelum menjalankan migrasi apa pun di produksi, verifikasi poin-poin ini:

  • Dapatkah skrip dijalankan dua kali tanpa error? Uji dengan menjalankannya dua kali berturut-turut pada salinan database Anda.
  • Apakah skrip memeriksa kolom, indeks, atau constraint yang sudah ada sebelum membuatnya?
  • Untuk transformasi data, apakah skrip menangani baris yang sudah ditransformasi dengan aman?
  • Apakah penghapusan dilakukan secara bertahap, dengan periode depresiasi sebelum penghapusan?
  • Apakah ada entri log untuk setiap eksekusi skrip, termasuk timestamp dan status?
  • Dapatkah Anda mengembalikan perubahan jika ada yang salah? Jika tidak, apakah Anda memiliki rencana?

Kesimpulan

Skrip migrasi yang gagal ketika dijalankan dua kali bukanlah skrip migrasi. Itu adalah bom waktu yang menunggu seseorang untuk me-retry pipeline. Tulislah setiap migrasi seolah-olah akan dieksekusi berkali-kali, karena dalam praktiknya, kemungkinan besar akan begitu. Periksa sebelum membuat, verifikasi sebelum mentransformasi, dan catat semuanya. Diri Anda di masa depan, yang sedang melakukan debugging deployment jam 2 pagi, akan berterima kasih.