从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定义的方法中,除了onCreateonOpen,其它方法都是为了处理数据库升降级。其中dropAllTables,createAllTables,validateMigration,onPreMigrateonPostMigrate均有注解处理器帮我们实现,我们需要关心的就是定义各个版本之间的升降级“行为”。

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 {
        //保存Migration的容器
        private final MigrationContainer mMigrationContainer;

        Builder(@NonNull Context context, @NonNull Class klass, @Nullable String name) {
            //...
            mMigrationContainer = new MigrationContainer();
        }
        
        @NonNull
        public Builder addMigrations(@NonNull Migration... migrations) {
            //...
            mMigrationContainer.addMigrations(migrations);
            return this;
        }
    }
    
    public static class MigrationContainer {
        //Migration最终被保存在了SparseArray中
        private SparseArrayCompat> 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 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之间是“稀疏”的,所以更适合使用一个“稀疏”的二维数组来保存。

从Room源码看抽象与封装——数据库的升降级_第1张图片
数据库的升级

如上图所示,假设我们的数据库有四个版本,每个箭头都表示了从一个版本向一个版本升级的Migration,那么这些Migration在二维SparseArray中大概是这么存储的:

从Room源码看抽象与封装——数据库的升降级_第2张图片
Migration存储示意图

其中白色方格代表第一维SparseArray以Migration的startVersion作为键,相同startVersion的Migration被放在了一起。灰色方格代表第二维具体存储了某一个Migration,其中的数字1-2表示从版本1到版本2的Migration。可以看出在第二维的SparseArray中,Migration按照endVersion从小到大依次排列。

3. 如何升级数据库

上篇文章讲了从Room.databaseBuilder方法,到最后创建出数据库的整个流程,这里复习一下。

从Room源码看抽象与封装——数据库的升降级_第3张图片
Room数据库创建流程

我们在RoomDatabase.Builder上配置的各种属性,最终会汇集到一个叫DatabaseConfiguration的类中,然后被传递给了我们的RoomOpenHelperDatabaseConfiguration中自然也包含有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 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方法就可以了。关键在于,如何找到正确的升级路径。

从Room源码看抽象与封装——数据库的升降级_第4张图片
数据库的升级

如上图所示,假设我们需要把数据库从版本1升级到版本4,那么正确的升级路径是1->3->4,而不是1->2->3->4。如上文所说,Migration被保存在了二维SparseArray中,所以说MigrationContainer.findMigrationPath的实现思路就是,先通过起始版本号StartVersion(=1)找到第一维的SparseArrayCompat,然后再通过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通过如下几个方面来化解这种危险。

  1. 将数据库的升降级抽象为一个类Migration,它包含了数据库升降级的全部信息:startVersion、endVersion和migrate。通过扩展Migration类,我们可以方便地定义一个个具体的升降级“行为”。数据库升降级在使用层面被大大简化。
  2. 把各个Migration存储在MigrationContainer的二维SparseArray中。这种数据结构方便查找出最佳的升降级路径,高效升降级。
  3. 数据库升降级之后,Room会通过RoomOpenHelper.DelegatevalidateMigration方法帮我们验证升降级后数据库表结构的正确性。
  4. Room提供了测试工具方便我们测试数据库,特别适合于验证数据库升降级前后数据迁移的正确性。

Room可以以丢弃原有表,创建新表的方式完成数据库的升降级(上文所说的destruct),但是这种方式会导致之前的数据完全丢失,并不是一种很好的升降级方式,默认是不会使用这种方式的(默认行为是,如果你没有定义对应的升降级Migration,直接抛出异常)。如果你希望使用destruct的方式,可以通过RoomDatabase.Builder进行相应的设置,具体使用方式可以查看官方文档(应用尚处于开发阶段时会比较有用)。

你可能感兴趣的:(从Room源码看抽象与封装——数据库的升降级)