前言
最近在用Room时候,由于很大意,错误的升级数据库。导致灰度期间出现了不少crash。这篇文章就来纪念一下自己的“年少无知”吧。
这篇文章翻译于Google的官方博客(自备梯子)https://medium.com/androiddevelopers/understanding-migrations-with-room-f01e04b07929,是我踩坑后搜到的,贴出来希望大家避免掉坑里吧~
正文
删除了一些原文中的“废话”,直接上处理代码。
有兴趣看更多细节的小伙伴,可以直接看原文。
前置条件,我们现在的app版本中已经建了这样的数据库:
@Database(entities = {User.class}, version = 1) public abstract class UsersDatabase extends RoomDatabase
如果我们在下一个版本的app中改了数据库的表结构,那么在不同的场景下会出现不同的问题:
一、场景1:vesion不变 - crash
此时运行app,crash欢迎你:
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. .
假设我们此时增加了版本号:
@Database(entities = {User.class}, version = 2) public abstract class UsersDatabase extends RoomDatabase
我们会遇到另一个问题:
二、场景2:增加vesion,但不提供Migration - crash
当我们把version从1改到了2。此时兴高采烈的从老版本升级上来后...crash欢迎你:
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.
三、场景3:增加vesion,使用fallback migration - 数据被清除
不提供自定义Migration,又不想引发crash,那么可以试试这个:
database = Room.databaseBuilder(context.getApplicationContext(), UsersDatabase.class, "Sample.db") .fallbackToDestructiveMigration() .build();
不过,用之前请好好理解它的意思:
Room启动时将检测version是否发生增加,如果有,那么将找到Migration去执行特定的操作。如果没有因为 fallbackToDestructiveMigration()。将会删除数据库并重建...
此时的确不会crash,但所有数据丢失。
四、场景4:vesion增加,提供Migration - 数据正常
如果version发生变化,即使表结构没有变化仍然需要提供Migration,那么此时应该怎么做呢?
@Database(entities = {User.class}, version = 2) public abstract class UsersDatabase extends RoomDatabase { // … 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();
五、场景5:改表结构
如果我们此时又想升级数据,并且这次需要更改表结构:给user表加一个last_update的字段。那么此时我们改怎么办?
1、先增加version
```kotlin @Database(entities = {User.class}, version = 3) public abstract class UsersDatabase extends RoomDatabase
2、提供Migration
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"); } };
PS:很多老铁,看到这可能觉得头疼。还得写这么多sql。其实有一个取巧的方式,当在 User.class增加完字段,编译后全局搜索3.json。你会发现sql语句已经被生成出来了。
3、修改Room.databaseBuilder
database = Room.databaseBuilder(context.getApplicationContext(), UsersDatabase.class, "Sample.db") .addMigrations(MIGRATION_1_2, MIGRATION_2_3) .build();
大功告成,基本的升库的用法我们已经了解完了。但是这也只是基本的用法,我相信有追求的小伙伴,可能需要来点更“刺激”的才行~
六、复杂的Migration
延续我们上边的表结构,此时我们想改表:把刚才增加的Int型的last_update改成String。
我猜有经验的老司机,已经猜到需要怎么做了:
- 创建一个新的临时表,
- 将数据从users表复制到临时表,
- 删了users表
- 将临时表重命名为users
那么针对此次更新,我们需要提供这样的Migration:
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"); } };
此时我们用version3版本的app升级到version4的app的确没有问题,很爽。不过我们多想一个问题:如果从version1版本的用户直接升到version4版本会怎么样?
其实不会有问题!Room会对这种用户一直执行1-2的Migration,2-3的Migration已经3-4的Migration。这很方便,不过如果你追求极致的性能和体验,你可以提供一个1-4的Migration,比如这样:
static final Migration MIGRATION_1_4 = new Migration(1, 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"); } };
然后把Migration加进来就ok了。
database = Room.databaseBuilder(context.getApplicationContext(), UsersDatabase.class, "Sample.db") .addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_1_4) .build();
尾声闲谈
首先来说Room的基本用法其实主流的ORM库并没有什么不一样。在api体验上个人感觉各有千秋吧,不过升级这一块,我觉得Room还是蛮方便的。
再聊聊官方极力“吹捧”的:对RxJava和LiveData的封装。怎么说能...个人最初的体验的确是很爽,但是一旦表结构变得复杂起来之后,由其是多表查询的时候。你会发现你的LiveData的回调会爆炸的...所以个人对这个功能持保留意见,有好用的地方也有尴尬的时候...
我自己对Room的感觉就是...还行...哈哈~