首页 > 代码库 > 你应该知道的 Android 数据库更新策略

你应该知道的 Android 数据库更新策略

  • GreenDao 自带更新的问题
  • 自定义更新
    • 1 自定义更新类
    • 2 关联自定义的 MyOpenHelper
  • 更新数据库
    • 1 新增表
    • 2 更新已有表的字段
      • 21 开源方案
      • 22 发现问题
      • 23 改造以解决问题
  • 跨版本升级

转载请注明出处:(http://blog.csdn.net/my_truelove/article/details/70196028)

访问 ruicb.com,一键抵达我的博客!

在涉及数据库的应用中,我们不可能在应用刚上线时,就提前预知未来需要的字段,只能在后期根据新的需求去不断完善。所以,数据库的更新就显得十分重要,因为从最初搭建数据库,你就需要做好后期升级的机制。如果刚开始没有做,等 App 上线了,再想更新数据库以新增表或字段,你会发现是个大问题。

本文以使用 GreenDao 3.2为例,侧重分享更新方案,至于基本配置及使用,网上已经有跟多优秀的译文或者博客,就不再赘述。

更新这块重视的人还不多,所以想记录一下,和大家一起交流。

1 GreenDao 自带更新的问题

GreenDao 3.2 中自带的更新类 DevOpenHelper,是不可用的,如下:

/** WARNING: Drops all table on Upgrade! Use only during development. */
public static class DevOpenHelper extends OpenHelper {
    public DevOpenHelper(Context context, String name) {
        super(context, name);
    }
    public DevOpenHelper(Context context, String name, CursorFactory factory) {
        super(context, name, factory);
    }
    @Override
    public void onUpgrade(Database db, int oldVersion, int newVersion) {
        dropAllTables(db, true);
        onCreate(db);
    }
}

注释明确说明了仅限于开发阶段,从上述代码可以看出,GreenDao 在数据库版本更新时,默认删除所有表再新建,开发阶段无所谓,但这对于线上 App 升级是致命的,这样一来老用户的数据就全丢了,所以不适合用于 App 上线后更新数据库。

可能我们会想,那我们改掉它不就行了吗?改是不行的,因为 DevOpenHelperDaoMaster的内部类,而 Daomaster 是 GreenDao 自动生成的,会在 build 项目时被覆盖重写。

2 自定义更新

看来只能自己写了,撸起袖子就是干。

2.1 自定义更新类

仿照 DevOpenHelper,我们自定义 MyOpenHelper 继承自 OpenHelper,并重写onUpgrade()方法以自己维护更新:

public class MyOpenHelper extends DaoMaster.OpenHelper {

    public MyOpenHelper(Context context, String name) {
        super(context, name);
    }

    public MyOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory) {
        super(context, name, factory);
    }

    @Override
    public void onUpgrade(Database db, int oldVersion, int newVersion) {
        super.onUpgrade(db, oldVersion, newVersion);
        //写自己的更新逻辑
    }
}

此时,就可以在上述注释的位置写自己的逻辑了。

2.2 关联自定义的 MyOpenHelper

光写不行,要让 GreenDao 知道我们使用自定义的更新类,所以在初始化 GreenDao 的地方指明使用 MyOpenHelper,如下:

public class App extends Application {

    private static DaoSession daoSession;

    @Override
    public void onCreate() {
        super.onCreate();
        //使用自定义更新类
        MyOpenHelper helper = new MyOpenHelper(this, "db-name");
        Database db = helper.getWritableDb();
        daoSession = new DaoMaster(db).newSession();
    }

    //对外暴露会话对象 DaoSession
    public static DaoSession getDaoSession() {
        return daoSession;
    }
}

3 更新数据库

关联自定义的更新类之后,下面开始真正的更新逻辑,分别以 新增表更新已有表的字段 为例。

3.1 新增表

随着项目迭代,假设这一版我们需要新增一个数据表,用来保存用户缓存的视频路径,大致步骤如下:

1.新建 VideoCache 对象,用 @Entry 标识一下,加几个属性,再 build 一下项目,GreenDao 会自动帮我们补全 gettersetter 方法,同时生成对应的 VideoCacheDao
2.修改 appbuild.gradle 中声明的数据库版本号,+1;
3.在 MyOpenHelperonUpgrade() 方法中创建新表:

@Override
public void onUpgrade(Database db, int oldVersion, int newVersion) {
    super.onUpgrade(db, oldVersion, newVersion);
    //这么写能更新,但实际还存在跨版本升级问题
    VideoCacheDao.createTable(db, false);
}

4.运行即可;

3.2 更新已有表的字段

同样,随着版本迭代,以前的数据库表需要新增字段以满足现有的需求,以在VideoCache表中新增 FileSize 字段为例。新增字段不同于新增表,更新过程概括来说分为三步:
1. 备份 VideoCache 表到临时表 VideoCache_Temp
2. 删除原来的 VideoCache 表;
3. 新建带有 FileSize 字段的 VideoCache 表;
4. 迁移 VideoCache_Temp 表的数据至新建的 VideoCache

3.2.1 开源方案

上述过程自己实现起来还是有难度的,好在网上有开源的辅助类,直接拿过来,加点注释,如下:

public class MigrationHelper {

    private static final String CONVERSION_CLASS_NOT_FOUND_EXCEPTION =
        "MIGRATION HELPER - CLASS DOESN‘T MATCH WITH THE CURRENT PARAMETERS";
    private static MigrationHelper instance;

    public static MigrationHelper getInstance() {
        if (instance == null) {
            instance = new MigrationHelper();
        }
        return instance;
    }

    public void migrate(Database db, Class<? extends AbstractDao<?, ?>>... daoClasses) {
        //1. 备份表
        generateTempTables(db, daoClasses);
        //2. 删除所有表
        DaoMaster.dropAllTables(db, true);
        //3. 重新创建所有表
        DaoMaster.createAllTables(db, false);
        //4. 恢复数据
        restoreData(db, daoClasses);
    }

    /**
     * 备份要更新的表
     */
    private void generateTempTables(Database db, Class<? extends AbstractDao<?, ?>>... daoClasses) {
        for (int i = 0; i < daoClasses.length; i++) {
            DaoConfig daoConfig = new DaoConfig(db, daoClasses[i]);

            String divider = "";
            String tableName = daoConfig.tablename;
            String tempTableName = daoConfig.tablename.concat("_TEMP");
            ArrayList<String> properties = new ArrayList<>();

            StringBuilder createTableStringBuilder = new StringBuilder();

            createTableStringBuilder.append("CREATE TABLE ").append(tempTableName).append(" (");

            for (int j = 0; j < daoConfig.properties.length; j++) {
                String columnName = daoConfig.properties[j].columnName;

                if (getColumns(db, tableName).contains(columnName)) {
                    properties.add(columnName);

                    String type = null;

                    try {
                        type = getTypeByClass(daoConfig.properties[j].type);
                    } catch (Exception exception) {
                    }

                    createTableStringBuilder.append(divider).append(columnName).append(" ").append(type);

                    if (daoConfig.properties[j].primaryKey) {
                        createTableStringBuilder.append(" PRIMARY KEY");
                    }

                    divider = ",";
                }
            }
            createTableStringBuilder.append(");");

            db.execSQL(createTableStringBuilder.toString());

            StringBuilder insertTableStringBuilder = new StringBuilder();

            insertTableStringBuilder.append("INSERT INTO ").append(tempTableName).append(" (");
            insertTableStringBuilder.append(TextUtils.join(",", properties));
            insertTableStringBuilder.append(") SELECT ");
            insertTableStringBuilder.append(TextUtils.join(",", properties));
            insertTableStringBuilder.append(" FROM ").append(tableName).append(";");

            db.execSQL(insertTableStringBuilder.toString());
        }
    }

    /**
     * 恢复数据
     */
    private void restoreData(Database db, Class<? extends AbstractDao<?, ?>>... daoClasses) {
        for (int i = 0; i < daoClasses.length; i++) {
            DaoConfig daoConfig = new DaoConfig(db, daoClasses[i]);

            String tableName = daoConfig.tablename;
            String tempTableName = daoConfig.tablename.concat("_TEMP");
            ArrayList<String> properties = new ArrayList();

            for (int j = 0; j < daoConfig.properties.length; j++) {
                String columnName = daoConfig.properties[j].columnName;

                if (getColumns(db, tempTableName).contains(columnName)) {
                    properties.add(columnName);
                }
            }

            StringBuilder insertTableStringBuilder = new StringBuilder();

            insertTableStringBuilder.append("INSERT INTO ").append(tableName).append(" (");
            insertTableStringBuilder.append(TextUtils.join(",", properties));
            insertTableStringBuilder.append(") SELECT ");
            insertTableStringBuilder.append(TextUtils.join(",", properties));
            insertTableStringBuilder.append(" FROM ").append(tempTableName).append(";");

            StringBuilder dropTableStringBuilder = new StringBuilder();

            dropTableStringBuilder.append("DROP TABLE ").append(tempTableName);

            db.execSQL(insertTableStringBuilder.toString());
            db.execSQL(dropTableStringBuilder.toString());
        }
    }

    private String getTypeByClass(Class<?> type) throws Exception {
        if (type.equals(String.class)) {
            return "TEXT";
        }
        if (type.equals(Long.class) || type.equals(Integer.class) || type.equals(long.class)) {
            return "INTEGER";
        }
        if (type.equals(Boolean.class)) {
            return "BOOLEAN";
        }

        Exception exception =
            new Exception(CONVERSION_CLASS_NOT_FOUND_EXCEPTION.concat(" - Class: ").concat(type.toString()));
        throw exception;
    }

    private static List<String> getColumns(Database db, String tableName) {
        List<String> columns = new ArrayList<>();
        Cursor cursor = null;
        try {
            cursor = db.rawQuery("SELECT * FROM " + tableName + " limit 1", null);
            if (cursor != null) {
                columns = new ArrayList<>(Arrays.asList(cursor.getColumnNames()));
            }
        } catch (Exception e) {
            Log.v(tableName, e.getMessage(), e);
            e.printStackTrace();
        } finally {
            if (cursor != null) cursor.close();
        }
        return columns;
    }
}

这样,只需要在更新字段时,在 MyOpenHelper 类的 onUpgrade() 方法中:

@Override
public void onUpgrade(Database db, int oldVersion, int newVersion) {
    super.onUpgrade(db, oldVersion, newVersion);
    //更新表的字段
    MigrationHelper.getInstance().migrate(db, VideoCacheDao.class);
}

3.2.2 发现问题

上面的开源方案,使用起来如此顺手,但不知道细心的你发现没,这个更新辅助类是存在问题的:

public void migrate(Database db, Class<? extends AbstractDao<?, ?>>... daoClasses) {
    //1. 备份表
    generateTempTables(db, daoClasses);
    //2. 删除所有表
    DaoMaster.dropAllTables(db, true);
    //3. 重新创建所有表
    DaoMaster.createAllTables(db, false);
    //4. 恢复数据
    restoreData(db, daoClasses);
}

发现其每次都是删除所有表、再新建所有表,这意味着:

当我想更新一张表中的某个字段,我却要传入所有的表对应的 XxxDao.class 对象,即使其它表不需要更新,也会经历 备份删除新建恢复 的过程,效率低下不说,一不小心还容易出问题

在上面,我们这么更新表:

MigrationHelper.getInstance().migrate(db, VideoCacheDao.class);

问题在于,如果你不只是有一张表,在更新某张表的 字段时,如上你只传当前需要更新的表,则其它表的数据都会丢失。明白了吗?没明白的话再好好看看上面的代码。

3.2.3 改造以解决问题

于是我改造了一下,只需要传入你想更新的表即可:

public class MigrationHelper {

    private static final String CONVERSION_CLASS_NOT_FOUND_EXCEPTION =
        "MIGRATION HELPER - CLASS DOESN‘T MATCH WITH THE CURRENT PARAMETERS";
    private static MigrationHelper instance;

    public static MigrationHelper getInstance() {
        if (instance == null) {
            instance = new MigrationHelper();
        }
        return instance;
    }

    public void migrate(Database db, Class<? extends AbstractDao<?, ?>>... daoClasses) {
        //1.备份(同上)
        generateTempTables(db, daoClasses);
        //2. 只删除需要更新的表(改造)
        deleteOriginalTables(db, daoClasses);
        //3. 只创建需要更新的表(改造)
        //DaoMaster.createAllTables(db, false);
        createOrignalTables(db, daoClasses);
        //4. 恢复数据
        restoreData(db, daoClasses);
    }

    /**
     * 备份要更新的表
     */
    private void generateTempTables(Database db, Class<? extends AbstractDao<?, ?>>... daoClasses) {
        //...
    }

    /**
     * 通过反射,删除要更新的表
     */
    private void deleteOriginalTables(Database db, Class<? extends AbstractDao<?, ?>>... daoClasses){
        for (Class<? extends AbstractDao<?, ?>> daoClass : daoClasses) {
            try {
                Method method = daoClass.getMethod("dropTable", Database.class, boolean.class);
                method.invoke(null, db, true);
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 通过反射,重新创建要更新的表
     */
    private void createOrignalTables(Database db, Class<? extends AbstractDao<?, ?>>... daoClasses){
        for (Class<? extends AbstractDao<?, ?>> daoClass : daoClasses) {
            try {
                Method method = daoClass.getMethod("createTable", Database.class, boolean.class);
                method.invoke(null, db, false);
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 恢复数据
     */
    private void restoreData(Database db, Class<? extends AbstractDao<?, ?>>... daoClasses) {
        //...
    }

    private String getTypeByClass(Class<?> type) throws Exception {
        //...
    }

    private static List<String> getColumns(Database db, String tableName) {
        //...
    }
}

上面,我们通过反射,成功的做到了只 删除备份 你传入的表,其它不需要更新的表不需要关心。

有关反射的知识,//todo

至此,有关 GreenDao 的更新方案全部介绍完了,最后我们再看看上面遗留的问题:跨版本升级

4 跨版本升级

升级数据库时,我们无法保证用户每一版本都会及时更新,可能会跨版本升级,所以一般在 MyOpenHelperonUpgrade() 的方法中,我们不能直接忽视数据库版本,像上面那样直接将更新语句怼上去。

这一块就不细说了,下面给出我跨版本升级的方案。假设即将发出去的应用数据库版本为 7,则之前每一版本数据库的变动如下所示。当然,这不是在某一版写的,而是在升级过程中慢慢加上去的:

@Override
public void onUpgrade(Database db, int oldVersion, int newVersion) {
    super.onUpgrade(db, oldVersion, newVersion);
    //判断之前的版本
    switch (oldVersion){
        case 1:
            // 无变动
        case 2:
            //新增 VideoCache 表
            VideoCacheDao.createTable(db, false);
        case 3:
        case 4:
        case 5:
            //新增 User 表
            UserDao.createTable(db, false);
        case 6:
            //更新 VideoCache 表字段
            MigrationHelper.getInstance().migrate(db, VideoCacheDao.class);
            //更新 User 表字段
            MigrationHelper.getInstance().migrate(db, UserDao.class);
    }
}

如果你对跨版本升级还不是很了解,上面的方案理解起来可能会比较困难,建议你多看几遍。

总之,在版本 迭代过程中:

  • 数据库升级的每一个 version 号都要出现在 case 中;
  • 而且中途不要有 break

这样才能确保用户跨版本升级不会出现问题。

以上就是本次分享全部内容,若有任何不当之处,还请指教。


扫描下方二维码,关注我的公众号,及时获取最新文章推送!

技术分享

<script type="text/javascript"> $(function () { $(‘pre.prettyprint code‘).each(function () { var lines = $(this).text().split(‘\n‘).length; var $numbering = $(‘
    ‘).addClass(‘pre-numbering‘).hide(); $(this).addClass(‘has-numbering‘).parent().append($numbering); for (i = 1; i <= lines; i++) { $numbering.append($(‘
  • ‘).text(i)); }; $numbering.fadeIn(1700); }); }); </script>

    你应该知道的 Android 数据库更新策略