美文网首页数据库GreenDao工作生活
从Room源码看抽象与封装——数据库的升降级

从Room源码看抽象与封装——数据库的升降级

作者: 珞泽珈群 | 来源:发表于2019-07-04 23:34 被阅读0次

    目录

    源码解析目录
    从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<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中大概是这么存储的:

    Migration存储示意图

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

    3. 如何升级数据库

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

    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<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通过如下几个方面来化解这种危险。

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

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

    相关文章

      网友评论

        本文标题:从Room源码看抽象与封装——数据库的升降级

        本文链接:https://www.haomeiwen.com/subject/uladhctx.html