Menu

SQLite Migrations: user_version ile Şema Versiyonlama

SQLite şemanızı zaman içinde güvenle nasıl evriltirsiniz? PRAGMA user_version, sıralı migration scriptleri ve transaction'larla geri alınabilir bir akış kuralım.

Bu sayfada çalıştırılabilir editörler var — düzenle, çalıştır ve sonucu anında gör.

Şemalar değişir. Buna göre plan yapın.

Şemanızın ilk hâli asla son hâli olmaz. Yeni sütunlar eklenir, tablolar bölünür, indeksler baştan tasarlanır. Asıl soru şemanızın değişip değişmeyeceği değil; bu değişikliğin eski veritabanı kopyası bulunan her dizüstüde, sunucuda ve kullanıcı cihazında sorunsuz çalışıp çalışmayacağıdır.

İşte sqlite migrations tam olarak bunun için var: veritabanını N sürümünden N+1 sürümüne taşıyan, sıralı ve küçük scriptlerden oluşan bir dizi. Sırasıyla çalıştırırsanız, herhangi bir veritabanı güncel hâle gelir. Bu disiplini bırakırsanız, tüm öğleden sonranızı yiyip bitiren "benim makinemde çalışıyor" hatalarıyla yüzleşirsiniz.

SQLite size bu iş için tek bir yerleşik araç sunar: PRAGMA user_version. Veritabanının sizin için sakladığı, SQLite'ın kendisinin hiç dokunmadığı 32-bit'lik bir tam sayı. Ne anlama geleceğine siz karar veriyorsunuz.

Yepyeni bir veritabanı 0 değeriyle başlar. Az önce uyguladığınız migration numarası neyse, bu değeri ona ayarla. Uygulamayı başlatırken de bu sayıyı okuyup nerede olduğunu anlayabilirsiniz.

Minimal Bir Migration Döngüsü

Mantığı şöyle düşün: her migration, numaralandırılmış bir SQL scriptidir. Uygulamanız önce mevcut user_version değerini okur, ardından bu numaradan büyük olan tüm scriptleri sırayla çalıştırır ve her birinin sonunda user_version değerini günceller.

İşte 1 numaralı migration — ilk şemayı oluşturuyoruz:

İşte dikkat etmeniz gereken iki nokta var. Tüm işlem BEGIN; ... COMMIT; içine sarılmış durumda, yani atomik çalışıyor — eğer CREATE TABLE başarısız olursa, user_version artmaz ve hatayı düzeltip tekrar deneyebilirsiniz. Bir de PRAGMA user_version = 1 ifadesi commit'ten hemen önce, yani en sonda yer alıyor; böylece versiyon ancak diğer her şey başarıyla tamamlandığında değişiyor.

Şimdi diyelim ki tabloya bir created_at sütunu eklemeniz gerekiyor. İşte migration 2:

Veritabanı 0 sürümündeyse iki migration da çalışır. 1 sürümündeyse sadece ikincisi çalışır. 2 sürümündeyse hiçbiri çalışmaz. Sıra, sözleşmenin ta kendisidir.

ALTER TABLE neyi yapar, neyi yapamaz?

SQLite'taki ALTER TABLE bilinçli olarak dar tutulmuştur. Desteklediği işlemler şunlar:

  • ADD COLUMN — varsayılan değeri opsiyonel olan yeni bir sütun ekler.
  • DROP COLUMN — sütunu kaldırır (3.35'ten itibaren).
  • RENAME COLUMN — sütunun adını değiştirir (3.25'ten itibaren).
  • RENAME TO — tablonun kendisini yeniden adlandırır.

Hepsi bu kadar. Bir sütunun tipini değiştiremez, NOT NULL kısıtını ekleyip kaldıramaz, CHECK kısıtını düzenleyemez ya da mevcut bir sütuna FOREIGN KEY ekleyemezsin.

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

SQLite'ın doğrudan yapamadığı bir değişikliğe ihtiyacın olduğunda, resmi tarif "tabloyu baştan kurmak"tır. Biraz uzun ama kesin sonuç veren bir yöntem.

Daha Kapsamlı Değişiklikler İçin Tabloyu Yeniden Oluşturma

Mantık şöyle: istediğiniz yapıda yeni bir tablo oluştur, verileri ona kopyala, eskisini sil ve yenisini onun yerine yeniden adlandır. Hepsi tek bir transaction içinde.

SQLite resmi dokümantasyonunda buna 12 adımlı tarif deniyor ve trigger, view ile foreign key referansları için ek uyarılar veriyor — production şemasında uygulamadan önce bir kez okumanızı tavsiye ederim. Çoğu durumda yukarıdaki dört adımlık versiyon yeterli.

Bir not: Yeniden inşa ettiğiniz tabloya işaret eden foreign key'ler varsa, migration öncesinde PRAGMA foreign_keys = OFF, sonrasında ise PRAGMA foreign_keys = ON çalıştırın. Aksi halde DROP TABLE adımı, işin ortasında referans bütünlüğünü bozabilir.

Migration'ları Uygulama Tarafından Yönetmek

Tutulması gereken bilgi o kadar basit ki bunu kendiniz de yazabilirsiniz. Python'da, standart kütüphaneyle şöyle görünüyor:

Temel kurallar şöyle:

  • Migration'lar 1'den başlayarak ardışık şekilde numaralandırılır. Atlama olmaz, sıralama değişmez.
  • Her migration, PRAGMA user_version = N artırımıyla birlikte tek bir transaction içine sarılır.
  • Bir migration commit edilip yayına alındıktan sonra asla dokunulmaz. Yeni değişiklikler yeni bir migration dosyasına yazılır.

Ekiplerin en sık çiğnediği kural işte bu sonuncusu. Diyelim ki bir arkadaşınızın veritabanı 3 numaralı migration'ı çoktan uygulamış; siz sonradan o migration'ı düzenlerseniz, o veritabanı sizinkiyle sessiz sedasız ve kalıcı olarak senkronizasyondan çıkar.

Denetim İzi Tutmak

user_version size veritabanının hangi noktada olduğunu söyler ama her adımın ne zaman koştuğunu ya da ne yaptığını söylemez. Küçük bir kayıt tablosu bu eksiği kapatır:

Artık her migration için bir satırınız var: ad ve zaman damgasıyla birlikte. "Bu veritabanında kodun beklemediği bir sütun neden var?" diye debug ederken paha biçilmez.

Döngü açısından gerçeğin tek kaynağı hâlâ PRAGMA user_version; tablo ise insanlar için.

Rollback: Transaction'lar size neyi verir, neyi vermez

SQLite'ta DDL transaction'a dahildir. Diyelim 5 numaralı migration tablo oluşturmaya, veri kopyalamaya ve user_version değerini artırmaya başladı; kopyalama yarıda kaldı. ROLLBACK her şeyi geri alır — CREATE TABLE dahil. Veritabanı, BEGIN öncesindeki haline tıpatıp döner.

Yukarıda başarısız migration'ları konuştuk. Peki ya başarıyla commit edilmiş ama sonradan pişman olduğumuz migration'lar? Onlar için ayrı bir down-migration yazarsınız — yani değişikliği geri alan bir script. SQLite'ın otomatik bir geri alma mekanizması yok. 7 numaralı migration bir kolon eklediyse, down versiyonu bu kolonu düşürür. 7 numaralı migration bir kolon düşürdüyse, down versiyonu veriyi geri getiremez; yapabileceği en iyi şey kolonu boş olarak yeniden oluşturmaktır.

Pratikte pek çok küçük proje down-migration'ları tamamen atlar ve "geri alma" işini yedeklere bırakır. Yedeklerinizi düzenli aldığınız sürece bu da geçerli bir tercih.

İleride Başınızı Ağrıtmayacak Birkaç Alışkanlık

  • Her mantıksal değişiklik için tek bir migration. Birbiriyle ilgisiz üç kolonu birden ekleyen bir migration'ı hem incelemek hem de geri almak, üç ayrı migration'a göre çok daha zordur.
  • Migration'ları production'ın bir kopyasında test edin. Şema değişiklikleri büyük tablolarda yavaş olabilir; bunu production'da öğrenmek hiç keyifli değil.
  • Yayına çıkmış bir migration'ı asla düzenlemeyin. Yenisini ekleyin.
  • Önce yedek alın. CLI'da hızlı bir .backup ya da veritabanı kapalıyken alınmış basit bir dosya kopyası, biraz iddialı her migration öncesi ucuz bir sigortadır.
  • PRAGMA foreign_keys'e dikkat edin. Tablo yeniden inşaları sırasında kapatın, iş bitince tekrar açın.

Daha büyük projelerde özel bir araca yönelin — SQLAlchemy ile Alembic, golang-migrate, Knex, Flyway gibi. Sıralama, eşzamanlı çalıştırıcılar ve takım içi konvansiyonlar gibi kendi başınıza yeniden icat edeceğiniz şeyleri sizin için hallediyorlar. Prensipler yukarıdaki döngüyle aynı; araç sadece tekrar eden kalıp kodu ortadan kaldırıyor.

Sırada: WAL Modu ve Eşzamanlılık

Migration'lar genellikle uygulama kapalıyken ya da exclusive lock tutarken çalışır. Geri kalan zamanda veritabanınız aynı anda birden fazla bağlantıdan gelen okuma ve yazma isteklerine cevap veriyor — ve SQLite'ın varsayılan journal modu her zaman bu iş için en uygun seçim değil. Sonraki sayfada WAL modunu, neyi değiştirdiğini ve ne zaman geçiş yapmanız gerektiğini ele alıyoruz.

Sıkça Sorulan Sorular

SQLite şemasını nasıl versiyonlarım?

SQLite her veritabanına özel, 32-bit'lik bir tamsayı alanı sunar: user_version. Bu alana PRAGMA user_version ile erişirsiniz. Uygulama açıldığında bu değeri okuyun, kodunuzun bildiği en güncel migration numarasıyla karşılaştırın ve eksik olanları sırayla çalıştırın. Ekstra bir tabloya gerek yok; yine de pek çok proje denetim kaydı (audit trail) için ayrı bir tablo tutmayı tercih ediyor.

SQLite'ta bir migration'ı geri alabilir miyim?

Her migration'ı BEGIN; ... COMMIT; arasına alın. İçeride bir şey patlarsa ROLLBACK tüm adımı geri alır — şema değişiklikleri ve veri değişiklikleri dahil, çünkü SQLite'ta DDL transaction'a dahildir. Ama zaten commit edilmiş bir migration'ı geri almak istiyorsanız, kendi yazacağınız ayrı bir down-script gerekir; SQLite bunu sizin için otomatik üretmez.

SQLite'ta ALTER TABLE neden bu kadar kısıtlı?

SQLite ALTER TABLE ADD COLUMN, RENAME TABLE, RENAME COLUMN ve DROP COLUMN destekler; ama bir kolonun tipini ya da kısıtlarını değiştirmek gibi keyfi değişikliklere izin vermez. Çözüm, klasik 12 adımlı yöntemdir: istediğiniz şekilde yeni bir tablo oluşturun, INSERT INTO new_table SELECT ... FROM old_table ile veriyi taşıyın, eskiyi silin ve yeniyi eskinin adıyla yeniden adlandırın.

Hazır bir migration aracı mı kullanmalıyım, yoksa kendim mi yazmalıyım?

Küçük projelerde, numaralandırılmış .sql dosyalarını PRAGMA user_version üzerinden döndüren elle yazılmış bir döngü 30 satır kod ya tutar ya tutmaz — fazlasıyla iş görür. Daha büyük projelerde Alembic (Python), golang-migrate (Go) veya Knex (Node) gibi araçlar; sıralama, kilitleme ve takım iş akışları gibi sıfırdan yazmak zorunda kalacağınız konuları zaten halletmiş oluyor.

Coddy programming languages illustration

Coddy ile kodlamayı öğren

BAŞLA