Room数据库升级

随着业务的变化,我们的数据库可能也需要做一些调整,例如,我们的数据表可能需要增加一个新字段。这篇文章我们将介绍Room如何升级数据库,升级过程中可能会遇到哪些问题,以及相对于Sqlite和其它ORM,Room有哪些特性值得我们去学习和使用。


Android提供了一个名为Migration的类,来完成Room的升级。

public Migration(int startVersion, int endVersion)

Migration有两个参数,startVersionendVersion。startVersion表示当前版本(手机上安装的版本),endVersion表示将要升级到的版本。如果你的手机中的应用程序数据库的版本为1,那么下方Migration会将你的数据库版本从1升级到2。

static final Migration MIGRATION_1_2 = new Migration(1, 2)
{
    @Override
    public void migrate(@NonNull SupportSQLiteDatabase database)
    {
        //执行升级相关操作
    }
};

以此类推,如果你的数据库需要从2升级到3,则需要写这样一个Migration。

private static Migration MIGRATION_2_3 = new Migration(2, 3)
{
    @Override
    public void migrate(@NonNull SupportSQLiteDatabase database)
    {
        //执行升级相关操作
    }
};

如果用户手机上安装的应用程序数据库版本为1,而当前要安装的应用程序数据库版本为3,这种情况该怎么办呢?这种情况下,Room会先判断当前有没有从1->3的Migration升级方案,如果有,就直接执行从1->3的升级方案,如果没有,那么Room会按照顺序先后执行Migration(1, 2)->Migration(2, 3)以完成升级。

写好Migration之后,我们还需要通过addMigrations()方法,将升级方案添加到Room。

Room.databaseBuilder(context.getApplicationContext(), MyDatabase.class, DATABASE_NAME)
    .addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_1_3)
    .build();

异常处理

假设此时我们升级系统版本为4,却没有为此写相应的Migration,则会出现一个IllegalStateException异常。

Caused by: java.lang.IllegalStateException: A migration from 3 to 4 was required but not found.
 Please provide the necessary Migration path via RoomDatabase.Builder.addMigration(Migration ...) or 
allow for destructive migrations via one of the RoomDatabase.Builder.fallbackToDestructiveMigration* methods.
        at androidx.room.RoomOpenHelper.onUpgrade(RoomOpenHelper.java:117)
        at androidx.sqlite.db.framework.FrameworkSQLiteOpenHelper$OpenHelper.onUpgrade(FrameworkSQLiteOpenHelper.java:124)
        at android.database.sqlite.SQLiteOpenHelper.getDatabaseLocked(SQLiteOpenHelper.java:400)
        at android.database.sqlite.SQLiteOpenHelper.getReadableDatabase(SQLiteOpenHelper.java:322)
        at androidx.sqlite.db.framework.FrameworkSQLiteOpenHelper$OpenHelper.getReadableSupportDatabase(FrameworkSQLiteOpenHelper.java:103)
        at androidx.sqlite.db.framework.FrameworkSQLiteOpenHelper.getReadableDatabase(FrameworkSQLiteOpenHelper.java:58)

这是因为Room在升级过程中没有匹配到相应的Migration。为了防止出现升级失败导致应用程序Crash的情况,我们可以在创建数据库时加入fallbackToDestructiveMigration()方法。该方法能够在出现升级异常时,重新创建数据库表。虽然应用程序不会Crash,但由于数据表被重新创建,所有的数据也将会丢失。

Room.databaseBuilder(context.getApplicationContext(), MyDatabase.class, DATABASE_NAME)
    .fallbackToDestructiveMigration()
    .addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_1_3)
    .build();

如何修改Room数据库的版本号呢?直接通过@Database标签中的version属性进行修改就可以了。

@Database(entities = {Student.class}, version = 1)

Schema文件

在没有Room之前,如果想要验证数据库的修改是否符合我们的预期,我们可能需要找到这个数据库文件,然后使用第三方Sqlite查看工具,对其进行查看和验证。另外,我们希望能够查看到数据库的历次升级情况,似乎也只能通过代码版本控制工具来查看源代码。这无疑是非常耗时耗力的。为此,Room提供了一项功能,在每次数据库的升级过程中,它都会为你导出一个Schema文件,这是一个json文件,里面包含了数据库的所有基本信息。有了这些文件,开发者们就能知道数据库的历史变更情况,极大地方便了开发者们排查问题。Schema文件是默认导出的,你只需要指定它导出的位置即可。

appbuild.gradle文件中指定Schema文件的导出位置。

android {
    defaultConfig {
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = ["room.schemaLocation": "$projectDir/schemas".toString()]//指定数据库schema导出的位置
            }
        }
    }
}

这样,当你的数据库生成后,你便可以在项目的根目录下看见这些文件。数字代表着数据库的版本号。

v2-908d91a5c4ee6a448424f495b93c5336_720w

Room默认你希望导出Schema文件,如果你不想导出这些文件,那么可以在数据库标签@Database中指定exportSchema = false。但我们建议你导出这些文件,并且,如果你有使用版本控制系统(如Git)来管理代码,可以将这些文件一并提交到仓库。

@Database(entities = {Student.class}, exportSchema = false, version = 1)

销毁与重建策略

在Sqlite中修改表结构会比较麻烦。比如我们希望将Student表中的age字段类型从INTEGER改为TEXT

面对此类需求,最好的方式就是采用“销毁与重建策略”,该策略大致分为以下几个步骤:

1.创建一张符合我们要求的临时表temp_Student

2.将数据从旧表Student拷贝至临时表temp_Student

3.删除旧表Student

4.将临时表temp_Student重命名为Student

static final Migration MIGRATION_3_4 = new Migration(3, 4)
    {
        @Override
        public void migrate(SupportSQLiteDatabase database)
        {
            database.execSQL("CREATE TABLE temp_Student (" +
                    "id INTEGER PRIMARY KEY NOT NULL," +
                    "name TEXT," +
                    "age TEXT)");
            database.execSQL("INSERT INTO temp_Student (id, name, age) " +
                    "SELECT id, name, age FROM Student");
            database.execSQL("DROP TABLE Student");
            database.execSQL("ALTER TABLE temp_Student RENAME TO Student");
        }
    };

通过查看schema升级文件,我们可以看到,age字段已经被修改为TEXT类型了,说明这次的数据库修改升级是成功的。

v2-37c0baac4c1a7765bfee1809589b887b_720w

这篇文章我们介绍了如何使用Migration升级我们的数据库。除此之外,我们还指出了升级过程中可能会出现的异常。Schema文件是Room提供的非常实用的一个特性,它的作用有点类似一个针对数据库的Git仓库,有了它,便能知道数据库的历史变更情况。在开发过程中,Schema文件还可以帮助我们判断对数据库的修改是否成功。最后,我们还介绍了销毁与重建策略,当我们遇到一些复杂情况的时候,可以考虑利用这种策略升级我们的数据库。

项目地址:michaelye/RoomMigrationDemo?

Room数据库升级
滚动到顶部