Menu

Migrasi SQLite: Versi Skema dengan user_version

Cara mengubah skema SQLite dengan aman dari waktu ke waktu memakai PRAGMA user_version, script migrasi yang berurutan, dan transaksi agar semuanya bisa dibatalkan.

Halaman ini dilengkapi editor yang bisa dijalankan. Edit, jalankan, dan lihat output-nya seketika.

Skema Pasti Berubah. Bersiaplah.

Versi pertama skemamu tidak akan pernah menjadi yang terakhir. Kolom ditambahkan, tabel dipecah, index dipikirkan ulang. Pertanyaannya bukan apakah skemamu akan berubah, melainkan apakah perubahan itu akan diterapkan dengan mulus di setiap laptop, server, dan perangkat pengguna yang sudah punya salinan database versi lama.

Untuk itulah migrasi ada: rangkaian script kecil yang berurutan, yang membawa database dari versi N ke versi N+1. Jalankan secara berurutan, dan database mana pun akan menyusul ke versi terkini. Abaikan disiplin ini, dan kamu akan berakhir dengan bug "di komputerku jalan kok" yang butuh seharian untuk dilacak.

SQLite memberimu tepat satu tool bawaan untuk ini: PRAGMA user_version. Ini adalah integer 32-bit yang disimpan database untukmu dan tidak pernah disentuh oleh SQLite sendiri. Kamulah yang menentukan artinya.

Database baru dimulai dari 0. Atur nilainya ke nomor migrasi yang baru saja kamu terapkan. Baca nilainya saat aplikasi mulai untuk tahu posisimu sekarang.

Perulangan Migrasi yang Minimal

Model berpikirnya: setiap migrasi adalah script SQL bernomor. Aplikasimu membaca user_version saat ini, menjalankan setiap script dengan nomor yang lebih tinggi secara berurutan, dan memperbarui user_version setelah masing-masing script.

Ini migrasi 1, yang membuat skema awal:

Ada dua hal yang perlu diperhatikan. Semuanya dibungkus dengan BEGIN; ... COMMIT; sehingga atomik: jika CREATE TABLE gagal, user_version tidak dinaikkan dan kamu bisa memperbaikinya lalu mencoba lagi. Dan PRAGMA user_version = 1 adalah pernyataan terakhir sebelum commit, jadi versinya hanya berubah jika semua yang lain berhasil.

Sekarang misalkan kamu perlu menambahkan kolom created_at. Itulah migrasi 2:

Database di versi 0 menjalankan keduanya. Database di versi 1 hanya menjalankan yang kedua. Database di versi 2 tidak menjalankan apa pun. Urutannya adalah kontraknya.

Yang Bisa dan Tidak Bisa Dilakukan ALTER TABLE

ALTER TABLE di SQLite sengaja dibuat sempit. Yang didukung:

  • ADD COLUMN: menambahkan kolom baru di akhir, dengan nilai default opsional.
  • DROP COLUMN: menghapus kolom (sejak 3.35).
  • RENAME COLUMN: mengganti nama kolom (sejak 3.25).
  • RENAME TO: mengganti nama tabelnya sendiri.

Hanya itu. Kamu tidak bisa mengganti tipe kolom, mengubah NOT NULL, mengubah constraint CHECK, atau menambahkan FOREIGN KEY ke kolom yang sudah ada.

-- Not supported:
ALTER TABLE users ALTER COLUMN email TYPE VARCHAR(255);
ALTER TABLE users ADD CONSTRAINT users_email_check CHECK (email LIKE '%@%');

Jika kamu butuh perubahan yang tidak bisa dilakukan SQLite secara langsung, resep resminya adalah "bangun ulang tabelnya." Caranya lebih panjang, tapi sepenuhnya bisa diandalkan.

Membangun Ulang Tabel untuk Perubahan yang Lebih Besar

Polanya: buat tabel baru dengan bentuk yang kamu inginkan, salin datanya, hapus tabel lama, lalu ganti nama tabel baru agar menempati posisinya. Semuanya di dalam satu transaksi.

Dokumentasi lengkap SQLite menyebut ini resep 12 langkah dan menambahkan beberapa peringatan untuk trigger, view, dan referensi foreign key. Resep itu layak dibaca sekali sebelum kamu melakukannya di skema produksi. Untuk sebagian besar kasus, versi empat langkah di atas sudah cukup.

Satu peringatan: jika ada foreign key yang merujuk ke tabel yang sedang kamu bangun ulang, jalankan PRAGMA foreign_keys = OFF sebelum migrasi dan PRAGMA foreign_keys = ON sesudahnya. Kalau tidak, DROP TABLE bisa merusak integritas referensial di tengah jalan.

Menjalankan Migrasi dari Aplikasimu

Pencatatannya cukup sederhana sehingga kamu bisa menulisnya sendiri. Di Python dengan library standar:

Aturan yang harus selalu berlaku:

  • Migrasi diberi nomor berurutan mulai dari 1. Tidak ada nomor yang terlewat, tidak ada urutan yang diubah.
  • Setiap migrasi dibungkus dalam transaksi bersama kenaikan PRAGMA user_version = N.
  • Begitu sebuah migrasi sudah di-commit dan dirilis, kamu tidak pernah mengeditnya lagi. Perubahan baru masuk ke migrasi baru.

Aturan terakhir itulah yang paling sering dilanggar tim. Jika kamu mengedit migrasi 3 setelah database rekanmu sudah menerapkannya, database mereka akan selamanya tidak sinkron dengan milikmu tanpa ada yang sadar.

Mencatat Jejak Audit

user_version memberitahumu di mana posisi sebuah database. Tapi nilai itu tidak memberitahumu kapan setiap langkah dijalankan atau apa yang dilakukannya. Tabel pencatatan kecil bisa mengatasinya:

Sekarang kamu punya satu baris per migrasi beserta nama dan timestamp-nya, yang berguna saat men-debug pertanyaan "kenapa database ini punya kolom yang tidak diharapkan kode?"

PRAGMA user_version tetap menjadi sumber kebenaran bagi perulangannya; tabel itu untuk dibaca manusia.

Rollback: Apa yang Diberikan Transaksi, dan Apa yang Tidak

DDL di SQLite bersifat transaksional. Jika migrasi 5 mulai membuat tabel, menyalin data, dan menaikkan user_version, lalu proses salin gagal di tengah jalan, ROLLBACK membatalkan semuanya, termasuk CREATE TABLE. Database kembali persis seperti sebelum BEGIN.

Itu mencakup migrasi yang gagal. Tapi itu tidak mencakup migrasi yang sudah berhasil di-commit dan sekarang kamu sesali. Untuk yang seperti itu, kamu menulis down-migration terpisah, yaitu script yang membatalkan perubahannya. SQLite tidak punya pembalik otomatis. Jika migrasi 7 menambahkan kolom, versi down-nya menghapus kolom itu. Jika migrasi 7 menghapus kolom, versi down-nya tidak bisa mengembalikan datanya; paling banter hanya bisa membuat ulang kolomnya dalam keadaan kosong.

Dalam praktiknya, banyak proyek kecil sama sekali tidak memakai down-migration dan mengandalkan backup untuk "undo." Itu pilihan yang sah, asalkan kamu memang membuat backup-nya.

Beberapa Kebiasaan yang Menghindarkanmu dari Masalah

  • Satu migrasi untuk satu perubahan logis. Migrasi yang menambahkan tiga kolom yang tidak berkaitan lebih sulit di-review dan lebih sulit dibatalkan daripada tiga migrasi terpisah.
  • Uji migrasi pada salinan data produksi. Perubahan skema bisa lambat di tabel besar, dan mengetahuinya saat sudah di produksi sama sekali tidak menyenangkan.
  • Jangan pernah mengedit migrasi yang sudah dirilis. Tambahkan migrasi baru.
  • Backup dulu. .backup singkat di CLI atau menyalin file saat database sedang tertutup adalah asuransi murah sebelum migrasi apa pun yang tidak sepele.
  • Waspadai PRAGMA foreign_keys. Matikan saat membangun ulang tabel, lalu nyalakan lagi sesudahnya.

Untuk proyek yang lebih besar, pakai tool khusus: Alembic dengan SQLAlchemy, golang-migrate, Knex, Flyway. Tool-tool itu menangani urutan, beberapa proses yang berjalan bersamaan, dan konvensi tim yang kalau tidak harus kamu buat ulang sendiri. Prinsipnya sama dengan perulangan di atas; tool itu hanya menghilangkan kode berulangnya.

Selanjutnya: WAL Mode dan Konkurensi

Migrasi biasanya dijalankan saat aplikasi sedang offline atau memegang kunci eksklusif. Di luar waktu itu, database-mu melayani baca dan tulis dari beberapa koneksi sekaligus, dan journal mode bawaan SQLite tidak selalu menjadi pilihan terbaik. Halaman berikutnya membahas WAL mode, apa yang diubahnya, dan kapan sebaiknya beralih.

Pertanyaan yang sering diajukan

Bagaimana cara memberi versi pada skema SQLite?

SQLite punya satu slot integer 32-bit bawaan di setiap database yang disebut user_version, yang diakses lewat PRAGMA user_version. Baca nilainya saat aplikasi mulai, bandingkan dengan nomor migrasi terbaru yang diketahui kodemu, lalu jalankan migrasi yang belum ada secara berurutan. Tidak perlu tabel tambahan, meskipun banyak aplikasi menambahkannya sebagai jejak audit.

Bisakah migrasi SQLite di-rollback?

Bungkus setiap migrasi dengan BEGIN; ... COMMIT;. Jika ada yang gagal di dalamnya, ROLLBACK membatalkan seluruh langkah itu, baik perubahan skema maupun perubahan data, karena DDL di SQLite bersifat transaksional. Untuk membatalkan migrasi yang sudah di-commit, kamu butuh down-script terpisah yang kamu tulis sendiri; SQLite tidak akan membuatkannya untukmu.

Kenapa ALTER TABLE terbatas di SQLite?

SQLite mendukung ALTER TABLE ADD COLUMN, RENAME TABLE, RENAME COLUMN, dan DROP COLUMN, tapi tidak mendukung perubahan sembarang seperti mengganti tipe atau constraint sebuah kolom. Solusinya adalah resep 12 langkah: buat tabel baru dengan bentuk yang diinginkan, INSERT INTO new_table SELECT ... FROM old_table, hapus tabel lama, lalu ganti nama tabel baru.

Sebaiknya pakai tool migrasi atau menulis sendiri?

Untuk aplikasi kecil, perulangan buatan sendiri atas file .sql bernomor yang dikendalikan oleh PRAGMA user_version kira-kira hanya 30 baris kode dan sudah bekerja dengan baik. Untuk proyek yang lebih besar, tool seperti Alembic (Python), golang-migrate (Go), atau Knex (Node) menangani urutan, penguncian, dan alur kerja tim yang kalau tidak harus kamu buat ulang sendiri.

Ilustrasi bahasa pemrograman di Coddy

Belajar coding dengan Coddy

Mulai