Topik ini membahas migrasi skema (schema migration), yaitu praktik merekam setiap perubahan struktur basis data sebagai berkas SQL bernomor yang diterapkan secara berurutan. Pembahasan mencakup alasan pencatatan versi, bentuk berkas migrasinya, skrip penerapan yang membaca tabel pencatat, serta verifikasi hasilnya. Setelah menyelesaikan topik ini, versi skema basis data aplikasi dapat dinaikkan secara berulang tanpa menebak kondisi terkininya.
Perubahan skema yang perlu dicatat#
Skema basis data tidak berhenti pada desain awalnya. Aplikasi yang berkembang menuntut kolom baru, indeks tambahan, atau tabel pendukung, dan setiap tuntutan itu berupa pernyataan ALTER TABLE atau CREATE TABLE yang harus dijalankan pada server produksi, server latihan, serta komputer setiap pengembang. Menerapkannya secara manual di setiap tempat menimbulkan perbedaan yang sulit dilacak, karena tidak ada catatan tentang perubahan yang sudah diterapkan pada setiap server.
Migrasi skema menjawab masalah itu dengan dua komponen. Komponen pertama adalah berkas SQL bernomor yang memuat satu perubahan utuh dan tidak pernah disunting lagi setelah diterapkan. Komponen kedua adalah tabel pencatat, lazim bernama schema_migrations, yang menyimpan daftar berkas yang sudah diterapkan. Skrip penerapan membandingkan isi direktori berkas migrasi dengan isi tabel pencatat, lalu menerapkan berkas yang belum tercatat secara berurutan. Dengan komponen ini, skema yang sama dapat dibangun ulang dari nol di tempat mana pun hanya dengan menjalankan seluruh rangkaiannya.
Berkas migrasi dan urutannya#
Urutan penerapan mengikuti urutan nama berkas, dan prefiks tiga digit seperti 001 serta 002 sudah cukup untuk menjaga susunannya. Rangkaian pada topik ini membangun basis data perpustakaan dari nol. Berkas pertama membuat tiga tabel utama beserta data awalnya.
-- tidak-dijalankan
-- Migrasi 001: struktur awal basis data perpustakaan beserta data awalnya.
CREATE TABLE anggota (
id INT AUTO_INCREMENT PRIMARY KEY,
nama VARCHAR(60) NOT NULL,
kota VARCHAR(40) NOT NULL
);
CREATE TABLE buku (
id INT AUTO_INCREMENT PRIMARY KEY,
judul VARCHAR(120) NOT NULL,
penulis VARCHAR(60) NOT NULL
);
CREATE TABLE peminjaman (
id INT AUTO_INCREMENT PRIMARY KEY,
anggota_id INT NOT NULL,
buku_id INT NOT NULL,
tanggal_pinjam DATE NOT NULL,
tanggal_kembali DATE NULL,
FOREIGN KEY (anggota_id) REFERENCES anggota (id),
FOREIGN KEY (buku_id) REFERENCES buku (id)
);
INSERT INTO anggota (nama, kota) VALUES
('Ali', 'Bandung'),
('Budi', 'Semarang'),
('Citra', 'Surabaya');
INSERT INTO buku (judul, penulis) VALUES
('Katalog Dasar', 'Sari Utami'),
('Panduan Arsip', 'Tono Wijaya'),
('Sistem Rak Buku', 'Rini Hartati'),
('Jejak Peminjam', 'Bambang Sutrisno');
INSERT INTO peminjaman (anggota_id, buku_id, tanggal_pinjam, tanggal_kembali) VALUES
(1, 1, '2026-10-20', '2026-10-27'),
(2, 2, '2026-10-22', NULL),
(1, 3, '2026-10-25', NULL);Tabel peminjaman memuat kunci asing ke anggota dan buku, sehingga integritas relasinya ikut terjaga sejak migrasi pertama. Data awalnya sengaja kecil, yaitu tiga anggota, empat buku, dan tiga peminjaman, agar setiap keluaran mudah dihitung ulang.
Berkas kedua menaikkan skema dengan menambahkan kolom email pada tabel anggota dan kolom stok pada tabel buku.
-- tidak-dijalankan
-- Migrasi 002: kolom email pada tabel anggota dan kolom stok pada tabel buku.
ALTER TABLE anggota ADD COLUMN email VARCHAR(120) NULL AFTER nama;
ALTER TABLE buku ADD COLUMN stok INT NOT NULL DEFAULT 0 AFTER penulis;Kedua pernyataan ALTER TABLE menambahkan kolom tanpa mengubah data yang ada, dan kolom stok diberi nilai default 0 agar baris lama memiliki nilai yang jelas. Inilah bentuk perubahan yang paling sering terjadi pada aplikasi yang sudah berjalan, yaitu struktur bertambah sedangkan data lama tetap dipelihara.
Skrip penerapan dengan tabel pencatat#
Skrip migrasi.py berikut menghubungkan kedua komponen tersebut. Skrip ini memastikan tabel schema_migrations ada, membaca versi yang sudah tercatat, lalu menerapkan setiap berkas .sql yang belum tercatat di direktorinya, berurutan sesuai urutan namanya.
-- tidak-dijalankan
CREATE TABLE schema_migrations (
versi VARCHAR(64) PRIMARY KEY
);# tidak-dijalankan
import os
import pymysql
DIREKTORI = os.path.dirname(os.path.abspath(__file__))
def utama():
koneksi = pymysql.connect(
host="127.0.0.1",
user="root",
database="perpustakaan",
autocommit=True,
)
with koneksi.cursor() as kursor:
kursor.execute(
"CREATE TABLE IF NOT EXISTS schema_migrations ("
"versi VARCHAR(64) PRIMARY KEY)"
)
kursor.execute("SELECT versi FROM schema_migrations")
diterapkan = {baris[0] for baris in kursor.fetchall()}
daftar = sorted(n for n in os.listdir(DIREKTORI) if n.endswith(".sql"))
for nama in daftar:
if nama in diterapkan:
print(f"{nama}: sudah diterapkan, dilewati")
continue
with open(os.path.join(DIREKTORI, nama), encoding="utf-8") as berkas:
isi = berkas.read()
try:
with koneksi.cursor() as kursor:
for pernyataan in isi.split(";"):
if pernyataan.strip():
kursor.execute(pernyataan)
kursor.execute(
"INSERT INTO schema_migrations (versi) VALUES (%s)", (nama,)
)
except pymysql.err.MySQLError as err:
print(f"{nama}: GAGAL — {err}")
raise SystemExit(1)
print(f"{nama}: diterapkan")
koneksi.close()
utama()Pencatatan versi ditulis dengan query berparameter, sebagaimana layaknya nilai yang berasal dari luar query. Bila sebuah berkas gagal diterapkan, skrip mencetak pesannya dan berhenti, sehingga berkas berikutnya tidak dijalankan di atas skema yang belum siap. Eksekusi pertamanya berlangsung seperti berikut.
# menyiapkan basis data latihan lalu menerapkan seluruh migrasi tertunda
mysql -u root -e "DROP DATABASE IF EXISTS perpustakaan;"
mysql -u root -e "CREATE DATABASE perpustakaan;"
python3 migrasi.py001_init.sql: diterapkan
002_tambah_kolom.sql: diterapkanKedua berkas diterapkan sesuai urutan namanya, dan setiap keberhasilannya langsung dicatat ke schema_migrations.
Penerapan ulang dan verifikasi hasil#
Menjalankan skrip untuk kedua kalinya menguji sifat idempotennya, yaitu hasil yang tetap sama meskipun perintahnya diulang. Karena kedua versi sudah tercatat, tidak ada berkas yang diterapkan lagi.
# menjalankan ulang skrip migrasi
python3 migrasi.py001_init.sql: sudah diterapkan, dilewati
002_tambah_kolom.sql: sudah diterapkan, dilewatiStruktur akhir diperiksa dengan SHOW COLUMNS pada kedua tabel yang berubah.
# memeriksa struktur tabel yang ditambah kolomnya
mysql -t -u root perpustakaan -e "SHOW COLUMNS FROM anggota;"
mysql -t -u root perpustakaan -e "SHOW COLUMNS FROM buku;"+-------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------+--------------+------+-----+---------+----------------+
| id | int | NO | PRI | NULL | auto_increment |
| nama | varchar(60) | NO | | NULL | |
| email | varchar(120) | YES | | NULL | |
| kota | varchar(40) | NO | | NULL | |
+-------+--------------+------+-----+---------+----------------+
+---------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+---------+--------------+------+-----+---------+----------------+
| id | int | NO | PRI | NULL | auto_increment |
| judul | varchar(120) | NO | | NULL | |
| penulis | varchar(60) | NO | | NULL | |
| stok | int | NO | | 0 | |
+---------+--------------+------+-----+---------+----------------+Kolom email berada di antara nama dan kota, sedangkan stok berada di akhir tabel buku dengan default 0. Verifikasi terakhir mengecek bahwa data awal utuh dan tabel pencatat memuat seluruh versi.
# memeriksa jumlah baris data awal dan isi tabel pencatat
mysql -t -u root perpustakaan -e "SELECT COUNT(*) AS jumlah FROM peminjaman; SELECT versi FROM schema_migrations ORDER BY versi;"+--------+
| jumlah |
+--------+
| 3 |
+--------+
+----------------------+
| versi |
+----------------------+
| 001_init.sql |
| 002_tambah_kolom.sql |
+----------------------+Hitungan peminjaman tetap tiga, yang berarti data dari migrasi pertama tidak tersentuh oleh migrasi kedua. Tabel schema_migrations mencatat dua versi sesuai urutan namanya, dan daftar itulah yang dibaca skrip pada setiap penerapan.
Kesalahan umum#
Berkas migrasi yang sudah tercatat disunting. Versi yang tercatat tidak akan dijalankan lagi, sehingga suntingan itu hanya diterapkan pada lingkungan yang masih membangun skemanya dari nol. Akibatnya skema antarlingkungan berbeda tanpa ada yang menyadari. Perbaikannya selalu berupa berkas baru, misalnya 003_perbaiki_email.sql, yang mengoreksi hasil berkas sebelumnya.
Isi tabel pencatat dihapus, tetapi tabel skema masih ada. Skrip menganggap seluruh migrasi belum diterapkan dan mencoba membuat tabel yang sudah ada. Contoh berikut mengosongkan schema_migrations lalu menjalankan skripnya kembali.
# menghapus catatan versi lalu menjalankan ulang skrip migrasi
mysql -u root perpustakaan -e "DELETE FROM schema_migrations;"
python3 migrasi.py001_init.sql: GAGAL — (1050, "Table 'anggota' already exists")Pesan error 1050 menunjukkan tabel anggota sudah ada, dan skrip berhenti sebelum menyentuh 002_tambah_kolom.sql. Kondisi seperti ini dipulihkan dengan mengosongkan basis data lalu menjalankan seluruh migrasi dari awal.
# pemulihan: basis data diosongkan lalu seluruh migrasi dijalankan ulang
mysql -u root -e "DROP DATABASE IF EXISTS perpustakaan;"
mysql -u root -e "CREATE DATABASE perpustakaan;"
python3 migrasi.py001_init.sql: diterapkan
002_tambah_kolom.sql: diterapkanSatu berkas migrasi memuat terlalu banyak perubahan. Berkas yang menggabungkan beberapa ALTER TABLE pada tabel besar memakan waktu lama, dan karena DDL tidak dapat digulung balik, kegagalannya menyisakan skema yang hanya sebagian berubah. Pecah perubahan menjadi beberapa berkas kecil sehingga satu kegagalan hanya menyangkut satu perubahan.
Ringkasan#
- Migrasi skema merekam perubahan struktur sebagai berkas SQL bernomor yang tidak pernah disunting setelah diterapkan.
- Tabel
schema_migrationsmenyimpan versi yang sudah diterapkan, dan skrip penerapannya hanya menjalankan berkas yang belum tercatat secara berurutan. - Penerapan ulang bersifat idempoten, dan kegagalan satu berkas menghentikan rangkaian sebelum berkas berikutnya dijalankan.
- Hasilnya diverifikasi dengan
SHOW COLUMNSdan hitungan data, sedangkan koreksi pada berkas lama diganti berkas baru.
Referensi#
- ALTER TABLE Statement — Oracle
— Opsi penambahan kolom pada
ALTER TABLEbeserta perilakunya. - CREATE TABLE Statement — Oracle — Sintaks pembuatan tabel yang dipakai berkas migrasi pertama.
- PyMySQL Cursors — PyMySQL Project
— Perilaku
execute()yang diandalkan skrip penerapan migrasi.