Room 데이터베이스 이전

앱에서 기능을 추가하고 변경하는 경우 Room 항목 클래스와 기본 데이터베이스 테이블을 수정하여 이러한 변경사항을 반영해야 합니다. 앱 업데이트로 인해 데이터베이스 스키마가 변경될 때는 기기 내 데이터베이스에 있는 사용자 데이터를 유지하는 것이 중요합니다.

Room에서는 자동 및 수동 방식의 증분 이전 옵션을 모두 지원합니다. 대부분의 기본 스키마 변경은 자동 이전이 가능하지만 좀 더 복잡한 변경인 경우 수동으로 이전 경로를 정의해야 할 수 있습니다.

자동 이전

두 데이터베이스 버전 간의 자동 이전을 선언하려면 @AutoMigration 주석을 autoMigrations 속성에 @Database 추가하세요.

// Database class before the version update.
@Database(
  version = 1,
  entities = [User::class]
)
abstract class AppDatabaseV1 : RoomDatabase() {
  abstract fun userDao(): UserDao
}

// Database class after the version update.
@Database(
  version = 2,
  entities = [User::class],
  autoMigrations = [
    AutoMigration(from = 1, to = 2)
  ]
)
abstract class AppDatabaseV2 : RoomDatabase() {
  abstract fun userDao(): UserDao
}

자동 이전 사양

Room에서 모호한 스키마 변경사항을 감지했는데 추가 입력 없이는 이전 계획을 생성할 수 없는 경우 컴파일 시간 오류가 발생하고 개발자는 AutoMigrationSpec 구현을 제공해야 합니다. 이 오류는 일반적으로 이전이 다음 중 하나를 포함할 때 발생합니다.

  • 테이블 삭제 또는 이름 바꾸기
  • 열 삭제 또는 이름 바꾸기

AutoMigrationSpec을 사용하여 이전 경로를 올바르게 생성하는 데 필요한 추가 정보를 Room에 제공할 수 있습니다. RoomDatabase 클래스에서 AutoMigrationSpec을 구현하는 클래스를 정의하고 다음 중 하나 이상의 주석으로 주석 처리합니다.

자동 이전에 AutoMigrationSpec 구현을 사용하려면 상응하는 @AutoMigration 주석에 spec 속성을 설정하세요.

@Database(
  version = 2,
  entities = [User::class],
  autoMigrations = [
    AutoMigration (
      from = 1,
      to = 2,
      spec = MigrationSpec1To2::class
    )
  ]
)
abstract class AppDatabaseWithSpec : RoomDatabase() {
  abstract fun userDao(): UserDao
}

@RenameTable(fromTableName = "User", toTableName = "AppUser")
internal class MigrationSpec1To2 : AutoMigrationSpec

자동 이전이 완료된 후 앱에서 추가 작업을 해야 하는 경우 앱에서 추가 작업을 해야 하는 경우 를 구현할 수 있습니다.onPostMigrate AutoMigrationSpec에서 이 함수를 구현하면 자동 이전이 완료된 후 Room에서 이 함수를 호출합니다.

수동 이전

이전이 복잡한 스키마 변경사항을 포함하는 경우 Room에서 적절한 이전 경로를 자동으로 생성하지 못할 수도 있습니다. 예를 들어 한 테이블의 데이터를 두 테이블로 분할하려고 하는 경우 Room에서는 어떻게 분할해야 하는지 알 수 없습니다. 이러한 상황에서는 이전 경로를 수동으로 정의해야 합니다.Migration

A Migration 클래스는 migrate 함수를 재정의하여 startVersion and an endVersion 간의 이전 경로를 명시적으로 정의합니다. your Migration 클래스를 데이터베이스 빌더에 추가합니다.addMigrations

val MIGRATION_1_2 = object : Migration(1, 2) {
  override suspend fun migrate(connection: SQLiteConnection) {
    connection.executeSQL("CREATE TABLE `Fruit` (`id` INTEGER, `name` TEXT, " +
      "PRIMARY KEY(`id`))")
  }
}

val MIGRATION_2_3 = object : Migration(2, 3) {
  override suspend fun migrate(connection: SQLiteConnection) {
    connection.executeSQL("ALTER TABLE Book ADD COLUMN pub_year INTEGER")
  }
}

Room.databaseBuilder<ManualMigrationDatabase>(applicationContext, "database-name")
  .addMigrations(MIGRATION_1_2, MIGRATION_2_3)
  .build()

이전 경로를 정의할 때 일부 버전에는 자동 이전을 사용하고 다른 버전에는 수동 이전을 사용할 수 있습니다. 같은 버전에 자동 이전과 수동 이전을 모두 정의하면 Room은 수동 이전을 사용합니다.

이전 테스트

이전은 복잡한 경우가 많으며 잘못 정의된 이전으로 인해 앱이 비정상 종료될 수 있습니다. 앱의 안정성을 유지하려면 이전을 테스트해야 합니다. Room은 room3-testing Maven 아티팩트를 제공하여 자동 및 수동 이전의 테스트 프로세스를 지원합니다. 이 아티팩트가 작동하려면 먼저 데이터베이스의 스키마를 내보내야 합니다.

스키마 내보내기

Room은 컴파일 시 데이터베이스 스키마 정보를 JSON 파일로 내보냅니다. 내보낸 JSON 파일은 데이터베이스의 스키마 기록을 나타냅니다. 테스트를 위해 이전 버전의 데이터베이스를 다시 만들고 자동 이전 생성을 지원할 수 있도록 이러한 파일을 버전 제어 시스템에 저장하세요.

Room Gradle 플러그인을 사용하여 스키마 위치 설정

스키마 디렉터리를 지정하려면 Room Gradle 플러그인을 적용하고 room3 확장 프로그램을 사용하세요.

Groovy

plugins {
  id 'androidx.room3'
}

room3 {
  schemaDirectory "$projectDir/schemas"
}

Kotlin

plugins {
  id("androidx.room3")
}

room3 {
  schemaDirectory("$projectDir/schemas")
}

데이터베이스 스키마가 변형, 제품 버전 또는 빌드 유형에 따라 다른 경우 schemaDirectory 구성을 여러 번 사용하여 각각 variantMatchName을 첫 번째 인수로 지정하여 다른 위치를 지정해야 합니다. 각 구성은 변형 이름과의 간단한 비교를 기반으로 하나 이상의 변형과 일치할 수 있습니다.

이러한 구성이 포괄적이고 모든 변형을 포함하는지 확인하세요. 다른 구성과 일치하지 않는 변형을 처리하기 위해 variantMatchName이 없는 schemaDirectory()를 포함할 수도 있습니다. 예를 들어 빌드 제품 버전이 demofull이고 빌드 유형이 debugrelease인 앱에서 다음은 유효한 구성입니다.

Groovy

room3 {
  // 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

room3 {
  // 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 Gradle 플러그인을 사용하지 않는 경우 room.schemaLocation 주석 프로세서 옵션을 사용하여 스키마 위치를 설정하세요.

Gradle은 이 디렉터리의 파일을 일부 Gradle 작업의 입력 및 출력으로 사용합니다. 증분 및 캐시된 빌드의 정확성과 성능을 위해 Gradle's CommandLineArgumentProvider를 사용하여 이 디렉터리에 관해 Gradle에 알려야 합니다.

먼저 다음 RoomSchemaArgProvider 클래스를 모듈의 Gradle 빌드 파일에 복사합니다. 샘플 클래스의 asArguments 함수는 room.schemaLocation=${schemaDir.path}KSP에 전달합니다. KAPTjavac를 사용하는 경우 이 값을 -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() {
    return ["room.schemaLocation=${schemaDir.path}".toString()]
  }
}

Kotlin

class RoomSchemaArgProvider(
  @get:InputDirectory
  @get:PathSensitive(PathSensitivity.RELATIVE)
  val schemaDir: File
) : CommandLineArgumentProvider {

  override fun asArguments(): Iterable<String> {
    return listOf("room.schemaLocation=${schemaDir.path}")
  }
}

그런 다음 지정된 스키마 디렉터리와 함께 RoomSchemaArgProvider를 사용하도록 컴파일 옵션을 구성합니다.

Groovy

ksp {
  arg(new RoomSchemaArgProvider(new File(projectDir, "schemas")))
}

Kotlin

ksp {
  arg(RoomSchemaArgProvider(File(projectDir, "schemas")))
}

단일 이전 테스트

이전을 테스트하려면 먼저 androidx.room3:room3-testing 아티팩트를 테스트 종속 항목에 추가하고 다음과 같이 내보낸 스키마의 위치를 애셋 디렉터리로 추가합니다.

Groovy

android {
    ...
    sourceSets {
        // Adds exported schema location as test app assets if not using
        // the Room Gradle Plugin.
        androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
    }
}

dependencies {
    ...
    androidTestImplementation "androidx.room3:room3-testing:3.0.1"
}

Kotlin

android {
    ...
    sourceSets {
        // Adds exported schema location as test app assets if not using
        // the Room Gradle Plugin.
        getByName("androidTest").assets.srcDir("$projectDir/schemas")
    }
}

dependencies {
    ...
    testImplementation("androidx.room3:room3-testing:3.0.1")
}

테스트 패키지는 MigrationTestHelper 클래스를 제공하며, 내보낸 스키마 파일을 읽을 수 있습니다. 또한 패키지는 JUnit4 TestRule 인터페이스를 구현하여 생성된 데이터베이스를 관리합니다.

다음 예는 단일 이전 테스트를 보여줍니다.

@RunWith(AndroidJUnit4::class)
class MigrationTest {
    private val TEST_DB = "migration-test"

    private val instrumentation = InstrumentationRegistry.getInstrumentation()

    @get:Rule
    val helper = MigrationTestHelper(
        instrumentation = instrumentation,
        databaseClass = MigrationDb::class,
        driver = AndroidSQLiteDriver(),
        file = instrumentation.targetContext.getDatabasePath(TEST_DB),
    )

    @Test
    fun migrate1To2() = runTest {
        val connection = helper.createDatabase(1)
        // Database has schema version 1. Insert some data using SQL queries.
        // You can't use DAO classes because they expect the latest schema.
        connection.execSQL("INSERT INTO User (id, name) VALUES (1, 'John Doe')")
        connection.close()

        // Re-open the database with version 2 and provide MIGRATION_1_2
        val migratedConnection = helper.runMigrationsAndValidate(2, listOf(MIGRATION_1_2))

        // MigrationTestHelper automatically verifies the schema changes,
        // but you need to validate that the data was migrated properly.
        val hasData = migratedConnection.prepare("SELECT COUNT(*) FROM User").use {
          it.step()
          it.getLong(0) > 0
        }
        assertTrue("Expected data was not migrated", hasData)
        migratedConnection.close()
    }
}

모든 이전 테스트

단일 증분 이전을 테스트할 수 있지만 앱 데이터베이스의 정의된 모든 이전을 포괄하는 테스트를 포함해야 합니다. 이렇게 하면 최근에 생성된 데이터베이스 인스턴스와 정의된 이전 경로를 따랐던 예전 인스턴스 간에 불일치가 발생하지 않도록 하는 데 도움이 됩니다.

다음 예는 정의된 모든 이전에 관한 테스트를 보여줍니다.

@RunWith(AndroidJUnit4::class)
class MigrationTest {
    private val TEST_DB = "migration-test"

    private val instrumentation = InstrumentationRegistry.getInstrumentation()

    // Array of all migrations.
    private val ALL_MIGRATIONS = arrayOf(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4)

    @get:Rule
    val helper: MigrationTestHelper = MigrationTestHelper(
        instrumentation = instrumentation,
        databaseClass = MigrationDb::class,
        driver = AndroidSQLiteDriver(),
        file = instrumentation.targetContext.getDatabasePath(TEST_DB),
    )

    @Test
    fun migrateAll() = runTest {
        // Create earliest version of the database.
        val connection = helper.createDatabase(1)
        connection.close()

        // Create latest version of the database.
        val db = Room.databaseBuilder<AppDatabase>(instrumentation.targetContext, TEST_DB)
          .setDriver(AndroidSQLiteDriver())
          .addMigrations(*ALL_MIGRATIONS)
          .build()
        // Open the database, Room validates the schema once all migrations
        // execute.
        db.useReaderConnection { connection ->
          // Perform additional validation
        }

        db.close()
    }
}

누락된 이전 경로의 적절한 처리

Room에서 기기의 기존 데이터베이스를 현재 버전으로 업그레이드하는 이전 경로를 찾을 수 없는 경우 IllegalStateException이 발생합니다. 이전 경로가 누락되었을 때 기존 데이터를 잃어도 괜찮다면 데이터베이스 생성 시 다음과 같이 fallbackToDestructiveMigration 빌더 함수를 호출합니다.

Room.databaseBuilder<FallbackMigrationDatabase>(applicationContext, "database-name")
        .fallbackToDestructiveMigration()
        .build()

이 함수는 증분 이전을 실행해야 하고 정의된 이전 경로가 없을 때 앱의 데이터베이스에 테이블을 파괴적인 방식으로 다시 생성하도록 Room을 구성합니다.

특정 상황에서만 파괴적인 재생성을 사용하려면 fallbackToDestructiveMigration 대신 다음 대안 중 하나를 사용하세요.

  • 스키마 기록의 특정 버전에서 이전 경로로 해결할 수 없는 오류가 발생하면 대신 fallbackToDestructiveMigrationFrom을 사용하세요. 이 함수는 특정 버전에서 이전할 때만 Room이 파괴적인 재생성을 사용하도록 하려는 것을 나타냅니다.
  • Room이 이전 버전의 데이터베이스에서 새 버전의 데이터베이스로 이전할 때만 파괴적인 재생성을 사용하도록 하려면 대신 fallbackToDestructiveMigrationOnDowngrade를 사용하세요.