从Room源码看抽象与封装——数据库的升降级
目录
源码解析目录
从Room源码看抽象与封装——SQLite的抽象
从Room源码看抽象与封装——数据库的创建
从Room源码看抽象与封装——数据库的升降级
从Room源码看抽象与封装——Dao
从Room源码看抽象与封装——数据流
前言
上篇文章讲了Room数据库的创建流程,其中刻意忽略了很重要的一个环节,那就是数据库的升降级。如果真的能忽略就好了,在平时的应用开发中,我们有时候刻意回避这个问题,甚至在定义数据库表时,就刻意增加一些冗余字段,为的就是尽量避免数据库的升级。的确,数据库升降级是个“危险”的操作,一不留神就可能破坏原有的数据库中的数据。但是,对于一个ORM框架而言,这才是真正体现水平的地方。
回顾一下数据库的升降级是在什么地方实现的:
public class RoomOpenHelper extends SupportSQLiteOpenHelper.Callback {
//Delegate类的定义就在下方
@NonNull
private final Delegate mDelegate;
@Override
public void onUpgrade(SupportSQLiteDatabase db, int oldVersion, int newVersion) {
//以下是伪代码
boolean migrated = false;
//配置了相应的数据库升级方法
if (has migrations) {
//升级前回调
mDelegate.onPreMigrate(db);
//我们定义的升级数据库的方法
migrate(db);
//验证数据库升级是否正确
mDelegate.validateMigration(db);
//升级后回调
mDelegate.onPostMigrate(db);
migrated = true;
}
//没有升级并且允许以重建表的形式升级的话(之前的数据会完全丢失)
if (!migrated && allowDestructiveMigration) {
//丢弃原有的所有数据库表
mDelegate.dropAllTables(db);
//创建新的表
mDelegate.createAllTables(db);
}
}
@Override
public void onDowngrade(SupportSQLiteDatabase db, int oldVersion, int newVersion) {
//升降级是统一处理的
onUpgrade(db, oldVersion, newVersion);
}
public abstract static class Delegate {
public final int version;
public Delegate(int version) {
this.version = version;
}
//丢弃原有的数据库表,创建新的表,也是一种升级策略
protected abstract void dropAllTables(SupportSQLiteDatabase database);
protected abstract void createAllTables(SupportSQLiteDatabase database);
protected abstract void onOpen(SupportSQLiteDatabase database);
protected abstract void onCreate(SupportSQLiteDatabase database);
//验证数据库升级的完整性
protected abstract void validateMigration(SupportSQLiteDatabase db);
//升级前
protected void onPreMigrate(SupportSQLiteDatabase database) {
}
//升级后
protected void onPostMigrate(SupportSQLiteDatabase database) {
}
}
}
数据库的升降级是在RoomOpenHelper
中被实现的,具体的升降级“行为”肯定是要我们去实现的。可以看出,RoomOpenHelper.Delegate
定义的方法中,除了onCreate
和onOpen
,其它方法都是为了处理数据库升降级。其中dropAllTables
,createAllTables
,validateMigration
,onPreMigrate
和onPostMigrate
均有注解处理器帮我们实现,我们需要关心的就是定义各个版本之间的升降级“行为”。
1. 数据库升级的抽象
Room将数据库升级抽象成了Migration
类:
public abstract class Migration {
public final int startVersion;
public final int endVersion;
/**
* 从 startVersion 到 endVersion 的“迁移”
*/
public Migration(int startVersion, int endVersion) {
this.startVersion = startVersion;
this.endVersion = endVersion;
}
/**
* 具体的数据库迁移行为,不可以使用Dao
*/
public abstract void migrate(@NonNull SupportSQLiteDatabase database);
}
看上去还是很简单,我们只需要定义从startVersion到endVersion的具体迁移行为就可以了。那我们就先定义两个:
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
//一般而言,这里都是通过execSQL方法去执行一些建表、修改表等SQL
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")
}
}
具体的Migration定义好了,我们要怎么传递给数据库呢?肯定还是要通过RoomDatabase.Builder
:
Room.databaseBuilder(applicationContext, AppDatabase::class.java, "database-name")
.addMigrations(MIGRATION_1_2, MIGRATION_2_3)
.build()
2. 保存Migration
正如上面展示的那样,数据库上的Migration会有多个,需要把这些Migration合理地保存下来,便于之后升级时使用。来看看Room是如何保存这些Migration的:
public abstract class RoomDatabase {
public static class Builder<T extends RoomDatabase> {
//保存Migration的容器
private final MigrationContainer mMigrationContainer;
Builder(@NonNull Context context, @NonNull Class<T> klass, @Nullable String name) {
//...
mMigrationContainer = new MigrationContainer();
}
@NonNull
public Builder<T> addMigrations(@NonNull Migration... migrations) {
//...
mMigrationContainer.addMigrations(migrations);
return this;
}
}
public static class MigrationContainer {
//Migration最终被保存在了SparseArray中
private SparseArrayCompat<SparseArrayCompat<Migration>> mMigrations =
new SparseArrayCompat<>();
public void addMigrations(@NonNull Migration... migrations) {
for (Migration migration : migrations) {
addMigration(migration);
}
}
private void addMigration(Migration migration) {
final int start = migration.startVersion;
final int end = migration.endVersion;
SparseArrayCompat<Migration> targetMap = mMigrations.get(start);
if (targetMap == null) {
targetMap = new SparseArrayCompat<>();
//外层SparseArray以startVersion为键
mMigrations.put(start, targetMap);
}
Migration existing = targetMap.get(end);
if (existing != null) {
Log.w(Room.LOG_TAG, "Overriding migration " + existing + " with " + migration);
}
//内层SparseArray以endVersion为键
targetMap.append(end, migration);
}
//...
}
}
很明显,Migration被保存在了MigrationContainer
中。MigrationContainer
顾名思义,就是保存Migration的容器。从MigrationContainer
的实现可以看出,MigrationContainer
使用了一个二维SparseArray
来最终保存Migration。这个二维SparseArray
的第一维(外层)以Migration的startVersion作为键,Migration的startVersion如果一样就会被放在一起;第二维(内层)以Migration的endVersion作为键,startVersion相同的情况下,Migration按照endVersion从小到大依次排列。
以二维SparseArray
作为存储Migration的数据结构的合理性在于,首先,Migration会有多个,并且Migration的startVersion和endVersion是Migration的“身份标志”,这适合用一个二维的数组来保存;其次,不同Migration的startVersion/endVersion之间是“稀疏”的,所以更适合使用一个“稀疏”的二维数组来保存。
如上图所示,假设我们的数据库有四个版本,每个箭头都表示了从一个版本向一个版本升级的Migration,那么这些Migration在二维SparseArray
中大概是这么存储的:
其中白色方格代表第一维SparseArray
以Migration的startVersion作为键,相同startVersion的Migration被放在了一起。灰色方格代表第二维具体存储了某一个Migration,其中的数字1-2表示从版本1到版本2的Migration。可以看出在第二维的SparseArray
中,Migration按照endVersion从小到大依次排列。
3. 如何升级数据库
上篇文章讲了从Room.databaseBuilder
方法,到最后创建出数据库的整个流程,这里复习一下。
我们在RoomDatabase.Builder
上配置的各种属性,最终会汇集到一个叫DatabaseConfiguration
的类中,然后被传递给了我们的RoomOpenHelper
,DatabaseConfiguration
中自然也包含有MigrationContainer
。之前分析RoomOpenHelper
,其onUpgrade
方法都被我替换为了伪代码,现在可以真正来看看其实现了:
public class RoomOpenHelper extends SupportSQLiteOpenHelper.Callback {
//其中包含有我们关心的MigrationContainer
@Nullable
private DatabaseConfiguration mConfiguration;
@Override
public void onUpgrade(SupportSQLiteDatabase db, int oldVersion, int newVersion) {
boolean migrated = false;
if (mConfiguration != null) {
//通过migrationContainer的findMigrationPath找到正确的升级路径,然后按顺序迁移就完了
List<Migration> migrations = mConfiguration.migrationContainer.findMigrationPath(
oldVersion, newVersion);
if (migrations != null) {
mDelegate.onPreMigrate(db);
for (Migration migration : migrations) {
//我们定义的升级行为在这里被调用
migration.migrate(db);
}
//验证升级是否正确
mDelegate.validateMigration(db);
mDelegate.onPostMigrate(db);
updateIdentity(db);
migrated = true;
}
}
//如果数据库版本发生变化,必须定义相应的 Migration
//除非我们通过RoomDatabase.Builder设置了可以通过destruct进行升级
if (!migrated) {
if (mConfiguration != null
&& !mConfiguration.isMigrationRequired(oldVersion, newVersion)) {
//destruct指的就是丢弃旧表,创建新表;所有之前的数据都会被丢弃
mDelegate.dropAllTables(db);
mDelegate.createAllTables(db);
} else {
throw new IllegalStateException("...");
}
}
}
}
数据库升级本身是简单的,调用我们定义的Migration
类上的migrate
方法就可以了。关键在于,如何找到正确的升级路径。
如上图所示,假设我们需要把数据库从版本1升级到版本4,那么正确的升级路径是1->3->4
,而不是1->2->3->4
。如上文所说,Migration被保存在了二维SparseArray中,所以说MigrationContainer.findMigrationPath
的实现思路就是,先通过起始版本号StartVersion(=1)找到第一维的SparseArrayCompat<Migration>
,然后再通过EndVersion从大往小找到合适的Migration(1->3);之后修改起始版本号StartVersion(=3)重复刚才的步骤(3->4),依次类推,直到StartVersion不小于EndVersion为止。具体代码就不展示了。
Room是如何升级数据库已经介绍完了,虽然说Room对于数据库的升级做了良好的抽象与封装,一切被封装到了一个简单的Migration
类当中,我们要做的就是创建几个Migration
类的具体对象就可以了。但是,这仅仅是对使用层面上而言。使用层面的简单的确会降低我们犯错的几率,但是,数据库升级仍然是一项“危险”操作,主要原因就在于Migration
类中定义的升级行为并不见得是对的,假如我们的数据库从版本1通过我们的Migration
升级到了版本2,升级完成后,却和我们直接定义的数据库版本2的表结构是不一致的,那么,必然是我们定义的Migration
有问题,这一问题应该尽早被发现,所以数据库升级完成后,会调用RoomOpenHelper.Delegate
上的验证方法validateMigration
。如上篇文章所说,这个方法在AppDatabase_Impl
中被实现,并且特别的冗长。冗长只是它的表现形式,validateMigration
实现的思想是特别简单的,就是验证升级之后的TableInfo
和我们直接定义的,不需要升级的数据库的TableInfo
是否相等,因为数据库的Table往往不止一个,所以才导致这个方法特别的长。有了这样的验证,问题可以被尽早发现,这就大大降低了数据库升级时犯错的几率。
Room定义了Migration
方便我们升级数据库,并且升级完之后还帮我们验证升级的完整性(表结构的正确性),真是非常贴心了,这还不算完,Room还提供了测试工具方便我们对数据库进行测试(数据的正确性),更多内容见官方文档。
4. 数据库的降级
上面都在讨论数据的升级,没有讲数据库的降级,但是,正如文章开头所说,升降级是统一处理的:
public class RoomOpenHelper extends SupportSQLiteOpenHelper.Callback {
@Override
public void onDowngrade(SupportSQLiteDatabase db, int oldVersion, int newVersion) {
onUpgrade(db, oldVersion, newVersion);
}
}
并不是说Migration
的startVersion就一定要小于endVersion,反过来也是可以的,反过来对应的就是降级。并且MigrationContainer.findMigrationPath
方法也是统一处理升降级的,只不过上文都只是阐述了升级这一种情况。
5. 总结
数据库的升降级是一项“危险”的操作,Room通过如下几个方面来化解这种危险。
- 将数据库的升降级抽象为一个类
Migration
,它包含了数据库升降级的全部信息:startVersion、endVersion和migrate。通过扩展Migration
类,我们可以方便地定义一个个具体的升降级“行为”。数据库升降级在使用层面被大大简化。 - 把各个
Migration
存储在MigrationContainer
的二维SparseArray
中。这种数据结构方便查找出最佳的升降级路径,高效升降级。 - 数据库升降级之后,Room会通过
RoomOpenHelper.Delegate
的validateMigration
方法帮我们验证升降级后数据库表结构的正确性。 - Room提供了测试工具方便我们测试数据库,特别适合于验证数据库升降级前后数据迁移的正确性。
Room可以以丢弃原有表,创建新表的方式完成数据库的升降级(上文所说的destruct),但是这种方式会导致之前的数据完全丢失,并不是一种很好的升降级方式,默认是不会使用这种方式的(默认行为是,如果你没有定义对应的升降级Migration,直接抛出异常)。如果你希望使用destruct的方式,可以通过RoomDatabase.Builder
进行相应的设置,具体使用方式可以查看官方文档(应用尚处于开发阶段时会比较有用)。