Android GreenDao数据库升级数据迁移

概述

之前数据库升级,做法是删除旧表新建新表,这样所有的数据均被删除,那么一些私有数据,例如用户登录信息也被删除。这样的用户体验很不好,用户更新APP后,需要重新登录。为改进用户体验,我们将用户私有数据做迁移。这样用户不需要重新登录。

设计方案

总体设计方案如下:

1、创建临时表存储旧表数据

2、删除旧表

3、创建新表

4、将旧表数据迁移至新表

在迁移时需要考虑一个问题:若新表的表结构有变化如何处理?在代码中我们会详细讲解。

代码设计

接下来我们看下代码

迁移方法:

 public static void migrate(Database db, Class>... daoClasses) {
        //创建临时表存储旧表数据
        generateTempTables(db, daoClasses);
        //删除旧表
        dropAllTables(db, true, daoClasses);
        //创建新表
        createAllTables(db, false, daoClasses);
        //旧表数据迁移至新表
        restoreData(db, daoClasses);
    }

可以看到,就是我们设计方案中的那几个步骤

下面我们一一来看,并在代码注释中讲解重要部分

创建临时表:

private static void generateTempTables(Database db, Class>... 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");
            StringBuilder insertTableStringBuilder = new StringBuilder();
            insertTableStringBuilder.append("CREATE TEMPORARY TABLE ").append(tempTableName);
            insertTableStringBuilder.append(" AS SELECT * FROM ").append(tableName).append(";");
            db.execSQL(insertTableStringBuilder.toString());
            if (DEBUG) {
                Log.d(TAG, "the table " + tableName +" columns are "+getColumnsStr(daoConfig));
                Log.d(TAG, "generate temp table " + tempTableName);

            }
        }
    }

这里回顾下临时表的概念,它仅在本次连接中存在,本次连接断开,临时表会被删除,不需要我们手动删除

删除旧表:

 private static void dropAllTables(Database db, boolean ifExists, @NonNull Class>... daoClasses) {
        reflectMethod(db, "dropTable", ifExists, daoClasses);
        if (DEBUG) {
            Log.d(TAG, "drop all table");
        }
    }

这里用了反射,是为了在创建表时复用reflectMethod()方法,该方法如下:

    /**
     * dao class already define the sql exec method, so just invoke it
     */
    private static void reflectMethod(Database db, String methodName, boolean isExists, @NonNull Class>... daoClasses) {
        if (daoClasses.length < 1) {
            return;
        }
        try {
            for (Class cls : daoClasses) {
                Method method = cls.getDeclaredMethod(methodName, Database.class, boolean.class);
                method.invoke(null, db, isExists);
            }
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

创建新表:

private static void createAllTables(Database db, boolean ifNotExists, @NonNull Class>... daoClasses) {
        reflectMethod(db, "createTable", ifNotExists, daoClasses);
        if (DEBUG) {
            Log.d(TAG, "create all table");
        }
    }

很简单,没什么可讲的,重点看数据迁移方法:

private static void restoreData(Database db, Class>... 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");
            // get all columns from tempTable, take careful to use the columns list
            //获取临时表所有列,也就是旧表
            List columns = getColumns(db, tempTableName);
            ArrayList properties = new ArrayList<>(columns.size());
            //遍历新表的所有列
            for (int j = 0; j < daoConfig.properties.length; j++) {
                String columnName = daoConfig.properties[j].columnName;
                //这里做新旧表对比适配,如果旧表包含新表的列,则将该列存储在队列中
                if (columns.contains(columnName)) {
                    properties.add(columnName);
                }
            }
            //如果队列长度大于0,说明有数据需要迁移,进行SQL语句操作
            if (properties.size() > 0) {
                final String columnSQL = TextUtils.join(",", properties);

                StringBuilder insertTableStringBuilder = new StringBuilder();
                insertTableStringBuilder.append("INSERT INTO ").append(tableName).append(" (");
                insertTableStringBuilder.append(columnSQL);
                insertTableStringBuilder.append(") SELECT ");
                insertTableStringBuilder.append(columnSQL);
                insertTableStringBuilder.append(" FROM ").append(tempTableName).append(";");
                db.execSQL(insertTableStringBuilder.toString());
                if (DEBUG) {
                    Log.d(TAG, "restore data to " + tableName);
                    Log.d(TAG, "the table " + tableName +" columns are "+getColumnsStr(daoConfig));
                }
            }
            //保险起见,手动删除临时表
            StringBuilder dropTableStringBuilder = new StringBuilder();
            dropTableStringBuilder.append("DROP TABLE ").append(tempTableName);
            db.execSQL(dropTableStringBuilder.toString());
            if (DEBUG) {
                Log.d(TAG, "drop temp table " + tempTableName);
            }
        }
    }

从注释可以看出这里的逻辑,这里会选取新表和旧表列的交集,假如新表删除或增加了某一列,都不会进入properties队列中,进而也就不会对新表做操作,完美解决了表结构发生变化的问题。

最后我们再看下getColumn()方法:

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

这里从旧表中获取到该列后,会判断该列是否存在数据,若存在才返回,这也在最大限度上优化了代码。

以上,希望可以给有相关需求的同学带来参考

你可能感兴趣的:(Android开发)