Migrasi Schema Database untuk Aplikasi PHP Kecil - Mengubah Struktur Tanpa Menebak-nebak
Release aplikasi dapat di-rollback dengan checkout commit sebelumnya. Database tidak sesederhana itu: ia menyimpan state yang terkumpul selama aplikasi berjalan. Jika deployment menambahkan kolom, mengubah ribuan nilai, lalu menghapus kolom lama, kembali ke code sebelumnya tidak akan menyusun ulang data yang sudah dibuang. Jadi, pertanyaan praktisnya bukan sekadar, "Bagaimana menjalankan ALTER TABLE?" melainkan, "Bagaimana membuat perubahan ini repeatable, terlihat, dan dapat dipulihkan?"
Database schema migration menawarkan jawaban, tetapi bukan sulap. Ia mengubah perubahan struktur menjadi artefak berurutan yang dapat ditinjau dan diuji. Migration tidak membuat semua statement SQL menjadi transactional, menghapus lock saat deployment, atau menjamin data yang hilang dapat dipulihkan. Untuk aplikasi PHP kecil, memahami batas ini lebih berguna daripada memakai tool besar tanpa memahami tugasnya.
Migration adalah transisi tercatat, bukan snapshot
Schema migration menjelaskan cara memindahkan database dari satu state yang diketahui ke state berikutnya. Isinya dapat membuat tabel, menambahkan index, memperkenalkan kolom nullable, atau melakukan backfill pada row lama. Dokumentasi Doctrine Migrations menjelaskannya sebagai versioning schema database agar perubahan dapat ditinjau dan diuji sebelum deployment production.
Ini berbeda dari hanya menyimpan schema dump untuk instalasi baru. Dump dapat menjelaskan bentuk database hari ini, tetapi tidak menerangkan cara instalasi lama bergerak dari struktur bulan lalu menuju struktur hari ini. Migration menyimpan jalur tersebut.
Migration juga berbeda dari transaction aplikasi biasa. Transaction mengelompokkan write saat runtime, misalnya membuat pesanan beserta itemnya. Schema migration mengubah struktur yang menjadi sandaran write tersebut. Keduanya bersinggungan saat migration mengubah data, tetapi menyelesaikan masalah koordinasi yang berbeda.
Sistem minimum yang andal memiliki dua bagian
Bagian pertama adalah direktori file migration yang disimpan bersama code aplikasi. Setiap file memiliki versi unik dan deskripsi jelas, misalnya:
migrations/
202608310001_add_summary_to_posts.sql
202608310002_backfill_post_summaries.sql
202608310003_require_post_summary.sql
Format nama yang persis tidak sepenting identitas stabil dan urutan deterministik. Script perlu berada di version control karena sebuah release seharusnya membawa code sekaligus perubahan database yang diharapkannya. Dokumentasi Flyway dari Redgate menjelaskan migration sebagai perubahan schema atau data secara incremental yang dijalankan dalam urutan konsisten di berbagai environment.
Bagian kedua adalah migration ledger di dalam database. Ledger minimal dapat mencatat versi, deskripsi, checksum, waktu eksekusi, dan status keberhasilan. Sebelum menerapkan perubahan, runner membandingkan file yang tersedia dengan versi yang tercatat. Runner hanya menerapkan migration yang pending sesuai urutan, lalu mencatat hasilnya.
Ledger ini menjawab pertanyaan yang tidak dapat dijawab oleh folder saja: apa yang sudah diterima database ini? Development, staging, dan production dapat memakai repository aplikasi yang sama sambil berada pada versi migration berbeda. History table membuat perbedaan itu terlihat.
Jangan sembarang menulis ulang riwayat yang sudah di-deploy
Setelah migration berjalan di environment bersama atau production, mengeditnya diam-diam menciptakan dua arti untuk satu versi. Database baru menerima SQL revisi, sedangkan database lama menyimpan efek SQL awal. Checksum dapat mendeteksi perbedaan, tetapi tidak dapat menentukan state mana yang dimaksud.
Pilihan default yang lebih aman adalah menambahkan corrective migration. Riwayat mungkin terlihat kurang rapi, tetapi urutannya jujur tentang apa yang terjadi. Migration lama dapat digabungkan secara sengaja nanti, tetapi itu merupakan maintenance terpisah dengan baseline yang jelas, bukan edit tersembunyi.
Jadikan compatibility sebagai bagian dari deployment
Bayangkan aplikasi PHP ingin menambahkan field summary yang wajib pada post lama. Satu migration yang menambah kolom NOT NULL, menulis ulang semua row, lalu langsung men-deploy code yang mewajibkannya akan memusatkan beberapa risiko pada satu saat.
Perubahan bertahap lebih mudah dipahami:
- Tambahkan
summarysebagai nullable agar aplikasi lama tetap valid. - Deploy code yang menulis summary sambil tetap menerima row lama.
- Lakukan backfill row lama dalam batch terkontrol dan verifikasi hasilnya.
- Alihkan read agar mengandalkan field baru hanya setelah validasi.
- Tambahkan constraint wajib dalam migration berikutnya.
SQL berikut sengaja hanya berupa ilustrasi, bukan resep production:
-- Expand: old code can ignore this nullable column.
ALTER TABLE posts ADD COLUMN summary VARCHAR(500) NULL;
-- A separate, reviewed data migration fills existing rows.
UPDATE posts
SET summary = LEFT(content, 500)
WHERE summary IS NULL;
-- Contract only after the application and data are ready.
ALTER TABLE posts MODIFY summary VARCHAR(500) NOT NULL;
Konten nyata belum tentu dapat diringkas dengan memotong HTML, sehingga UPDATE dalam contoh bukan rekomendasi untuk sistem publishing. Contoh ini justru menampilkan pertanyaan yang harus dijawab saat review migration: apakah transformasinya mempertahankan makna data lama? Kadang tidak ada pemetaan otomatis yang cukup jujur, sehingga desain perlu mengizinkan nilai kosong atau review manual.
Expand and contract memisahkan keputusan yang sulit dibalik
Versi lebih luas dari pendekatan bertahap ini sering disebut expand and contract. Saat expansion, database mendukung struktur lama dan baru. Code aplikasi dapat menulis ke keduanya, data lama di-backfill, dan jalur read baru diuji. Saat contraction, jalur lama baru dihapus setelah tidak diperlukan.
Prisma Data Guide menyajikannya sebagai rangkaian: bangun schema baru di samping schema lama, sesuaikan client, migrasikan data, uji, alihkan read, hentikan write lama, lalu hapus struktur awal. Pramod Sadalage dan Martin Fowler menjelaskan transition phase terkait dalam evolutionary database design, ketika pola akses lama dan baru hidup bersamaan untuk sementara.
Pola ini berguna ketika beberapa proses aplikasi mungkin menjalankan release berbeda selama deployment, atau saat backfill data tidak dapat selesai dengan aman dalam satu maintenance window. Biayanya adalah kompleksitas sementara: kolom ganda, dual write, cabang compatibility, dan pekerjaan cleanup. Situs kecil dengan maintenance window yang dapat diterima mungkin secara masuk akal memilih migration offline yang lebih singkat. "Zero downtime" tidak seharusnya menjadi slogan yang menyembunyikan prosedur lebih rapuh.
Rollback bukan tombol undo universal
Tool migration sering menyediakan cara mendefinisikan operasi kebalikan, tetapi SQL terbalik tidak sama dengan recovery. Menghapus kolom baru yang masih kosong mungkin mudah. Memulihkan kolom yang sudah dihapus beserta semua nilai lamanya tidak mudah. Memecah satu field ambigu menjadi dua dapat menghilangkan informasi yang tidak bisa digabungkan kembali dengan andal. Write baru setelah cutover mungkin juga tidak memiliki representasi dalam schema lama.
Database engine juga memiliki aturan transaction sendiri. MariaDB mendokumentasikan bahwa banyak statement DDL, termasuk ALTER TABLE, menyebabkan implicit commit. Membungkus statement tersebut dengan START TRANSACTION tidak menciptakan batas rollback universal. Engine dan operasi lain dapat berperilaku berbeda, sehingga dokumentasi versi terkait harus diperiksa, bukan disimpulkan dari transaction aplikasi.
Rencana recovery yang realistis dapat menggabungkan beberapa pilihan:
- Jalankan roll-forward aplikasi dengan corrective migration.
- Pertahankan struktur lama sementara agar code dapat dialihkan kembali.
- Restore backup yang telah diuji ketika data dihancurkan atau rusak.
- Hentikan deployment dan lakukan investigasi alih-alih otomatis melanjutkan.
Backup baru menjadi potensi recovery sampai jalur restore-nya diuji. Recovery point dan recovery time yang dapat diterima bergantung pada aplikasinya; situs personal dan sistem pembayaran tidak seharusnya berpura-pura memiliki model risiko yang sama.
Workflow yang hati-hati untuk tim kecil
1. Tentukan starting state
Catat versi schema yang diharapkan migration. Jika production memiliki perubahan manual yang tidak ada di version control, selesaikan drift itu sebelum menambahkan automation. Runner tidak dapat bernalar dengan aman dari baseline fiktif.
2. Jaga agar setiap perubahan tetap sempit
Pisahkan pekerjaan schema additive, data backfill, cutover code, dan cleanup destructive ketika failure mode-nya berbeda. Langkah kecil lebih mudah ditinjau dan didiagnosis, meski membuatnya terlalu kecil tanpa rencana deployment utuh hanya akan memindahkan kompleksitas.
3. Uji jalurnya, bukan hanya schema akhir
Buat database pada release sebelumnya, sertakan edge case yang representatif, terapkan migration pending, lalu jalankan test aplikasi. Instalasi bersih membuktikan schema hari ini dapat dibuat; itu tidak membuktikan data lama dapat mencapainya dengan aman.
4. Tinjau transformasi data secara eksplisit
Tanyakan apa yang terjadi pada null, nilai duplikat, record historis tidak valid, foreign key, dan row yang baru termigrasi sebagian. Buat backfill dapat dimulai ulang jika masuk akal. Script yang hanya berhasil ketika dijalankan tepat sekali tanpa gangguan sulit dipulihkan setelah timeout.
5. Rencanakan backup, lock, dan observation window
Periksa cara versi database yang tepat menjalankan DDL tersebut. Operasi tertentu dapat mengunci tabel sibuk atau membangunnya ulang; dampaknya bergantung pada engine, operasi, tabel, dan workload. Hindari perkiraan durasi yang dibuat-buat. Tentukan sinyal progress atau kegagalan, lalu verifikasi perilaku aplikasi setelah migration, bukan menganggap exit code nol sebagai satu-satunya pengujian.
6. Berikan otoritas migration kepada satu actor
Dua instance aplikasi yang berlomba menerapkan migration pending yang sama dapat menghasilkan kegagalan membingungkan. Deployment job, maintenance command eksplisit, atau runner dengan locking yang sesuai sebaiknya memiliki operasi tersebut. Migration otomatis saat startup bisa nyaman, tetapi juga menghubungkan setiap start proses dengan pekerjaan schema berprivilege. Tradeoff itu perlu diputuskan dengan sengaja.
Hal yang tidak dapat diputuskan migration untukmu
Migration runner dapat mengurutkan script, mencatat history, memvalidasi checksum, dan berhenti saat error. Ia tidak dapat menentukan apakah pemotongan teks mempertahankan makna, apakah pembangunan index dapat diterima saat traffic puncak, atau apakah menyimpan dua representasi menimbulkan masalah privasi. Itu adalah keputusan aplikasi dan operasional.
Framework juga tidak menghapus kebutuhan akan pengetahuan spesifik database. Doctrine, Flyway, atau runner internal kecil dapat mengoordinasikan pekerjaan, tetapi MariaDB tetap menentukan apa yang di-commit dan dikunci oleh DDL-nya. Tool adalah checklist yang memiliki ingatan, bukan pengganti pemahaman database.
Kesimpulan
Untuk aplikasi PHP kecil, praktik migration yang sehat dapat dimulai secara sederhana: file berurutan dalam version control, ledger yang dapat dipercaya, satu otoritas migration, test dari state sebelumnya, dan rencana recovery yang sebanding dengan perubahan. Perubahan additive sering dapat di-deploy sebelum code bergantung padanya; perubahan destructive sebaiknya menunggu sampai reader, writer, dan data lama sudah diperhitungkan.
Perubahan utamanya adalah berhenti memperlakukan schema change sebagai command yang diketik saat deployment dan mulai memperlakukannya sebagai bagian dari riwayat software. Riwayat ini tidak membuat kegagalan mustahil. Ia membuat jalur yang dimaksud dapat diperiksa, diulang, dan dipertanyakan dengan lebih mudah sebelum data production diminta melewatinya.
