理解Room数据库的迁移(Migration)

这是一篇来自Android官方介绍Room迁移的文章,主要是最近公司项目处于快速迭代状态,数据库经常更新,数据迁移就成了一个大问题,这里也学习了一个Room官方对迁移的理解和实践。

首先需要配置一下:

dependencies {
  def room_version = "2.2.5"

  implementation "androidx.room:room-runtime:$room_version"
  annotationProcessor "androidx.room:room-compiler:$room_version" // For Kotlin use kapt instead of annotationProcessor

  // optional - Kotlin Extensions and Coroutines support for Room
  implementation "androidx.room:room-ktx:$room_version"

  // optional - RxJava support for Room
  implementation "androidx.room:room-rxjava2:$room_version"

  // optional - Guava support for Room, including Optional and ListenableFuture
  implementation "androidx.room:room-guava:$room_version"

  // Test helpers
  testImplementation "androidx.room:room-testing:$room_version"
}

原文地址为:地址

原文如下:

使用 SQLiteAPI 对数据库迁移总是觉得自己在拆炸弹,哪怕是错了一步,程序就在用户手中崩溃。但是你使用 Room 来处理数据库的操作,迁移就是是打开一个开关一样简单.

使用了 Room,如果你更改了 数据库的架构,但是没有更新数据库的版本,你的 app 就将崩溃。如果你更新了数据库版本,但是没有提供任何的 迁移策略,你的app会崩溃,或者你的数据库表被删除了、或者用户数据会丢失。不要靠瞎想在实现数据库的迁移。深入理解 Room,那么迁移你的数据库将会更有信息。


数据库迁移的引擎

SQLite API 都干了些啥

SQLite 数据库依靠 数据库版本来处理数据库架构的变化。更精确点,每次你新增、移除、修改你的数据库表时,你必须得增加数据库版本数字,然后更新 SQLiteOpenHelper.onUpgrade 方法。这就是告诉 SQLite 从旧版本迁移到新版本,需要做什么事情。
当你的 App 开始使用数据库时, SQLiteOpenHelper.onUpgrade 也将触发。当打开数据库时,SQLite 首先将会处理版本更新。

Room 都干了些啥

Room 提供了一个抽象层来来缓解 SQLite 的迁移,展现形式是以 Migration 类 来实现的。Migration 类定义了从指定版本迁移到另外一个版本的动作。Room 使用了它自己的 SQLiteOpenHelper 的实现,在 onUpgrade 方法中,将会触发你定义的迁移动作。
这里展示了当第一次进入数据将会发生的事情:

  1. Room 数据将会被建立
  2. SQLiteOpenHelper.onUpgrade 被调用,Room触发迁移动作
  3. 数据库被打开

如果你没有提供迁移策略,但是你却增加了数据库版本,你的app可能会被崩溃或者你的数据将会被丢失,至于产生的结果,基于我们将会谈到的情形。

在迁移中,一个很重要的点就是 identity hash String. Room 就是通过唯一这个 identity String 区分数据库的版本。当前的数据库中有个 configuration table 保存了 identity String. 如果不要太惊讶你查看数据库时会有一张 room_maste_table 表。

我们来做个简单的例子,有一个 user 的表,存在两列:

  1. ID ,int类型,同时也是主键
  2. 用户名,String类型的

user 表属于版本1数据库的一部分,通过 SQLiteOpenHelper API来实现的。
我们来想一下,如果用户已经使用了这个版本,此时你想用 Room 来管理数据库,我们来看一下 Room 怎么处理这些场景。


迁移 SQLite API 代码 到 Room

我们假设 User 实例代码、UserDao 和 data access object class 都创建完成了,我们注重于 UserDatabase 类,它继承自RoomDatabase:


@Database(entities = {User.class},version = 1}
public abstract class UserDatabase extends RoomDatabase


情景一:保持数据库版本不变 — app崩溃

如果我们保持数据库版本不变,直接运行我们的App,Room在背后所做的事情如下:

第一步:尝试打开数据库

比对数据库的identity值:当前的版本的 identity hash与在 room_master_table 中 identity hash比较。但是因为identity hash没有被存储,因此app 将会奔溃:

java.lang.IllegalStateException: Room cannot verify the data integrity. Looks like you’ve changed schema 
but forgot to update the version number. You can simply fix this by increasing the version number.

如果你修改了数据库的架构,但是没有更新数据库的版本,Room 总会报 IllegalStateException。

我们听从它的意见,将版本修改为2:


@Database(entities = {User.class},version = 2}
public abstract class UserDatabase extends RoomDatabase


情景二:版本更新了,但是没有提供迁移策略 — app崩溃

我们按照以下步骤再次运行 Room:

第一步:更新数据库版本从1(已经安装到设备上了)到2

因为没有迁移策略,所以应用崩溃报 IllegalStateException.❌

java.lang.IllegalStateException: A migration from 1 to 2 is necessary. 
Please provide a Migration in the builder or call fallbackToDestructiveMigration in the builder 
in which case Room will re-create all of the tables.

如果你没有提供迁移策略,Room 就会报 IllegalStateException 异常


情景三:版本更新了,回退到破坏性迁移 — 数据库数据被清空了

如果你不想提供迁移策略,而且你特别指定了在更新数据库版本时,数据库数据将会被清空,那么调用fallbackToDestructiveMigration可以满足你的要求:

database = Room.databaseBuilder(context.getApplicationContext(),
                UsersDatabase.class, "Sample.db")
                .fallbackToDestructiveMigration()
                .build();

接下来我们再次运行,Room 将会做一下动作:

第一步:尝试从版本1更新到版本2

因为现在没有迁移策略,而且我们还设定了回退到破坏性迁移,那么所有的数据库表将会被删除掉,新的identity hash 将会被插入。

第二步:尝试重新打开数据库

因为当前版本的identity hash 和 插入的identity hash是同一个,数据库打开。✅

现在打开时,我们的app没有奔溃,但是我们丢失了所有数据。做这种操作时,先看看是不是真要这么操作😄。


情景四:版本更新了,也提供了迁移策略 — 数据健在

为了保存用户数据,我们需要实现一个迁移策略。因为数据库架构并没有发生更改,因此我们只需要提供一个空的迁移实现即可,然后告诉 Room 来使用它即可。

static final Migration MIGRATION_1_2 = new Migration(1, 2) {
    @Override
    public void migrate(SupportSQLiteDatabase database) {
        // 既然什么都没有更改,那就来个空的实现。
    }
};

database =  Room.databaseBuilder(context.getApplicationContext(),
        UsersDatabase.class, "Sample.db")
        .addMigrations(MIGRATION_1_2)
        .build();

当运行App时,Room做了一下事情:

第一步:尝试更新版本1到版本2
  1. 触发空的迁移实现✅
  2. 更新新的 identity hashroom_master_table 中✅
第二步:尝试打开数据库
  1. 比较 identity hash 发现一致,打开数据库✅.

到现在为止,我们的app可以打开了,而且我们的用户数据已经迁移成功了💐。


简单的数据库架构发生改变,如何迁移?

在用户表 user 中,新增一列 last_update. 因为更改了 User类,在 UserDatabase 中我们需要做如下的更改:

  1. 将数据库版本更改到 3

@Database(entities = {User.class},version = 3}
public abstract class UserDatabase extends RoomDatabase

  1. 添加一个从版本2到版本3的迁移策略
static final Migration MIGRATION_2_3 = new Migration(2, 3) {
    @Override
    public void migrate(SupportSQLiteDatabase database) {
        database.execSQL("ALTER TABLE users ADD COLUMN last_update INTEGER");
    }
};
  1. 添加迁移策略到 Room database builder中:
database = Room.databaseBuilder(context.getApplicationContext(),
           UsersDatabase.class, "Sample.db")
           .addMigrations(MIGRATION_1_2, MIGRATION_2_3)
           .build();

当我们运行 app 时,将会执行以下步骤:

  1. 尝试数据库版本更新,从版本2升级到版本3

触发 MIGRATION_2_3, 新增user 表的 last_update 列,用户数据保留。
更新 identity hash 到 room_master_table 中

  1. 尝试打开数据库

复杂的数据库架构发生改变,如何迁移?

SQLiteALTER TABLE 命令 功能相当有限。举个例子,更改用户user表中的id,将int类型改成 String 类型的,需要以下几个步骤:

  1. 使用新的架构创建一个全新的临时表;
  2. 从用户表中把数据复制到临时表中;
  3. 删除用户表user;
  4. 将临时表改名为用户表user.✅

当使用 Room 时,迁移可能这么写:

static final Migration MIGRATION_3_4 = new Migration(3, 4) {
    @Override
    public void migrate(SupportSQLiteDatabase database) {
        //  创建新的临时表
        database.execSQL( "CREATE TABLE users_new (userid TEXT, username TEXT, last_update INTEGER, PRIMARY KEY(userid))" );
       // 复制数据
        database.execSQL( "INSERT INTO users_new (userid, username, last_update) SELECT userid, username, last_update FROM users" );
		// 删除表结构
        database.execSQL( "DROP TABLE users" );
		// 临时表名称更改
        database.execSQL( "ALTER TABLE users_new RENAME TO users" );
    }
};

多版本数据库增量如何迁移?

你有一个非常老的数据库版本为1,现在线上最新的为4。当前我们已经定义了以下几种迁移:

  1. 版本1 —> 版本2
  2. 版本2 —> 版本3
  3. 版本3 —> 版本4

那么 Room 将会触发所有的迁移策略,一个接一个去执行。

另外 Room 可以处理夸版本更新:直接可以定义一个迁移,直接一步从版本1到版本4,从而使迁移的进程更快。

static final Migration MIGRATION_1_4 = new Migration(1, 4) {
    @Override
    public void migrate(SupportSQLiteDatabase database) {
        // Create the new table
        database.execSQL("CREATE TABLE users_new (userid TEXT, username TEXT, last_update INTEGER, PRIMARY KEY(userid))");
        
        // Copy the data
        database.execSQL("INSERT INTO users_new (userid, username, last_update) SELECT userid, username, last_update FROM users");
        
		// Remove the old table
        database.execSQL("DROP TABLE users");
        
		// Change the table name to the correct one
        database.execSQL("ALTER TABLE users_new RENAME TO users");
    }
};

添加到迁移序列中:

database = Room.databaseBuilder(context.getApplicationContext(),
          UsersDatabase.class, "Sample.db")
         .addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_1_4)
         .build();

我们写的 Migration.migrate 的实现并非 实时编译的,并不像写在 DAO中写 query 语句那样。所以需要做好迁移之后的 测试工作。

结论

本身而言,数据库迁移并不是一件很高频的事件,但是它对数据库的设计者有很高的要求,需要懂得需求的扩展性,即需要知道现在需要什么,将来需要什么。使用 Room 做迁移,本身来说并不是特别困难,最困难的是该如何面对 一直在变化的需求。本人现在遇到的问题是,线上存在多个版本的数据库,从版本1到版本7的都有分布,这并非本人闲的蛋疼,而是需求一直在变化,经常更新数据会存在问题。

我当前的做法是,在需求未稳定之前,我每次的更新都会使用fallbackToDestructiveMigration,数据库都会被重建,同时数据也会被清除。待需求稳定之后,缓步向前推进时,再切换到 Migration 模式,进行数据库版本之间的迁移。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值