Aplikasi hidup lebih lama dari skema pertamanya. Kolom telepon ditambah bulan depan, tabel alamat menyusul kuartal berikutnya, dan indeks baru muncul saat data membesar. Tanpa disiplin, perubahan skema diterapkan lewat perintah manual yang berbeda di tiap lingkungan — basis data developer lengkap, staging tertinggal satu kolom, dan produksi gagal dengan pesan “column not found” justru pada malam rilis. Database migrations menuntas masalah ini dengan tiga aturan: setiap perubahan skema ditulis sebagai berkas terversi, diterapkan berurutan oleh alat yang sama di semua lingkungan, dan tercatat dalam tabel riwayat di database itu sendiri. Pada Chapter ini kita akan membangun mesin migrasi mainan untuk memahami mekanismenya, lalu melihat alat produksi yang menyandang pola ini.
Anatomi Sebuah Migrasi #
Setiap migrasi membawa tiga identitas: nomor versi yang menentukan urutan, nama deskriptif yang menjelaskan maksudnya, dan dua skrip — up untuk menerapkan perubahan, down untuk mengembalikannya. Konvensi penamaan Flyway, alat migrasi paling umum di ekosistem Java, memakai pola V1__buat_tabel_pengguna.sql; angka sebelum dua garis bawah adalah versi, teks setelahnya adalah nama. Sebuah direktori migrasi yang sehat terlihat seperti ini:
db/migration/
├── V1__buat_tabel_pengguna.sql
├── V2__tambah_kolom_telepon.sql
└── V3__buat_indeks_email.sqlAlat migrasi menjaga tabel kecil di database, biasanya bernama flyway_schema_history, berisi versi, nama, waktu terapkan, dan checksum berkas. Checksum inilah penjaga disiplin: bila seseorang menyunting berkas migrasi lama yang sudah terapkan, checksum berubah dan alat menolak berjalan — riwayat yang sudah tercatat tidak boleh ditulis ulang. Perbaikan dilakukan dengan migrasi baru, bukan dengan menyunting yang lama.
Mesin Migrasi Mini #
Mekanika inti migrasi cukup sederhana untuk kita bangun sendiri. Program berikut memodelkan buku migrasi: mendaftarkan migrasi terversi, menaikkan skema satu per satu, dan melaporkan kondisi “sudah terkini” dengan sopan:
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public class RiwayatSkema {
record Migrasi(int versi, String nama, String sqlNaik, String sqlTurun) {
}
static class BukuMigrasi {
private final Map<Integer, Migrasi> terdaftar = new LinkedHashMap<>();
private int versiTerpasang = 0;
void daftar(Migrasi m) {
if (terdaftar.containsKey(m.versi())) {
throw new IllegalStateException("versi " + m.versi() + " sudah ada");
}
terdaftar.put(m.versi(), m);
}
void naikkan() {
int berikut = versiTerpasang + 1;
Migrasi m = terdaftar.get(berikut);
if (m == null) {
System.out.println("tidak ada migrasi baru (versi " + versiTerpasang + ")");
return;
}
System.out.println("migrasi " + m.versi() + " -- " + m.nama());
System.out.println(" SQL: " + m.sqlNaik());
versiTerpasang = berikut;
}
int versi() {
return versiTerpasang;
}
List<String> riwayat() {
List<String> r = new ArrayList<>();
for (int v = 1; v <= versiTerpasang; v++) {
r.add("V" + v + "__" + terdaftar.get(v).nama());
}
return r;
}
}
public static void main(String[] args) {
BukuMigrasi buku = new BukuMigrasi();
buku.daftar(new Migrasi(1, "buat_tabel_pengguna",
"CREATE TABLE pengguna (...);", "DROP TABLE pengguna;"));
buku.daftar(new Migrasi(2, "tambah_kolom_telepon",
"ALTER TABLE pengguna ADD telepon VARCHAR(20);",
"ALTER TABLE pengguna DROP COLUMN telepon;"));
buku.daftar(new Migrasi(3, "buat_indeks_email",
"CREATE INDEX idx_email ON pengguna(email);",
"DROP INDEX idx_email;"));
buku.naikkan();
buku.naikkan();
buku.naikkan();
buku.naikkan(); // tidak ada lagi -- aman dan dilaporkan
System.out.println("versi skema : " + buku.versi());
System.out.println("riwayat : " + String.join(", ", buku.riwayat()));
}
}Output yang Dihasilkan:
migrasi 1 -- buat_tabel_pengguna
SQL: CREATE TABLE pengguna (...);
migrasi 2 -- tambah_kolom_telepon
SQL: ALTER TABLE pengguna ADD telepon VARCHAR(20);
migrasi 3 -- buat_indeks_email
SQL: CREATE INDEX idx_email ON pengguna(email);
tidak ada migrasi baru (versi 3)
versi skema : 3
riwayat : V1__buat_tabel_pengguna, V2__tambah_kolom_telepon, V3__buat_indeks_emailMari kita bedah bagian-bagian mesin tersebut:
record Migrasi(int versi, String nama, String sqlNaik, String sqlTurun): satu perubahan skema sebagai nilai utuh — versi untuk urutan, dan pasangan naik-turun untuk penerapan maupun pengembalian.BukuMigrasi.daftar(...): menolak nomor versi ganda denganIllegalStateException— urutan terversi harus tunggal, sama seperti checksum Flyway menjaga riwayat tak tersunting.naikkan(): hanya menerapkan migrasi bernomor tepat setelah versi terpasang. Pemanggilan keempat pada contoh menemukan kondisi terkini dan melaporkannya tanpa mengubah apa pun — perilaku idempoten yang membuat migrasi aman dijalankan berulang.riwayat(): menegaskan kembali versi final dan daftar migrasi terpasang — padanan tabelflyway_schema_historydi dunia nyata.
Praktik yang Menjaga Skema Sehat #
Pengalaman lintas tim merangkum menjadi segelintir aturan. Pertama, migrasi kecil dan satu maksud: satu tabel, satu kolom, atau satu indeks per berkas — tinjauan kode menjadi mudah dan kegagalan terlokalisasi. Kedua, jangan pernah menyunting migrasi yang sudah terapankan di lingkungan mana pun; tambahkan yang baru. Ketiga, tulis migrasi down sejak awal meski jarang dipakai — ia adalah jaring pengaman saat rilis harus ditarik mundur. Keempat, ubah skema dan kode aplikasi dalam rilis yang kompatibel dua arah: kolom baru ditambah dulu, kode membaca kolom lama dan baru bersamaan, baru kolom lama dihapus pada rilis berikutnya — teknik expand and contract yang menghindari downtime.
Alat di Ekosistem Java #
Dua nama menguasai lapangan. Flyway memakai pendekatan SQL murni: kita menulis berkas .sql terversi, alat menjalankannya berurutan dan mencatat riwayatnya — filosofi “SQL adalah sumber kebenaran”. Liquibase lebih abstrak: perubahan dideklarasikan dalam format XML/YAML/JSON yang kemudian diterjemahkan ke dialek database mana pun — berguna ketika aplikasi harus berjalan di beberapa database berbeda. Keduanya terintegrasi rapat dengan Maven/Gradle maupun Spring Boot (Spring Boot menjalankan Flyway otomatis saat startup bila berkasnya ada di classpath). Untuk proyek baru ber satu database, Flyway dengan berkas SQL polos adalah titik awal yang paling sedikit menyimpan kejutan.
Mekanisme yang kita rangkai sendiri tadi — versi, urutan, riwayat, idempotensi — adalah tepat prinsip yang dijalankan alat-alat itu berulang setiap hari di jutaan deployment. Chapter penutup Section database merangkum seluruh praktik JDBC dan pola akses data menjadi satu daftar periksa siap pakai.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
RiwayatSkema.java
Unduh berkasimport java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public class RiwayatSkema {
record Migrasi(int versi, String nama, String sqlNaik, String sqlTurun) {
}
static class BukuMigrasi {
private final Map<Integer, Migrasi> terdaftar = new LinkedHashMap<>();
private int versiTerpasang = 0;
void daftar(Migrasi m) {
if (terdaftar.containsKey(m.versi())) {
throw new IllegalStateException("versi " + m.versi() + " sudah ada");
}
terdaftar.put(m.versi(), m);
}
void naikkan() {
int berikut = versiTerpasang + 1;
Migrasi m = terdaftar.get(berikut);
if (m == null) {
System.out.println("tidak ada migrasi baru (versi " + versiTerpasang + ")");
return;
}
System.out.println("migrasi " + m.versi() + " -- " + m.nama());
System.out.println(" SQL: " + m.sqlNaik());
versiTerpasang = berikut;
}
int versi() {
return versiTerpasang;
}
List<String> riwayat() {
List<String> r = new ArrayList<>();
for (int v = 1; v <= versiTerpasang; v++) {
r.add("V" + v + "__" + terdaftar.get(v).nama());
}
return r;
}
}
public static void main(String[] args) {
BukuMigrasi buku = new BukuMigrasi();
buku.daftar(new Migrasi(1, "buat_tabel_pengguna",
"CREATE TABLE pengguna (...);", "DROP TABLE pengguna;"));
buku.daftar(new Migrasi(2, "tambah_kolom_telepon",
"ALTER TABLE pengguna ADD telepon VARCHAR(20);",
"ALTER TABLE pengguna DROP COLUMN telepon;"));
buku.daftar(new Migrasi(3, "buat_indeks_email",
"CREATE INDEX idx_email ON pengguna(email);",
"DROP INDEX idx_email;"));
buku.naikkan();
buku.naikkan();
buku.naikkan();
buku.naikkan(); // tidak ada lagi -- aman dan dilaporkan
System.out.println("versi skema : " + buku.versi());
System.out.println("riwayat : " + String.join(", ", buku.riwayat()));
}
}