之前数据库升级,做法是删除旧表新建新表,这样所有的数据均被删除,那么一些私有数据,例如用户登录信息也被删除。这样的用户体验很不好,用户更新APP后,需要重新登录。为改进用户体验,我们将用户私有数据做迁移。这样用户不需要重新登录。
总体设计方案如下:
1、创建临时表存储旧表数据
2、删除旧表
3、创建新表
4、将旧表数据迁移至新表
在迁移时需要考虑一个问题:若新表的表结构有变化如何处理?在代码中我们会详细讲解。
接下来我们看下代码
迁移方法:
public static void migrate(Database db, Class extends AbstractDao, ?>>... daoClasses) {
//创建临时表存储旧表数据
generateTempTables(db, daoClasses);
//删除旧表
dropAllTables(db, true, daoClasses);
//创建新表
createAllTables(db, false, daoClasses);
//旧表数据迁移至新表
restoreData(db, daoClasses);
}
可以看到,就是我们设计方案中的那几个步骤
下面我们一一来看,并在代码注释中讲解重要部分
创建临时表:
private static void generateTempTables(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");
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 extends AbstractDao, ?>>... 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 extends AbstractDao, ?>>... 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 extends AbstractDao, ?>>... daoClasses) {
reflectMethod(db, "createTable", ifNotExists, daoClasses);
if (DEBUG) {
Log.d(TAG, "create all table");
}
}
很简单,没什么可讲的,重点看数据迁移方法:
private static 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");
// 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;
}
这里从旧表中获取到该列后,会判断该列是否存在数据,若存在才返回,这也在最大限度上优化了代码。
以上,希望可以给有相关需求的同学带来参考