앱에서 기능을 추가하고 변경하는 경우 Room 항목 클래스와 기본 데이터베이스 테이블을 수정하여 이러한 변경사항을 반영해야 합니다. 앱 업데이트로 인해 데이터베이스 스키마가 변경될 때는 기기 내 데이터베이스에 있는 사용자 데이터를 유지하는 것이 중요합니다.
Room에서는 자동 및 수동 방식의 증분 이전 옵션을 모두 지원합니다. 대부분의 기본 스키마 변경은 자동 이전이 가능하지만 좀 더 복잡한 변경인 경우 수동으로 이전 경로를 정의해야 할 수 있습니다.
자동 이전
두 데이터베이스 버전 간의 자동 이전을 선언하려면 @Database
의 autoMigrations
속성에 @AutoMigration
주석을 추가하세요.
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 { ... }
자동 이전 사양
Room에서 모호한 스키마 변경사항을 감지했는데 추가 입력 없이는 이전 계획을 생성할 수 없는 경우 컴파일 시간 오류가 발생하고 개발자에게 AutoMigrationSpec
을 구현하라는 메시지가 표시됩니다.
다음 중 하나가 포함된 이전에서 이러한 상황이 가장 자주 발생합니다.
- 테이블 삭제 또는 이름 바꾸기
- 열 삭제 또는 이름 바꾸기
AutoMigrationSpec
을 사용하여 이전 경로를 올바르게 생성하는 데 필요한 추가 정보를 Room에 제공할 수 있습니다. RoomDatabase
클래스에서 AutoMigrationSpec
을 구현하는 정적 클래스를 정의하고 다음 중 하나 이상으로 주석을 달면 됩니다.
자동 이전에 AutoMigrationSpec
구현을 사용하려면 상응하는 @AutoMigration
주석에 spec
속성을 설정하세요.
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 { } ... }
자동 이전이 완료된 후 앱에서 더 많은 작업을 해야 한다면 onPostMigrate()
를 구현하면 됩니다.
AutoMigrationSpec
에 이 메서드를 구현하면 Room에서는 자동 이전이 완료된 후 이를 호출합니다.
수동 이전
복잡한 스키마 변경사항이 포함된 이전이라면 Room에서 적절한 이전 경로를 자동으로 생성하지 못할 수도 있습니다. 예를 들어 한 테이블의 데이터를 두 테이블로 분할하려고 하는 경우 Room에서는 어떻게 분할해야 하는지 알 수 없습니다. 이 같은 경우에는 Migration
클래스를 구현하여 수동으로 이전 경로를 정의해야 합니다.
Migration
클래스는 Migration.migrate()
메서드를 재정의하여 startVersion
과 endVersion
간의 이전 경로를 명시적으로 정의합니다. addMigrations()
메서드를 사용하여 Migration
클래스를 데이터베이스 빌더에 추가하세요.
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();
이전 경로를 정의할 때 일부 버전에는 자동 이전을 사용하고 다른 버전에는 수동 이전을 사용할 수 있습니다. 같은 버전에 자동 이전과 수동 이전을 모두 정의하면 Room은 수동 이전을 사용합니다.
이전 테스트
이전은 복잡한 경우가 많으며 잘못 정의된 이전으로 인해 앱이 비정상 종료될 수 있습니다. 앱의 안정성을 유지하려면 이전을 테스트해야 합니다. Room은 room-testing
Maven 아티팩트를 제공하여 자동 및 수동 이전의 테스트 프로세스를 지원합니다. 이 아티팩트가 작동하려면 먼저 데이터베이스 스키마를 내보내야 합니다.
스키마 내보내기
Room은 컴파일 시 데이터베이스 스키마 정보를 JSON 파일로 내보낼 수 있습니다. 내보낸 JSON 파일은 데이터베이스의 스키마 기록을 나타냅니다. 스토어 이러한 파일을 버전 제어 시스템에 저장하므로 Room은 테스트 목적과 자동 이전 생성을 사용 설정하기 위해 데이터베이스를 배포해야 합니다.
Room Gradle 플러그인을 사용하여 스키마 위치 설정
Room 버전 2.6.0 이상을 사용하는 경우
Room Gradle 플러그인을 실행하고
room
확장자를 사용하여 스키마 디렉터리를 지정합니다.
Groovy
plugins {
id 'androidx.room'
}
room {
schemaDirectory "$projectDir/schemas"
}
Kotlin
plugins {
id("androidx.room")
}
room {
schemaDirectory("$projectDir/schemas")
}
데이터베이스 스키마가 변형, 맛 또는 빌드에 따라 다른 경우
schemaDirectory()
.
구성을 여러 번 가리키며 각각 variantMatchName
가 첫 번째
인수입니다. 각 구성은 단순성을 기반으로 한 하나 이상의 대안과
대안 이름과 비교됩니다.
이러한 제안이 모든 정보를 포함하고 모든 옵션을 포함하는지 확인하세요. 또한
일치하지 않는 대안을 처리하기 위한 variantMatchName
없이 schemaDirectory()
다른 구성으로 인해
발생하지 않을 수 있습니다 예를 들어
버전 demo
및 full
, 두 빌드 유형 debug
및 release
,
유효한 구성은 다음과 같습니다.
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")
}
주석 프로세서 옵션을 사용하여 스키마 위치 설정
Room 2.5.2 이하 버전을 사용하거나
Room Gradle 플러그인: room.schemaLocation
를 사용하여 스키마 위치 설정
주석 프로세서 옵션
이 디렉터리의 파일은 일부 Gradle 작업의 입력 및 출력으로 사용됩니다.
증분 빌드와 캐시된 빌드의 정확성과 성능을 위해서는
Gradle의
CommandLineArgumentProvider
드림
Gradle에 이 디렉터리에 관해 알립니다.
먼저 아래에 표시된 RoomSchemaArgProvider
클래스를 모듈의
Gradle 빌드 파일 샘플 클래스의 asArguments()
메서드는
room.schemaLocation=${schemaDir.path}
에서 KSP
(으)로 KAPT
및
javac
, 대신 이 값을 -Aroom.schemaLocation=${schemaDir.path}
로 변경하세요.
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}")
}
}
그런 다음 RoomSchemaArgProvider
를 다음과 함께 사용하도록 컴파일 옵션을 구성합니다.
지정된 스키마 디렉터리:
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"))
)
}
}
}
}
단일 이전 테스트
이전을 테스트하려면 먼저 Room의 androidx.room:room-testing
Maven 아티팩트를 테스트 종속 항목에 추가하고 다음과 같이 내보낸 스키마의 위치를 애셋 폴더로 추가합니다.
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") }
테스트 패키지는 내보낸 스키마 파일을 읽을 수 있는 MigrationTestHelper
클래스를 제공합니다. 또한 패키지는 JUnit4 TestRule
인터페이스도 구현하므로 생성된 데이터베이스를 관리할 수 있습니다.
다음 예는 단일 이전 테스트를 보여줍니다.
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. } }
모든 이전 테스트
단일 증분 이전을 테스트할 수 있지만, 앱 데이터베이스에 정의된 모든 이전을 포괄하는 테스트를 포함하는 것이 좋습니다. 이렇게 하면 최근에 생성된 데이터베이스 인스턴스와 정의된 이전 경로를 따랐던 예전 인스턴스 간에 불일치가 발생하지 않도록 하는 데 도움이 됩니다.
다음 예는 정의된 모든 이전에 관한 테스트를 보여줍니다.
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}; }
누락된 이전 경로의 적절한 처리
Room이 기기의 기존 데이터베이스를 현재 버전으로 업그레이드하기 위한 이전 경로를 찾을 수 없으면 IllegalStateException
이 발생합니다. 이전 경로가 누락되었을 때 기존 데이터를 잃어도 괜찮다면 데이터베이스 생성 시 다음과 같이 fallbackToDestructiveMigration()
빌더 메서드를 호출합니다.
Kotlin
Room.databaseBuilder(applicationContext, MyDb::class.java, "database-name") .fallbackToDestructiveMigration() .build()
Java
Room.databaseBuilder(getApplicationContext(), MyDb.class, "database-name") .fallbackToDestructiveMigration() .build();
이 메서드는 증분 이전을 실행해야 하고 정의된 이전 경로가 없을 때 앱의 데이터베이스에 테이블을 파괴적인 방식으로 다시 생성하도록 Room에 지시합니다.
Room이 특정 상황에서만 파괴적인 방식으로 테이블을 다시 생성하도록 하려면 다음과 같은 fallbackToDestructiveMigration()
의 대안을 사용할 수 있습니다.
- 스키마 기록의 특정 버전에서 이전 경로로 해결할 수 없는 오류가 발생하면 대신
fallbackToDestructiveMigrationFrom()
을 사용하세요. 이 메서드를 사용하면 특정 버전에서 이전할 때만 Room이 파괴적인 재생성 방식을 선택하게 됩니다. - 데이터베이스 상위 버전에서 하위 버전으로 이전할 때만 Room이 파괴적인 재생성 방식을 선택하게 하려면
fallbackToDestructiveMigrationOnDowngrade()
를 사용하세요.
Room 2.2.0으로 업그레이드 시 열 기본값 처리
Room 2.2.0 이상에서는 @ColumnInfo(defaultValue = "...")
주석을 사용하여 열의 기본값을 정의할 수 있습니다.
2.2.0보다 낮은 버전에서 열의 기본값을 정의하는 유일한 방법은 실행된 SQL 문에서 직접 기본값을 정의하는 것으로, 이는 Room이 알지 못하는 기본값을 생성합니다. 즉, 데이터베이스가 원래 2.2.0보다 낮은 Room 버전으로 생성되었다면 Room 2.2.0을 사용하도록 앱을 업그레이드하려고 할 때 Room API를 사용하지 않고 정의한 기존 기본값에 특수한 이전 경로를 제공해야 할 수 있습니다.
예를 들어 데이터베이스 버전 1이 Song
항목을 정의한다고 가정해 보겠습니다.
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; }
또한 동일한 데이터베이스의 버전 2가 새로운 NOT NULL
열을 추가하고 버전 1에서 버전 2로의 이전 경로를 정의한다고 가정해 보겠습니다.
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 ''"); } };
이로 인해 기본 테이블에 앱 업데이트와 신규 설치 간 불일치가 발생합니다. tag
열의 기본값은 버전 1에서 버전 2로의 이전 경로에만 선언되므로 버전 2부터 앱을 설치한 사용자는 데이터베이스 스키마에 tag
의 기본값이 없습니다.
2.2.0보다 낮은 Room 버전에서는 불일치가 있어도 무해합니다. 그러나 나중에 앱이 Room 2.2.0 이상을 사용하도록 업그레이드하고 @ColumnInfo
주석을 사용하여 tag
의 기본값을 포함하도록 Song
항목 클래스를 변경하면 Room에서 이 불일치를 확인할 수 있습니다. 이로 인해 스키마 유효성 검사가 실패합니다.
초기에 이전 경로에서 열 기본값이 선언될 때 데이터베이스 스키마가 모든 사용자에게서 일관되도록 하려면 앱을 처음 업그레이드할 때 다음을 실행하여 Room 2.2.0 이상을 사용합니다.
@ColumnInfo
주석을 사용하여 각 항목 클래스에서 열 기본값을 선언합니다.- 데이터베이스 버전 번호를 1씩 늘립니다.
- 삭제 및 재생성 전략을 구현하는 새 버전의 이전 경로를 정의하여 필요한 기본값을 기존 열에 추가합니다.
다음 예는 이 프로세스를 보여줍니다.
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"); } };