Room官方文档(翻译)5.迁移数据库

1,925 阅读5分钟

当app 新增和修改功能时,需要修改实体类来适应这些改动。当用户更新到这个 app 的最新版本时,你不希望他们丢失现有数据,尤其是你无法从远程服务器恢复这些数据。

Room允许你编写 Migration 类来保存用户数据。每个 Migration 类指定一个开始版本号和结束版本号。在运行时,Room 运行每个 Migration 类的 migrate() 方法,使用正确的顺序将数据库迁移到更高版本。

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 会验证schema文件以确保迁移正确进行,如果 Room发现了问题,会抛出包含了不匹配信息的异常。

测试迁移

迁移编写起来并不容易,如果不能正确地编写它们,就可能导致 app 出现循环崩溃。为了保持 app的稳定性,需要事先测试迁移。Room 提供了 Maven 测试组件协助完成测试过程。为了这个组件能工作起来,你需要导出数据库的 schema文件。

导出 schema 文件

编译后,Room将数据库的 schema 导出到一个 JSON 文件。要导出 schema,请在 build.gradle做如下设置

android {
    ...
    defaultConfig {
        ...
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = ["room.schemaLocation":
                             "$projectDir/schemas".toString()]
            }
        }
    }
}

您应该将导出的JSON文件(代表数据库的架构历史记录)存储在版本控制系统中,因为它允许Room创建用于测试目的的较旧版本的数据库。

要测试这些迁移,将android.arch.persistence.room:testing添加到测试依赖中,然后将 schema的导出路径添加到资源文件夹中

android {
    ...
    sourceSets {
        androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
    }
}

测试包提供了MigrationTestHelper读取 schema 文件,它实现了 Junit4 TestRule接口,因此它可以管理已创建的数据库。

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

    @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 {
            // db has schema version 1. insert some data using SQL queries.
            // You cannot 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.
    }
}

测试所有迁移

上述例子展示从一个版本到另一个版本的增量迁移。建议你进行一次贯穿所有迁移到测试。这种类型的测试对于捕获数据库从迁移路径与最近创建路径的差异是有用的。

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

    @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 will validate the schema
        // once all migrations execute.
        Room.databaseBuilder(
                InstrumentationRegistry.getInstrumentation().getTargetContext(),
                AppDatabase.class,
                TEST_DB
        ).addMigrations(*ALL_MIGRATIONS).build().apply {
            getOpenHelper().getWritableDatabase()
            close()
        }
    }
}

优雅地处理丢失的迁移路径

当升级完数据库,某些设备上仍然运行的是旧的数据库版本。如果 Room 无法找到迁移规则将老的数据库版本升级,将会抛出 IllegalStateException 异常。

为了防止这种情况下 app 崩溃,在创建数据库对象时调用 builder的fallbackToDestructiveMigration()方法

这个方法就是告诉 Room 当缺少迁移路径文件丢失时,以破坏性的方式回滚数据库表

提醒:配置了这个属性,当迁移路径丢失时,Room将会彻底删除数据表

破坏性重建回退逻辑包含其他几个选项:

  • 如果在特定数据库版本因无法解决迁移路径而出现错误,使用fallbackToDestructiveMigrationFrom().这个方法表明你只希望 Room在数据库尝试从一个版本迁移到某个错误版本材质行回退逻辑。
  • 仅当数据库降级时执行破坏性重建,请使用fallbackToDestructiveMigrationOnDowngrade()

升级到 Room 2.2.0处理列的默认值

Room 2.2.0新增列队默认值支持,@ColumnInfo(defaultValue = ""). 列的默认值对数据库和实体来说是很重要的,在迁移期间,Room 将对其验证。如果你的数据库是由 Room, 2.2.0之前版本创建的您可能需要为以前添加的Room未知默认值提供迁移。

例如,在版本号为1的数据库中,有一个 Song 实体:

Song Entity, DB Version 1, Room 2.1.0

@Entity
data class Song(
    @PrimaryKey
    val id: Long,
    val title: String
)

在版本号为2的数据库中,一个非空列被添加了:

Song Entity, DB Version 2, Room 2.1.0

@Entity
data class Song(
    @PrimaryKey
    val id: Long,
    val title: String,
    val tag: String // added in version 2
)

从版本1迁移到版本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 ''")
    }
}

在2.2.0之前的 Room 版本中,这种迁移是没有问题的。但是一旦升级到2.2.0并且通过@CoulumnInfo设置默认值添加到同一列就会有问题。通过 ALTER TABLE,Song实体不仅新增了一列而且还为这列设置了默认值。 但是,2.2.0之前的Room版本没有意识到这种更改,从而导致具有全新安装的应用程序用户与从版本1迁移到版本2的用户之间的架构不匹配。具体而言, 版本2将不包含默认值。 然后,对于这种情况,必须提供迁移,以便数据库模式在应用程序的各个用户之间保持一致,因为一旦在实体类中定义默认值后,Room 2.2.0便会对其进行验证。 所需的迁移类型涉及:

  • 在实体中使用@ColumnInfo声明默认值。
  • 数据库版本号+1
  • 提供一种实现删除和重新创建策略的迁移,该策略允许将默认值添加到已创建的列中。

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")
    }
}

注意:如果数据库回退到破坏性迁移,或者您没有这样的迁移(可能已经添加了列和默认值),则不需要此迁移。


Room官方文档(翻译)0.概览

Room官方文档(翻译)1.使用Room实体定义数据

Room官方文档(翻译)2.定义对象间的关系

Room官方文档(翻译)3.在数据库中创建视图

Room官方文档(翻译)4.使用Room DAOs访问数据

Room官方文档(翻译)5.迁移数据库

Room官方文档(翻译)6.测试数据库

Room官方文档(翻译)7.使用Room引用复杂数据