Uygulamanızda özellik ekleyip değiştirirken, Oda 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.
Oda, 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 bildirmek için @Database
içindeki autoMigrations
özelliğine @AutoMigration
ek açıklaması 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
Oda, belirsiz şema değişiklikleri algılar ve daha fazla giriş olmadan bir taşıma planı oluşturamazsa derleme zamanı hatası verir ve sizden bir AutoMigrationSpec
uygulamanızı ister.
Bu durum genellikle taşıma işlemi aşağıdakilerden birini içerdiğinde gerçekleşir:
- Tablo silme veya yeniden adlandırma.
- Sütun silme veya yeniden adlandırma
Odaya, taşıma yollarını doğru şekilde oluşturmak için ihtiyaç duyduğu ek bilgileri vermek üzere AutoMigrationSpec
kullanabilirsiniz. RoomDatabase
sınıfınızda AutoMigrationSpec
uygulayan statik bir sınıf tanımlayın ve aşağıdakilerden biri veya birkaçıyla ek açıklama ekleyin:
Otomatik taşıma işleminde AutoMigrationSpec
uygulamasını kullanmak için ilgili @AutoMigration
ek açıklamasında spec
özelliğini 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()
öğesini uygulayabilirsiniz.
Bu yöntemi AutoMigrationSpec
içinde uygularsanız Oda, otomatik taşıma işlemi tamamlandıktan sonra 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, otomatik olarak uygun bir taşıma yolu oluşturamayabilir. Örneğin, bir tablodaki verileri iki tabloya bölmeye karar verirseniz Oda, bu bölme işleminin nasıl yapılacağını söyleyemez. Bu gibi durumlarda, Migration
sınıfı uygulayarak manuel olarak bir taşıma yolu tanımlamanız gerekir.
Migration
sınıfı, Migration.migrate()
yöntemini geçersiz kılarak startVersion
ile endVersion
arasındaki taşıma yolunu açıkça tanımlar. addMigrations()
yöntemini kullanarak Migration
sınıflarınızı 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ı, bazıları için manuel taşımaları kullanabilirsiniz. Aynı sürüm için hem otomatik taşıma hem de manuel taşıma tanımlarsanız Oda, 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. Oda, hem otomatik hem de manuel taşıma işlemleri için test sürecine yardımcı olmak amacıyla 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
Oda, derleme sırasında veritabanınızın şema bilgilerini bir JSON dosyasına aktarabilir. Dışa aktarılan JSON dosyaları, veritabanınızın şema geçmişini temsil eder. Odanın test amacıyla veritabanının daha düşük sürümlerini oluşturabilmesi ve otomatik taşıma oluşturmayı etkinleştirebilmesi için bu dosyaları sürüm kontrol sisteminizde depolayın.
Room Gradle eklentisini kullanarak şema konumunu ayarlama
Room 2.6.0 veya sonraki bir sürümü kullanıyorsanız Room Gradle Plugin'i uygulayabilir ve şema dizinini belirtmek için room
uzantısını kullanabilirsiniz.
Eski
plugins {
id 'androidx.room'
}
room {
schemaDirectory "$projectDir/schemas"
}
Kotlin
plugins {
id("androidx.room")
}
room {
schemaDirectory("$projectDir/schemas")
}
Veritabanı şemanız varyant, aroma veya derleme türüne göre farklılık gösteriyorsa schemaDirectory()
yapılandırmasını birden fazla kez kullanarak farklı konumlar belirtmeniz gerekir. Bu yapılandırmaların her birinde ilk bağımsız değişken olarak variantMatchName
yer alır. Her yapılandırma, varyant adıyla yapılan basit karşılaştırmaya dayanarak bir veya daha fazla varyantı eşleştirebilir.
Bu koşulların 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
içermeyen bir schemaDirectory()
de ekleyebilirsiniz. Örneğin, demo
ve full
iki derleme türü ile debug
ve release
derleme türüne sahip bir uygulamada aşağıdakiler geçerli yapılandırmalardır:
Eski
room {
// Applies to 'demoDebug' only
schemaLocation "demoDebug", "$projectDir/schemas/demoDebug"
// Applies to 'demoDebug' and 'demoRelease'
schemaLocation "demo", "$projectDir/schemas/demo"
// Applies to 'demoDebug' and 'fullDebug'
schemaLocation "debug", "$projectDir/schemas/debug"
// Applies to variants that aren't matched by other configurations.
schemaLocation "$projectDir/schemas"
}
Kotlin
room {
// Applies to 'demoDebug' only
schemaLocation("demoDebug", "$projectDir/schemas/demoDebug")
// Applies to 'demoDebug' and 'demoRelease'
schemaLocation("demo", "$projectDir/schemas/demo")
// Applies to 'demoDebug' and 'fullDebug'
schemaLocation("debug", "$projectDir/schemas/debug")
// Applies to variants that aren't matched by other configurations.
schemaLocation("$projectDir/schemas")
}
Ek açıklama işlemci 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
ek açıklama işlemci 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'ı bu dizin hakkında bilgilendirmek amacıyla Gradle'ın CommandLineArgumentProvider
hizmetini kullanmanız gerekir.
İlk olarak 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, KSP
sınıfına room.schemaLocation=${schemaDir.path}
iletir. KAPT
ve javac
kullanıyorsanız bu değeri -Aroom.schemaLocation=${schemaDir.path}
olarak değiştirin.
Eski
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, RoomSchemaArgProvider
öğesini belirtilen şema diziniyle kullanmak için derleme seçeneklerini yapılandırın:
Eski
// 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 etmeden önce, Room'daki 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:
derleme.gradle
Eski
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şlemine yönelik 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 et
Tek bir artımlı taşımayı 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, yakın zamanda oluşturulmuş bir veritabanı örneği ile tanımlanan taşıma yollarını izleyen eski bir örnek arasında fark olmamasını sağlar.
Aşağıdaki örnekte tanımlanan tüm taşıma işlemlerine yönelik 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ın sorunsuz bir şekilde ele alınması
Oda, bir cihazdaki mevcut veritabanını geçerli sürüme yükseltmek için bir taşıma yolu bulamazsa IllegalStateException
işlemi gerçekleşir. Taşıma yolu eksik olduğunda mevcut verilerin kaybedilmesi kabul edilebilir bir durumsa veritabanı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, odanın artımlı bir taşıma yapması gerektiğinde ve tanımlanmış bir taşıma yolu olmadığında uygulamanızın veritabanındaki tabloları yıkıcı bir şekilde yeniden oluşturmasını bildirir.
Oda modunu yalnızca belirli durumlarda yıkıcı eğlence faaliyetlerine geri dönmek istiyorsanız fallbackToDestructiveMigration()
yerine kullanabileceğiniz birkaç alternatif vardır:
- Şema geçmişinizin belirli sürümleri, taşıma yollarıyla çözemediğiniz hatalara neden oluyorsa bunun yerine
fallbackToDestructiveMigrationFrom()
değerini kullanın. Bu yöntem, Room'un yalnızca belirli sürümlerden taşıma yaparken zararlı yeniden oluşturma işlevini kullanmayı istediğinizi belirtir. - Room'un yalnızca daha yüksek bir veritabanı sürümünden daha düşük bir sürüme geçiş yaparken yıkıcı yeniden oluşturma işlemlerine yedeklenmesini istiyorsanız bunun yerine
fallbackToDestructiveMigrationOnDowngrade()
kullanın.
Oda 2.2.0'a yükseltme yaparken sütun varsayılan değerlerini işleme
Oda 2.2.0 ve sonraki sürümlerde, @ColumnInfo(defaultValue = "...")
ek açıklamasını kullanarak bir sütun için varsayılan bir değer tanımlayabilirsiniz.
2.2.0'dan düşük sürümlerde, bir sütun için varsayılan değer tanımlamanın tek yolu, sütunu doğrudan yürütülen bir SQL ifadesinde tanımlamaktır. Bu işlem Oda'nın bilmediği bir varsayılan değer oluşturur. Bu, veritabanının orijinal olarak Odanın 2.2.0'dan daha eski bir sürümüyle oluşturulduğu durumlarda, uygulamanızı Oda 2.2.0'ı kullanacak şekilde yükselttiğinizde, Room API'leri kullanmadan tanımladığınız mevcut varsayılan değerler için özel bir taşıma yolu sağlamanız gerekebileceği anlamına gelir.
Örneğin, veritabanı sürüm 1'in bir Song
varlığını tanımladığı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; }
Ayrıca, aynı veritabanına ait sürüm 2'nin yeni bir NOT NULL
sütunu eklediğini ve sürüm 1'den sürüm 2'ye bir taşıma yolu tanımladığı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 sürüm 1 ile sürüm 2 arasındaki taşıma yolunda belirtildiğinden, uygulamayı sürüm 2'den başlayarak yükleyen kullanıcılar veritabanı şemasında varsayılan tag
değerine sahip değildir.
Odanın 2.2.0'dan önceki sürümlerinde, bu tutarsızlık zararsızdır. Bununla birlikte, uygulama daha sonra Oda 2.2.0 veya sonraki bir sürümü kullanacak şekilde yükseltilir ve @ColumnInfo
ek açıklamasını kullanarak Song
varlık sınıfını tag
için varsayılan bir değer içerecek şekilde değiştirirse Oda 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ı Oda 2.2.0 veya sonraki bir sürümü kullanacak şekilde ilk kez yükselttiğinizde aşağıdaki adımları uygulayın:
@ColumnInfo
ek açıklamasını kullanarak, ilgili varlık sınıflarındaki sütun varsayılan değerlerini bildirin.- Veritabanı sürüm numarasını 1 artırın.
- Gerekli varsayılan değerleri mevcut sütunlara eklemek için bırak ve yeniden oluşturma stratejisini uygulayan yeni sürüme 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"); } };