Uygulamanızda özellik ekleyip değiştirirken Room varlık sınıflarınızı ve temel veritabanı tablolarınızı bu değişiklikleri yansıtacak şekilde değiştirmeniz gerekir. Bir uygulama güncellemesi veritabanı şemasını değiştirdiğinde, cihaz üzerindeki veritabanında bulunan kullanıcı verilerinin korunması önemlidir.
Room, artımlı taşıma için hem otomatik hem de manuel seçenekleri destekler. Otomatik taşıma işlemleri, temel şema değişikliklerinin çoğunda işe yarar ancak daha karmaşık değişiklikler için taşıma yollarını manuel olarak tanımlamanız gerekebilir.
Otomatik taşıma işlemleri
İki veritabanı sürümü arasında otomatik taşıma işlemini beyan etmek için @Database
dosyasında autoMigrations
mülküne @AutoMigration
ek açıklama ekleyin:
Kotlin
// Database class before the version update. @Database( version = 1, entities = [User::class] ) abstract class AppDatabase : RoomDatabase() { ... } // Database class after the version update. @Database( version = 2, entities = [User::class], autoMigrations = [ AutoMigration (from = 1, to = 2) ] ) abstract class AppDatabase : RoomDatabase() { ... }
Java
// Database class before the version update. @Database( version = 1, entities = {User.class} ) public abstract class AppDatabase extends RoomDatabase { ... } // Database class after the version update. @Database( version = 2, entities = {User.class}, autoMigrations = { @AutoMigration (from = 1, to = 2) } ) public abstract class AppDatabase extends RoomDatabase { ... }
Otomatik taşıma özellikleri
Room, belirsiz şema değişiklikleri algılarsa ve daha fazla giriş olmadan bir taşıma planı oluşturamazsa derleme zamanında hata verir ve bir AutoMigrationSpec
uygulamanızı ister.
Bu durum genellikle taşıma işlemi aşağıdakilerden birini içerdiğinde ortaya çıkar:
- Tabloları silme veya yeniden adlandırma
- Sütunları silme veya yeniden adlandırma
Room'a taşıma yollarını doğru şekilde oluşturması için gereken ek bilgileri vermek üzere AutoMigrationSpec
öğesini kullanabilirsiniz. RoomDatabase
sınıfınızda AutoMigrationSpec
sınıfını uygulayan statik bir sınıf tanımlayın ve aşağıdakilerden bir veya daha fazlasıyla ek açıklama ekleyin:
Otomatik taşıma için AutoMigrationSpec
uygulamasını kullanmak istiyorsanız ilgili @AutoMigration
ek açıklamalarında spec
mülkünü ayarlayın:
Kotlin
@Database( version = 2, entities = [User::class], autoMigrations = [ AutoMigration ( from = 1, to = 2, spec = AppDatabase.MyAutoMigration::class ) ] ) abstract class AppDatabase : RoomDatabase() { @RenameTable(fromTableName = "User", toTableName = "AppUser") class MyAutoMigration : AutoMigrationSpec ... }
Java
@Database( version = 2, entities = {AppUser.class}, autoMigrations = { @AutoMigration ( from = 1, to = 2, spec = AppDatabase.MyAutoMigration.class ) } ) public abstract class AppDatabase extends RoomDatabase { @RenameTable(fromTableName = "User", toTableName = "AppUser") static class MyAutoMigration implements AutoMigrationSpec { } ... }
Otomatik taşıma işlemi tamamlandıktan sonra uygulamanızın daha fazla işlem yapması gerekiyorsa onPostMigrate()
'i uygulayabilirsiniz.
Bu yöntemi AutoMigrationSpec
dosyanızda uygularsanız Room, otomatik taşıma işlemi tamamlandıktan sonra bu yöntemi çağırır.
Manuel taşıma işlemleri
Taşıma işleminin karmaşık şema değişiklikleri içerdiği durumlarda Room, uygun bir taşıma yolunu otomatik olarak oluşturamayabilir. Örneğin, bir tablodaki verileri iki tabloya bölme kararı alırsanız Oda, bu bölme işleminin nasıl yapılacağını bilemez. Bu gibi durumlarda, Migration
sınıfı uygulayarak taşıma yolunu manuel olarak tanımlamanız gerekir.
Migration
sınıfı, Migration.migrate()
yöntemini geçersiz kılarak startVersion
ile endVersion
arasında bir taşıma yolunu açıkça tanımlar. Migration
sınıflarınızı, addMigrations()
yöntemini kullanarak veritabanı oluşturucunuza ekleyin:
Kotlin
val MIGRATION_1_2 = object : Migration(1, 2) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL("CREATE TABLE `Fruit` (`id` INTEGER, `name` TEXT, " + "PRIMARY KEY(`id`))") } } val MIGRATION_2_3 = object : Migration(2, 3) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL("ALTER TABLE Book ADD COLUMN pub_year INTEGER") } } Room.databaseBuilder(applicationContext, MyDb::class.java, "database-name") .addMigrations(MIGRATION_1_2, MIGRATION_2_3).build()
Java
static final Migration MIGRATION_1_2 = new Migration(1, 2) { @Override public void migrate(SupportSQLiteDatabase database) { database.execSQL("CREATE TABLE `Fruit` (`id` INTEGER, " + "`name` TEXT, PRIMARY KEY(`id`))"); } }; static final Migration MIGRATION_2_3 = new Migration(2, 3) { @Override public void migrate(SupportSQLiteDatabase database) { database.execSQL("ALTER TABLE Book " + " ADD COLUMN pub_year INTEGER"); } }; Room.databaseBuilder(getApplicationContext(), MyDb.class, "database-name") .addMigrations(MIGRATION_1_2, MIGRATION_2_3).build();
Taşıma yollarınızı tanımlarken bazı sürümler için otomatik taşımaları, diğerleri için manuel taşımaları kullanabilirsiniz. Aynı sürüm için hem otomatik taşıma hem de manuel taşıma işlemi tanımlarsanız Room, manuel taşıma işlemini kullanır.
Taşıma işlemlerini test etme
Taşıma işlemleri genellikle karmaşıktır ve yanlış tanımlanmış bir taşıma işlemi, uygulamanızın kilitlenmesine neden olabilir. Uygulamanızın kararlılığını korumak için taşıma işlemlerinizi test edin. Room, hem otomatik hem de manuel taşıma işlemlerinin test sürecine yardımcı olmak için bir room-testing
Maven yapısı sağlar. Bu yapının çalışması için önce veritabanınızın şemasını dışa aktarmanız gerekir.
Şemaları dışa aktarma
Room, derleme sırasında veritabanınızın şema bilgilerini JSON dosyasına aktarabilir. Dışa aktarılan JSON dosyaları, veritabanınızın şema geçmişini temsil eder. Room'un test amacıyla veritabanının daha eski sürümlerini oluşturabilmesi ve otomatik taşıma oluşturmayı etkinleştirebilmesi için bu dosyaları sürüm kontrol sisteminizde saklayın.
Room Gradle eklentisini kullanarak şema konumunu ayarlama
Room 2.6.0 veya sonraki bir sürümü kullanıyorsanız Room Gradle eklentisini uygulayabilir ve şema dizinini belirtmek için room
uzantısını kullanabilirsiniz.
Groovy
plugins {
id 'androidx.room'
}
room {
schemaDirectory "$projectDir/schemas"
}
Kotlin
plugins {
id("androidx.room")
}
room {
schemaDirectory("$projectDir/schemas")
}
Veritabanı şemanız varyant, lezzet veya derleme türüne göre farklıysa schemaDirectory()
yapılandırmasını birden çok kez kullanarak farklı konumlar belirtmeniz gerekir. Bu yapılandırmanın her biri ilk bağımsız değişken olarak bir variantMatchName
içermelidir. Her yapılandırma, varyant adıyla basit bir karşılaştırmaya göre bir veya daha fazla varyantla eşleşebilir.
Bu bilgilerin kapsamlı olduğundan ve tüm varyantları kapsadığından emin olun. Diğer yapılandırmalardan hiçbiriyle eşleşmeyen varyantları işlemek için variantMatchName
olmadan bir schemaDirectory()
de dahil edebilirsiniz. Örneğin, iki derleme çeşidi demo
ve full
ve iki derleme türü debug
ve release
içeren bir uygulamada aşağıdaki yapılandırmalar geçerlidir:
Groovy
room {
// Applies to 'demoDebug' only
schemaDirectory "demoDebug", "$projectDir/schemas/demoDebug"
// Applies to 'demoDebug' and 'demoRelease'
schemaDirectory "demo", "$projectDir/schemas/demo"
// Applies to 'demoDebug' and 'fullDebug'
schemaDirectory "debug", "$projectDir/schemas/debug"
// Applies to variants that aren't matched by other configurations.
schemaDirectory "$projectDir/schemas"
}
Kotlin
room {
// Applies to 'demoDebug' only
schemaDirectory("demoDebug", "$projectDir/schemas/demoDebug")
// Applies to 'demoDebug' and 'demoRelease'
schemaDirectory("demo", "$projectDir/schemas/demo")
// Applies to 'demoDebug' and 'fullDebug'
schemaDirectory("debug", "$projectDir/schemas/debug")
// Applies to variants that aren't matched by other configurations.
schemaDirectory("$projectDir/schemas")
}
Not oluşturma işlemcisi seçeneğini kullanarak şema konumunu ayarlama
Room'un 2.5.2 veya daha eski bir sürümünü kullanıyorsanız ya da Room Gradle eklentisini kullanmıyorsanız room.schemaLocation
notlandırma işleyici seçeneğini kullanarak şema konumunu ayarlayın.
Bu dizindeki dosyalar, bazı Gradle görevleri için giriş ve çıkış olarak kullanılır.
Artımlı ve önbelleğe alınmış derlemelerin doğruluğu ve performansı için Gradle'i bu dizin hakkında bilgilendirmek üzere Gradle'in CommandLineArgumentProvider
özelliğini kullanmanız gerekir.
Öncelikle, aşağıda gösterilen RoomSchemaArgProvider
sınıfını modülünüzün Gradle derleme dosyasına kopyalayın. Örnek sınıftaki asArguments()
yöntemi, room.schemaLocation=${schemaDir.path}
değerini KSP
değerine iletir. KAPT
ve javac
kullanıyorsanız bu değeri -Aroom.schemaLocation=${schemaDir.path}
olarak değiştirin.
Groovy
class RoomSchemaArgProvider implements CommandLineArgumentProvider {
@InputDirectory
@PathSensitive(PathSensitivity.RELATIVE)
File schemaDir
RoomSchemaArgProvider(File schemaDir) {
this.schemaDir = schemaDir
}
@Override
Iterable<String> asArguments() {
// Note: If you're using KAPT and javac, change the line below to
// return ["-Aroom.schemaLocation=${schemaDir.path}".toString()].
return ["room.schemaLocation=${schemaDir.path}".toString()]
}
}
Kotlin
class RoomSchemaArgProvider(
@get:InputDirectory
@get:PathSensitive(PathSensitivity.RELATIVE)
val schemaDir: File
) : CommandLineArgumentProvider {
override fun asArguments(): Iterable<String> {
// Note: If you're using KAPT and javac, change the line below to
// return listOf("-Aroom.schemaLocation=${schemaDir.path}").
return listOf("room.schemaLocation=${schemaDir.path}")
}
}
Ardından, derleme seçeneklerini RoomSchemaArgProvider
'yi belirtilen şema dizini ile kullanacak şekilde yapılandırın:
Groovy
// For KSP, configure using KSP extension:
ksp {
arg(new RoomSchemaArgProvider(new File(projectDir, "schemas")))
}
// For javac or KAPT, configure using android DSL:
android {
...
defaultConfig {
javaCompileOptions {
annotationProcessorOptions {
compilerArgumentProviders(
new RoomSchemaArgProvider(new File(projectDir, "schemas"))
)
}
}
}
}
Kotlin
// For KSP, configure using KSP extension:
ksp {
arg(RoomSchemaArgProvider(File(projectDir, "schemas")))
}
// For javac or KAPT, configure using android DSL:
android {
...
defaultConfig {
javaCompileOptions {
annotationProcessorOptions {
compilerArgumentProviders(
RoomSchemaArgProvider(File(projectDir, "schemas"))
)
}
}
}
}
Tek bir taşıma işlemini test etme
Taşıma işlemlerinizi test edebilmek için Room'dan androidx.room:room-testing
Maven yapısını test bağımlılıklarınıza ve dışa aktarılan şemanın konumunu öğe klasörü olarak ekleyin:
build.gradle
Groovy
android { ... sourceSets { // Adds exported schema location as test app assets. androidTest.assets.srcDirs += files("$projectDir/schemas".toString()) } } dependencies { ... androidTestImplementation "androidx.room:room-testing:2.6.1" }
Kotlin
android { ... sourceSets { // Adds exported schema location as test app assets. getByName("androidTest").assets.srcDir("$projectDir/schemas") } } dependencies { ... testImplementation("androidx.room:room-testing:2.6.1") }
Test paketi, dışa aktarılan şema dosyalarını okuyabilen bir MigrationTestHelper
sınıfı sağlar. Paket, oluşturulan veritabanlarını yönetebilmek için JUnit4 TestRule
arayüzünü de uygular.
Aşağıdaki örnekte tek bir taşıma işlemi için bir test gösterilmektedir:
Kotlin
@RunWith(AndroidJUnit4::class) class MigrationTest { private val TEST_DB = "migration-test" @get:Rule val helper: MigrationTestHelper = MigrationTestHelper( InstrumentationRegistry.getInstrumentation(), MigrationDb::class.java.canonicalName, FrameworkSQLiteOpenHelperFactory() ) @Test @Throws(IOException::class) fun migrate1To2() { var db = helper.createDatabase(TEST_DB, 1).apply { // Database has schema version 1. Insert some data using SQL queries. // You can't use DAO classes because they expect the latest schema. execSQL(...) // Prepare for the next version. close() } // Re-open the database with version 2 and provide // MIGRATION_1_2 as the migration process. db = helper.runMigrationsAndValidate(TEST_DB, 2, true, MIGRATION_1_2) // MigrationTestHelper automatically verifies the schema changes, // but you need to validate that the data was migrated properly. } }
Java
@RunWith(AndroidJUnit4.class) public class MigrationTest { private static final String TEST_DB = "migration-test"; @Rule public MigrationTestHelper helper; public MigrationTest() { helper = new MigrationTestHelper(InstrumentationRegistry.getInstrumentation(), MigrationDb.class.getCanonicalName(), new FrameworkSQLiteOpenHelperFactory()); } @Test public void migrate1To2() throws IOException { SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 1); // Database has schema version 1. Insert some data using SQL queries. // You can't use DAO classes because they expect the latest schema. db.execSQL(...); // Prepare for the next version. db.close(); // Re-open the database with version 2 and provide // MIGRATION_1_2 as the migration process. db = helper.runMigrationsAndValidate(TEST_DB, 2, true, MIGRATION_1_2); // MigrationTestHelper automatically verifies the schema changes, // but you need to validate that the data was migrated properly. } }
Tüm taşıma işlemlerini test etme
Tek bir artımlı taşıma işlemini test etmek mümkün olsa da uygulamanızın veritabanı için tanımlanan tüm taşıma işlemlerini kapsayan bir test eklemenizi öneririz. Bu sayede, yakın zamanda oluşturulan bir veritabanı örneği ile tanımlanmış taşıma yollarını izleyen eski bir örnek arasında tutarsızlık olmaması sağlanır.
Aşağıdaki örnekte, tanımlanan tüm taşıma işlemleri için bir test gösterilmektedir:
Kotlin
@RunWith(AndroidJUnit4::class) class MigrationTest { private val TEST_DB = "migration-test" // Array of all migrations. private val ALL_MIGRATIONS = arrayOf( MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4) @get:Rule val helper: MigrationTestHelper = MigrationTestHelper( InstrumentationRegistry.getInstrumentation(), AppDatabase::class.java.canonicalName, FrameworkSQLiteOpenHelperFactory() ) @Test @Throws(IOException::class) fun migrateAll() { // Create earliest version of the database. helper.createDatabase(TEST_DB, 1).apply { close() } // Open latest version of the database. Room validates the schema // once all migrations execute. Room.databaseBuilder( InstrumentationRegistry.getInstrumentation().targetContext, AppDatabase::class.java, TEST_DB ).addMigrations(*ALL_MIGRATIONS).build().apply { openHelper.writableDatabase.close() } } }
Java
@RunWith(AndroidJUnit4.class) public class MigrationTest { private static final String TEST_DB = "migration-test"; @Rule public MigrationTestHelper helper; public MigrationTest() { helper = new MigrationTestHelper(InstrumentationRegistry.getInstrumentation(), AppDatabase.class.getCanonicalName(), new FrameworkSQLiteOpenHelperFactory()); } @Test public void migrateAll() throws IOException { // Create earliest version of the database. SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 1); db.close(); // Open latest version of the database. Room validates the schema // once all migrations execute. AppDatabase appDb = Room.databaseBuilder( InstrumentationRegistry.getInstrumentation().getTargetContext(), AppDatabase.class, TEST_DB) .addMigrations(ALL_MIGRATIONS).build(); appDb.getOpenHelper().getWritableDatabase(); appDb.close(); } // Array of all migrations. private static final Migration[] ALL_MIGRATIONS = new Migration[]{ MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4}; }
Eksik taşıma yollarını sorunsuz bir şekilde ele alma
Room, cihazdaki mevcut bir veritabanını mevcut sürüme yükseltecek bir taşıma yolu bulamıyorsa IllegalStateException
oluşur. Taşıma yolu eksik olduğunda mevcut verilerin kaybedilmesinin kabul edilebilir olduğu durumlarda, veri tabanını oluştururken fallbackToDestructiveMigration()
oluşturucu yöntemini çağırın:
Kotlin
Room.databaseBuilder(applicationContext, MyDb::class.java, "database-name") .fallbackToDestructiveMigration() .build()
Java
Room.databaseBuilder(getApplicationContext(), MyDb.class, "database-name") .fallbackToDestructiveMigration() .build();
Bu yöntem, artımlı taşıma işlemi gerçekleştirmesi gerektiğinde ve tanımlanmış bir taşıma yolu olmadığında Room'a uygulamanızın veritabanındaki tabloları yıkıcı bir şekilde yeniden oluşturmasını söyler.
Room'un yalnızca belirli durumlarda yıkıcı yeniden oluşturmaya başvurmasını istiyorsanız fallbackToDestructiveMigration()
için birkaç alternatif vardır:
- Şema geçmişinizin belirli sürümleri, taşıma yollarıyla çözemediğiniz hatalara neden oluyorsa bunun yerine
fallbackToDestructiveMigrationFrom()
seçeneğini kullanın. Bu yöntem, Room'un yalnızca belirli sürümlerden taşınırken yıkıcı yeniden oluşturmaya geri dönmesini istediğinizi belirtir. - Room'un yalnızca daha yüksek bir veritabanı sürümünden daha düşük bir sürüme taşınırken veri kaybına yol açan yeniden oluşturma işlemine başvurmasını istiyorsanız bunun yerine
fallbackToDestructiveMigrationOnDowngrade()
değerini kullanın.
Room 2.2.0'a yükseltme yaparken sütun varsayılan değerlerini işleme
Room 2.2.0 ve sonraki sürümlerde, @ColumnInfo(defaultValue = "...")
ek açıklamasını kullanarak bir sütun için varsayılan değer tanımlayabilirsiniz.
2.2.0'den önceki sürümlerde, bir sütun için varsayılan değer tanımlamanın tek yolu, doğrudan yürütülen bir SQL ifadesiyle tanımlamaktır. Bu işlem, Room'un bilmediği bir varsayılan değer oluşturur. Yani bir veritabanı başlangıçta Room 2.2.0'dan eski bir Room sürümü tarafından oluşturulduysa uygulamanızı Room 2.2.0'ı kullanacak şekilde yükseltmek için Room API'lerini kullanmadan tanımladığınız mevcut varsayılan değerler için özel bir taşıma yolu sağlamanız gerekebilir.
Örneğin, bir veritabanının 1. sürümünde Song
öğesi tanımlandığını varsayalım:
Kotlin
// Song entity, database version 1, Room 2.1.0. @Entity data class Song( @PrimaryKey val id: Long, val title: String )
Java
// Song entity, database version 1, Room 2.1.0. @Entity public class Song { @PrimaryKey final long id; final String title; }
Aynı veritabanının 2. sürümüne yeni bir NOT NULL
sütunu eklendiğini ve 1. sürümden 2. sürüme bir taşıma yolu tanımlandığını varsayalım:
Kotlin
// Song entity, database version 2, Room 2.1.0. @Entity data class Song( @PrimaryKey val id: Long, val title: String, val tag: String // Added in version 2. ) // Migration from 1 to 2, Room 2.1.0. val MIGRATION_1_2 = object : Migration(1, 2) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL( "ALTER TABLE Song ADD COLUMN tag TEXT NOT NULL DEFAULT ''") } }
Java
// Song entity, database version 2, Room 2.1.0. @Entity public class Song { @PrimaryKey final long id; final String title; @NonNull final String tag; // Added in version 2. } // Migration from 1 to 2, Room 2.1.0. static final Migration MIGRATION_1_2 = new Migration(1, 2) { @Override public void migrate(SupportSQLiteDatabase database) { database.execSQL( "ALTER TABLE Song ADD COLUMN tag TEXT NOT NULL DEFAULT ''"); } };
Bu durum, temel tabloda uygulamanın güncellemeleri ile yeni yüklemeleri arasında tutarsızlığa neden olur. tag
sütununun varsayılan değeri yalnızca 1. sürümden 2. sürüme geçiş yolunda belirtildiğinden, uygulamayı 2. sürümden itibaren yükleyen kullanıcıların veritabanı şemasında tag
için varsayılan değer yoktur.
Room'un 2.2.0'den önceki sürümlerinde bu tutarsızlık zararsızdır. Ancak uygulama daha sonra Room 2.2.0 veya sonraki bir sürümü kullanacak şekilde yükseltilirse ve Song
varlık sınıfı, @ColumnInfo
ek açıklamasını kullanarak tag
için varsayılan bir değer içerecek şekilde değiştirilirse Room bu tutarsızlığı görebilir. Bu durum, şema doğrulamalarının başarısız olmasına neden olur.
Önceki taşıma yollarınızda sütun varsayılan değerleri tanımlandığında veritabanı şemasının tüm kullanıcılar arasında tutarlı olmasını sağlamak için uygulamanızı Room 2.2.0 veya sonraki bir sürümü kullanacak şekilde ilk kez yükseltirken aşağıdakileri yapın:
@ColumnInfo
ek açıklamasını kullanarak sütun varsayılan değerlerini ilgili varlık sınıflarında belirtin.- Veritabanı sürüm numarasını 1 artırın.
- Mevcut sütunlara gerekli varsayılan değerleri eklemek için yeni sürüme sil ve yeniden oluştur stratejisini uygulayan bir taşıma yolu tanımlayın.
Aşağıdaki örnekte bu işlem gösterilmektedir:
Kotlin
// Migration from 2 to 3, Room 2.2.0. val MIGRATION_2_3 = object : Migration(2, 3) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL(""" CREATE TABLE new_Song ( id INTEGER PRIMARY KEY NOT NULL, name TEXT, tag TEXT NOT NULL DEFAULT '' ) """.trimIndent()) database.execSQL(""" INSERT INTO new_Song (id, name, tag) SELECT id, name, tag FROM Song """.trimIndent()) database.execSQL("DROP TABLE Song") database.execSQL("ALTER TABLE new_Song RENAME TO Song") } }
Java
// Migration from 2 to 3, Room 2.2.0. static final Migration MIGRATION_2_3 = new Migration(2, 3) { @Override public void migrate(SupportSQLiteDatabase database) { database.execSQL("CREATE TABLE new_Song (" + "id INTEGER PRIMARY KEY NOT NULL," + "name TEXT," + "tag TEXT NOT NULL DEFAULT '')"); database.execSQL("INSERT INTO new_Song (id, name, tag) " + "SELECT id, name, tag FROM Song"); database.execSQL("DROP TABLE Song"); database.execSQL("ALTER TABLE new_Song RENAME TO Song"); } };