Hanbit the Developer

Room Migration 방법 본문

Mobile/Android

Room Migration 방법

hanbikan 2024. 1. 17. 21:06

배경

Room에 새 테이블로 User를 추가하였더니 아래와 같은 에러가 발생하였다.

error: There is a problem with the query: [SQLITE_ERROR] SQL error or missing database (no such table: user)

GPT에 의하면 이 경우 DB 버전을 올린 뒤 database에 migration strategy를 제공하라고 하였다:

1. Update Database Version

@Database(entities = [TaskEntity::class], version = 2)
abstract class NnDatabase : RoomDatabase() {
    abstract fun taskDao(): TaskDao
    abstract fun userDao(): UserDao
}

2. Migration Strategy

 - 기존 데이터를 유지하는 방법:

Room.databaseBuilder(appContext, AppDatabase::class.java, "YourDatabaseName")
    .fallbackToDestructiveMigration()
    .build()

 - 기존 데이터를 제거하는 가장 간단한 방법:

val MIGRATION_1_2 = object : Migration(1, 2) {
    override fun migrate(database: SupportSQLiteDatabase) {
        // Migration logic here
    }
}

Room.databaseBuilder(appContext, AppDatabase::class.java, "YourDatabaseName")
    .addMigrations(MIGRATION_1_2)
    .build()

하지만 GPT에만 의존하는 것은 내키지 않아 안드로이드 공식 문서(migrating-db-versions)를 기반으로 내용을 정리하고자 한다.

내용 정리: migrating-db-versions

Automated migrations

아래와 같은 코드를 통해 마이그레이션을 자동으로 진행할 수 있다:

// 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() {
  ...
}

Automatic migration specifications

스키마가 모호할 경우 컴파일 에러가 발생하며 이때 AutoMigrationSpec을 구현해야 한다. 이를 통해 Room에 추가 정보를 제공함으로써 마이그레이션이 성공적으로 진행된다.

대표적으로 다음의 경우에 컴파일 에러가 발생한다:

  • 테이블을 삭제하거나 이름을 변경하는 경우
  • Column을 삭제하거나 이름을 변경하는 경우

구현 사항은 다음과 같다:

  • RoomDatabase 내부에 AutoMigraionSpec 구현체를 정의하고 1개 이상의 어노테이션을 붙인다:
    • @DeleteTable
    • @RenameTable
    • @DeleteColumn
    • @RenameColumn
  • AutoMigration의 spec 프로퍼티에 해당 클래스를 지정해준다.

코드 예시는 다음과 같다:

@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
  ...
}

자동화된 마이그레이션 이후에 추가 작업을 해야 한다면 onPostMigrate()를 구현하면 된다.

Manual migrations

복잡한 스키마 변경이 있을 경우 자동화된 마이그레이션이 불가능하다. 예를 들면 테이블 하나를 두 개로 분리하는 경우가 있다. 이 경우 Migration 클래스를 구현하여 수동으로 마이그레이션 해야 한다.

구현 사항은 다음과 같다:

  • Migration 생성자에 startVersion, endVersion를 명시한다.
  • migrate() 함수를 오버라이드 한다.
  • 구현한 클래스를 database builder의 addMigrations() 함수로 추가한다.

코드 예시는 다음과 같다:

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()

어떤 버전에는 자동화된 마이그레이션을 사용하고 다른 버전에는 수동 마이그레이션을 사용하는 것도 가능하다. 단, 특정 버전에 자동화된 마이그레이션과 수동 마이그레이션이 모두 정의되어 있는 경우, Room은 수동 마이그레이션을 사용한다.

Gracefully handle missing migration paths

Room이 데이터베이스를 업그레이드하는 데 쓰이는 마이그레이션을 찾을 수 없으면 예외가 발생한다. 만약 앱이 이런 상황에서 기존 데이터를 전부 지우고 데이터베이스를 업데이트 하기 원한다면, fallbackToDestructiveMigration()를 database builder에 추가하면 된다.

Room.databaseBuilder(applicationContext, MyDb::class.java, "database-name")
        .fallbackToDestructiveMigration()
        .build()

만약 특정 상황에서만 이러한 destructive recreation을 진행하기 원한다면 대안이 있다:

  • fallbackToDestructiveMigrationFrom(): 특정 버전에서만 발생한다.
  • fallbackToDestructiveMigrationOnDowngrade(): 데이터베이스를 높은 버전에서 낮은 버전으로 다운그레이드 할 때만 발생한다.

적용하기

내 경우 테이블을 단순히 추가한 것이므로 수동 마이그레이션을 쓸만한 상황이 아니다. 따라서 자동화된 마이그레이션을 도입하였다.

@Database(
    version = 2,
    entities = [
        TaskEntity::class,
        UserEntity::class
    ],
    autoMigrations = [
        AutoMigration (from = 1, to = 2)
    ]
)
abstract class NnDatabase : RoomDatabase() {
    abstract fun taskDao(): TaskDao
    abstract fun userDao(): UserDao
}

문제 해결: Schema import directory

error: Schema import directory was not provided to the annotation processor so Room cannot read older schemas. To generate auto migrations, you must provide room.schemaLocation annotation processor arguments by applying the Room Gradle plugin (id 'androidx.room') AND set exportSchema to true.

위와 같은 컴파일 에러가 발생하여 아래의 코드를 Room이 쓰이는 모듈에 추가하여 해결하였다:

android {
    // namespace = ...

    defaultConfig {
        javaCompileOptions {
            annotationProcessorOptions {
                arguments["room.schemaLocation"] = "$projectDir/schemas"
            }
        }
    }
}

출처: https://stackoverflow.com/questions/44322178/room-schema-export-directory-is-not-provided-to-the-annotation-processor-so-we

문제 해결: Schema '1.json'

error: Schema '1.json' required for migration was not found at the schema out folder

위와 같은 컴파일 에러가 발생하였다. 이를 해결하기 위해 데이터베이스 버전을 1로 되돌리고 exportSchema를 true로 세팅한 뒤 빌드하여 1.json 파일을 생성하였다.

@Database(
    version = 1,
    entities = [TaskEntity::class],
    exportSchema = true
)
abstract class NnDatabase : RoomDatabase() {
    abstract fun taskDao(): TaskDao
}

출처: https://stackoverflow.com/questions/76872496/android-room-error-schema-1-json-required-for-migration-was-not-found-at-the

 

+ 추가: manual migration 사용 사례

앱을 업데이트 하다보니 tutorial_task라는 테이블을 완전히 초기화하는 상황이 발생하여 다음과 같이 수동으로 마이그레이션 하였습니다.

// NkDatabase.kt

@Database(
    version = 11,
    entities = [
        TaskEntity::class,
        UserEntity::class,
        TutorialTaskEntity::class,
        FishEntity::class,
    ],
    autoMigrations = [
        AutoMigration (from = 1, to = 2),
        AutoMigration (from = 2, to = 3),
        AutoMigration (from = 3, to = 4),
        AutoMigration (from = 4, to = 5),
        AutoMigration (from = 5, to = 6),
        // There is no auto migration for 6 to 7!
        AutoMigration (from = 7, to = 8),
        AutoMigration (from = 8, to = 9),
        AutoMigration (from = 9, to = 10),
        AutoMigration (from = 10, to = 11),
    ],
    exportSchema = true
)
@TypeConverters(Converters::class)
abstract class NkDatabase : RoomDatabase() {
    abstract fun taskDao(): TaskDao
    abstract fun tutorialTaskDao(): TutorialTaskDao
    abstract fun userDao(): UserDao
    abstract fun collectionDao(): CollectionDao
}

val MIGRATION_6_7 = object : Migration(6, 7) {
    override fun migrate(db: SupportSQLiteDatabase) {
        // 기존 테이블 삭제 후 생성(reset)
        db.execSQL("DROP TABLE tutorial_task")
        db.execSQL("""
            CREATE TABLE tutorial_task (
                id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
                user_id INTEGER NOT NULL DEFAULT -1,
                name TEXT NOT NULL,
                is_done INTEGER NOT NULL,
                details TEXT DEFAULT NULL,
                day INTEGER NOT NULL
            )
        """)
    }
}
// DatabaseModule.kt

@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {

    @Provides
    @Singleton
    fun providesNkDatabase(
        @ApplicationContext context: Context
    ) : NkDatabase = Room.databaseBuilder(
        context,
        NkDatabase::class.java,
        "nk-database"
    )
        .addMigrations(MIGRATION_6_7)
        .build()
}

'Mobile > Android' 카테고리의 다른 글

Android Document | Processes and app lifecycle  (3) 2024.01.25
Android Document | App startup time  (1) 2024.01.25
ViewModelStore, ViewModelProvider 분석  (1) 2024.01.12
AnimatedVisibility 오버랩 이슈 해결  (1) 2024.01.11
ViewModel 분석  (0) 2023.12.04