Menulis Migrasi Database yang Tidak Akan Merusak Produksi
Anda memiliki database yang sudah berjalan selama berbulan-bulan. Pengguna bergantung padanya. Tabel telah membesar, kueri telah dioptimalkan, dan skema telah menetap dalam bentuk yang berfungsi. Kemudian datanglah permintaan fitur yang membutuhkan kolom baru, penggantian nama tabel, atau migrasi data.
Saat Anda menjalankan ALTER TABLE di produksi, Anda sedang bertaruh. Jika migrasi memakan waktu terlalu lama, kueri akan mengantre. Jika mengunci tabel, pengguna melihat error. Jika gagal di tengah jalan, Anda perlu jalan kembali. Dan jika Anda tidak memiliki rencana rollback, satu-satunya pilihan adalah memulihkan dari cadangan, yang berarti kehilangan data apa pun yang dimasukkan sejak snapshot terakhir.
Inilah mengapa migrasi database yang aman bukan hanya tentang menulis SQL yang benar. Ini tentang menyusun perubahan sehingga dapat ditinjau, diuji, dan dibalikkan tanpa panik.
Setiap Migrasi Membutuhkan Dua File
Pola paling sederhana yang berulang kali menyelamatkan tim adalah pasangan migrasi naik dan turun.
Berikut adalah contoh konkret dari pasangan migrasi naik dan turun:
-- 20241101_add_last_login_at.up.sql
ALTER TABLE users ADD COLUMN last_login_at TIMESTAMP;
-- 20241101_add_last_login_at.down.sql
ALTER TABLE users DROP COLUMN last_login_at;
Migrasi naik berisi SQL yang membuat perubahan. Migrasi turun berisi SQL yang membatalkannya. Setiap perubahan mendapatkan kedua file, disimpan bersama dengan pengidentifikasi unik sehingga urutannya jelas.
20241101_add_email_index.sql -- naik
20241101_add_email_index_down.sql -- turun
Pengidentifikasi dapat berupa stempel waktu, nomor urut, atau awalan tanggal. Yang penting adalah siapa pun yang melihat folder tersebut dapat melihat urutan perubahan yang tepat. Ketika migrasi berjalan di produksi dan terjadi kesalahan, migrasi turun memberi Anda cara yang cepat dan dapat diprediksi untuk kembali.
Tanpa migrasi turun, satu-satunya cadangan Anda adalah pemulihan database penuh. Itu memakan waktu, membutuhkan koordinasi, dan berisiko kehilangan data terbaru. Migrasi turun berjalan dalam hitungan detik.
Ketika Migrasi Turun Tidak Cukup
Migrasi turun bekerja dengan baik untuk perubahan yang dapat dibalik: menambahkan kolom, membuat indeks, menyisipkan data referensi. Tetapi beberapa perubahan sulit untuk dibatalkan.
Menghapus kolom adalah contoh umum. Setelah kolom hilang dan data dihapus, migrasi turun yang menambahkan kembali kolom tidak dapat mengembalikan data. Masalah yang sama berlaku untuk mengganti nama tabel, mengubah tipe kolom, atau menghapus batasan yang bergantung pada bagian lain dari sistem.
Untuk kasus ini, pendekatan yang aman adalah memecah perubahan menjadi beberapa migrasi kecil, masing-masing dapat dibalik dengan sendirinya:
- Tambahkan kolom baru dengan tipe yang diinginkan.
- Isi ulang data dalam batch.
- Perbarui kode aplikasi untuk menggunakan kolom baru.
- Hapus kolom lama.
Setiap langkah memiliki migrasi naik dan turun sendiri. Jika langkah 3 mengungkapkan masalah, Anda dapat membalikkan langkah 2 dan langkah 1 dengan bersih. Anda tidak pernah mencapai titik di mana satu-satunya jalan keluar adalah pemulihan.
Tulis Migrasi yang Dapat Dijalankan Berkali-kali
Pipeline gagal. Jaringan putus, timeout terjadi, dan terkadang migrasi berjalan setengah jalan sebelum proses crash. Ketika pipeline mencoba ulang, migrasi berjalan lagi.
Jika migrasi Anda mengasumsikan perubahan belum diterapkan, migrasi akan gagal pada kali kedua. Kegagalan itu memblokir seluruh pipeline dan membutuhkan intervensi manual.
Buat setiap migrasi idempoten. Gunakan IF NOT EXISTS saat membuat tabel atau indeks. Gunakan IF EXISTS saat menghapus objek. Periksa apakah kolom sudah ada sebelum mengubahnya. Tujuannya sederhana: menjalankan migrasi yang sama dua kali harus menghasilkan hasil yang sama seperti menjalankannya sekali.
Hindari Kunci yang Lama pada Tabel Besar
ALTER TABLE yang mengubah tipe kolom dapat mengunci seluruh tabel selama beberapa menit pada tabel dengan jutaan baris. Selama waktu itu, setiap baca dan tulis ke tabel tersebut menunggu. Pengguna melihat timeout. Antrean menumpuk.
Perbaikannya adalah menghindari perubahan skema satu langkah pada tabel besar. Sebagai gantinya, gunakan pendekatan multi-langkah:
- Tambahkan kolom baru dengan tipe yang diinginkan.
- Perbarui baris dalam batch untuk mengisi kolom baru.
- Tambahkan indeks jika diperlukan.
- Hapus kolom lama dalam migrasi nanti.
Setiap langkah mengunci sebentar. Aplikasi dapat terus berjalan di antara langkah-langkah. Pola ini lebih lambat untuk ditulis tetapi jauh lebih aman untuk dijalankan.
Jauhkan Nilai Spesifik Lingkungan dari File Migrasi
File migrasi harus bekerja dengan cara yang sama di pengembangan, staging, dan produksi. Jika Anda meng-hardcode nama database, kata sandi, atau string koneksi ke dalam SQL, file tersebut menjadi terikat pada satu lingkungan. Anda tidak dapat menjalankannya di tempat lain tanpa mengeditnya, dan mengedit file migrasi setelah ditinjau akan mengalahkan tujuan kontrol versi.
Gunakan parameter, variabel lingkungan, atau konfigurasi yang disediakan oleh alat migrasi. SQL itu sendiri hanya boleh berisi logika skema dan data, bukan detail lingkungan.
Simpan Migrasi Bersamaan dengan Kode Aplikasi
Ada dua pendekatan umum: simpan file migrasi di repositori yang sama dengan kode aplikasi, atau simpan di repositori terpisah. Keduanya berfungsi, tetapi pilihan mempengaruhi bagaimana tim mengoordinasikan perubahan.
Ketika migrasi berada di repositori yang sama, setiap pull request yang mengubah skema juga menyertakan migrasi. Tinjauan kode mencakup perubahan aplikasi dan perubahan database bersama-sama. Ini memudahkan untuk melihat ketidakcocokan, seperti kueri yang mereferensikan kolom yang belum ditambahkan.
Ketika migrasi berada di repositori terpisah, perubahan aplikasi dan database dapat berkembang pada jadwal yang berbeda. Ini berguna ketika beberapa layanan berbagi satu database, tetapi membutuhkan lebih banyak koordinasi untuk menjaga skema dan kode tetap sinkron.
Bagaimanapun, kuncinya adalah file migrasi dikontrol versi, ditinjau, dan dapat dilacak. Perubahan database harus meninggalkan jejak audit yang sama seperti perubahan kode.
Daftar Periksa Praktis untuk Menulis Migrasi yang Aman
Sebelum Anda menggabungkan migrasi ke dalam pipeline, lakukan pemeriksaan ini:
- Apakah setiap migrasi memiliki migrasi turun yang sesuai?
- Dapatkah migrasi turun benar-benar mengembalikan keadaan sebelumnya tanpa kehilangan data?
- Apakah migrasi idempoten? Dapatkah dijalankan dua kali tanpa error?
- Akankah migrasi mengunci tabel besar selama lebih dari beberapa detik?
- Apakah nilai spesifik lingkungan tidak ada dalam file SQL?
- Apakah file migrasi disimpan di repositori dengan kode aplikasi atau repositori database khusus?
Kesimpulan
Migrasi database yang aman bukan tentang menghindari perubahan. Ini tentang membuat perubahan dapat dibalik, dapat diuji, dan dapat ditinjau. Setiap file migrasi yang Anda tulis adalah kontrak kecil: inilah yang berubah, dan inilah cara membatalkannya. Ketika kontrak itu jelas, tim dapat bergerak lebih cepat karena mereka tahu mereka memiliki jalan kembali.