İçeriğe geç
Tüm yazılar

2026'da Room veritabanı migrasyonları: tek bir satır kaybetmeden şema değişikliği göndermek

Android'de Room veritabanı migrasyonları için pratik bir rehber — AutoMigration, elle yazılmış Migration nesneleri ve kullanıcılarınız bulmadan önce bir migrasyonu nasıl test edersiniz.

MFKAPPS 4 dk okuma

Her yerel-öncelikli uygulama er ya da geç 1. sürümle gönderilmeyen bir şemaya ihtiyaç duyar. Yeni bir sütun, yeniden adlandırılmış bir tablo, bir zamanlar String olan ve Int’e dönüşmesi gereken bir alan. Bu olduğu an, dikkatsiz bir anotasyon uzağınızda kullanıcıların bir sonraki güncellemede tüm verilerini silmek var. Room migrasyonları bunu önlemek için var ve insanların bunlarla yaşadığı acının çoğu, isteğe bağlı gibi görünen ama olmayan kısımları atlamaktan kaynaklanıyor.

Tuzak: fallbackToDestructiveMigration()

Room, @Database sürüm numaranız eşleşen bir migrasyon yolu olmadan yükseldiği an bir IllegalStateException fırlatır. Her Stack Overflow cevabında karşınıza çıkan düzeltme tek satırdır:

Room.databaseBuilder(context, AppDatabase::class.java, "app.db")
    .fallbackToDestructiveMigration()
    .build()

Bu derlenir, gönderilir ve mükemmel çalışır — debug derlemelerinde, kendi cihazınızda, test verisini kaybetmeyi umursamadığınız yerde. Üretimde şu anlama gelir: bir sonraki sefer şema sürümünü yükselttiğinizde, Room her tabloyu siler ve boş olarak yeniden oluşturur. Granyn için bu, birinin iki yıllık bütçe kaydının, istemediği bir uygulama güncellemesinde sessizce yok olması demek. Ne bir uyarı diyaloğu, ne de geri alma var. Uygulama bir sonraki açıldığında sessizce oluyor. Bu satır debug dışı herhangi bir yapılandırmada varsa, onu bir kolaylık değil, bir hata olarak ele alın.

AutoMigration, insanların sandığından fazlasını kapsıyor

Yaygın durumlarda — varsayılan değerli bir sütun eklemek, bir tablo eklemek veya kaldırmak, bir sütunu yeniden adlandırmak — Room’un @AutoMigration’ı, iki şema anlık görüntüsünden migrasyonu sizin için üretebilir:

@Database(
    version = 2,
    entities = [Entry::class],
    autoMigrations = [
        AutoMigration(from = 1, to = 2)
    ]
)
abstract class AppDatabase : RoomDatabase()

Bu yalnızca şema dışa aktarmayı açtıysanız çalışır (Gradle yapılandırmanızda room.schemaLocation), böylece Room’un karşılaştıracağı her sürüm için bir JSON anlık görüntüsü olur. Bu kurulumu atlarsanız AutoMigration’ın karşılaştıracağı hiçbir şey olmaz ve bunu derleme zamanında öğrenirsiniz, gece yarısı bir çökme raporunda değil. Yeniden adlandırılmış veya silinmiş bir sütun için de Room’a eski ve yeni adları gösteren küçük bir @RenameColumn veya @DeleteColumn sınıfına ihtiyacınız var — niyeti yalnızca bir karşılaştırmadan çıkaramaz.

AutoMigration’ın yetersiz kaldığı yer

Var olan veriyi yeniden şekillendirmesi gereken her şey — bir sütunu ikiye bölmek, saklanan bir String tutarı sente dönüştürmek, yeni bir zorunlu alanı diğer satırlardan geriye doldurmak — AutoMigration’ın kapsamı dışındadır. Bu, etrafından dolaşılacak bir kısıtlama değil; gerçek SQL içeren gerçek bir Migration nesnesine ihtiyacınız olduğunun işaretidir.

Migrasyonu elle yazmak

Elle yazılmış bir Migration, sorumlu olduğunuz bir SQL bloğuyla birlikte bir sürüm çiftidir:

val MIGRATION_2_3 = object : Migration(2, 3) {
    override fun migrate(db: SupportSQLiteDatabase) {
        db.execSQL(
            "ALTER TABLE entries ADD COLUMN currency TEXT NOT NULL DEFAULT 'USD'"
        )
        db.execSQL(
            "UPDATE entries SET currency = (SELECT default_currency FROM user_prefs LIMIT 1)"
        )
    }
}

Bunu veritabanı builder’ında .addMigrations(MIGRATION_2_3) ile varsa AutoMigration’ların yanına kaydedin. SQLite’ın ALTER TABLE’ı sınırlıdır — sütun silme yok, yerinde sütun tipi değiştirme yok — bu yüzden bir sütun eklemenin ötesindeki her şey genellikle klasik üç adımlı dansı gerektirir: istediğiniz şekilde yeni bir tablo oluşturun, veriyi INSERT INTO ... SELECT ile karşıya kopyalayın, eski tabloyu silin, yenisini yeniden adlandırın. Yazmak isteyeceğinizden daha fazla SQL ve tam olarak doğru olması gereken SQL bu, çünkü her cihazda bir sonraki açılışta sessizce bir kez çalışıyor.

Migrasyonu test edin, sadece hedef şemayı değil

Burada en çok hataya yol açan yanlış SQL değil — migrasyonu gerçek, eskimiş bir veritabanına karşı hiç çalıştırmamak. Room’un MigrationTestHelper’ı tam da bunun için var:

@get:Rule
val helper = MigrationTestHelper(
    InstrumentationRegistry.getInstrumentation(),
    AppDatabase::class.java
)

@Test
fun migrate2To3_preservesExistingRows() {
    helper.createDatabase(TEST_DB, 2).apply {
        execSQL("INSERT INTO entries (id, amount) VALUES (1, 4200)")
        close()
    }
    helper.runMigrationsAndValidate(TEST_DB, 3, true, MIGRATION_2_3)
}

Bu, sürüm-2 bir veritabanı oluşturur, gerçek bir kullanıcının cihazının görüneceği şekilde tohumlar, sonra migrasyonunuzu buna karşı çalıştırır ve sonuçtaki şemanın Room’un beklediğiyle eşleştiğini doğrular. Üretimde gerçekten yaşanan iki hata modunu yakalar: boş bir veritabanında çalışan ama satırları olan birinde fırlatan bir migrasyon ve SQL’i sorunsuz ama sonuçtaki şeması entity tanımlarınızla eşleşmeyen bir migrasyon (Room bunu sütun sırası ve varsayılan değerlere kadar sıkı biçimde kontrol eder).

Kontrol listesi

Herhangi bir şema sürümü yükseltmesi gönderilmeden önce: şema dışa aktarma açık, her sürüm sıçraması ya bir AutoMigration’a ya da kayıtlı elle yazılmış bir Migration’a sahip, fallbackToDestructiveMigration() test kodu dışında hiçbir yerde görünmüyor ve en az bir MigrationTestHelper testi gerçek satırlar tohumlayıp hayatta kaldıklarını doğruluyor. Bunların hiçbiri heyecan verici bir iş değil. Ama kimsenin fark etmediği bir şema değişikliği ile bir sürümden sonraki sabah “verim kayboldu” mesajlarıyla dolu bir destek kutusu arasındaki fark da bu.