Android数据库自动升级
本文所有的数据库操作都基于ormlite框架,不了解的可以先自行学习。
传统升级方案的讨论
在以往的数据库升级方案中,大家基本的做法都是重写 onUpgrade() 方法,通过 oldVersion 与 newVersion 来判断数据库的升级策略,然后使用sql语句对表进行操作。这种方法很适合在小型项目进行开发,但是当开发大型的项目,由于数据表增多,项目迭代频繁,人员技术相差较大等各种原因,会出现很多问题。
现在我们讨论另一种数据库升级方案,当数据库版本号变化时,通过比较原始的数据表结果与当前的数据表结构是否有变化,来判断数据库是否升级,并通过差异化判断来决定升级过程,达到自动升级的目的。
自动升级方案设计
获取数据表结构
- 获取原始的数据表建表语句
SQLite 数据库中有一个特殊的名叫 sqlite_master 的数据表,每条CREATE INDEX语句的文本储存于sqlite_master或sqlite_temp_master表中,我们可以通过下面的代码获取到原始建表语句。
// 获取数据库已有表的建表语句
cursor = db.rawQuery("select * from sqlite_master where type = ? AND name = ?",
new String[]{"table", tableName});
if (cursor != null) {
cursor.moveToFirst();
int columnIndex = cursor.getColumnIndex("sql");
if (-1 != columnIndex && cursor.getCount() > 0) {
String oldCreateSql= cursor.getString(columnIndex);
}
}
- 获取当前数据表的建表语句就简单了,直接调用ormlite的接口即可。
// 获取当前数据表建表语句
String newCreateSql= TableUtils.getCreateTableStatements(connectionSource, clazz).get(0);
转换表结构
现在我们已经有了升级前后的建表语句,那么怎么比较建表语句的差异呢?首先,我们定义一个数据表列的结构,用来结构化表的建表结构。
/**
* 数据库表中列的结构
*/
public class ColumnStruct {
private String columnName; // 列名
private String columnLimit; // 列的限制条件
public ColumnStruct() {
}
public ColumnStruct(String columnName, String columnLimit) {
this.columnName = columnName;
this.columnLimit = columnLimit;
}
public String getColumnName() {
return columnName;
}
public void setColumnName(String columnName) {
this.columnName = columnName;
}
public String getColumnLimit() {
return columnLimit;
}
public void setColumnLimit(String columnLimit) {
this.columnLimit = columnLimit;
}
}
通过这个方法,我们可以对表结构进行拆分,得到表的列结构。这个方法有点笨的感觉,有好的想法欢迎轻喷。
/**
* 生成表结构
* @param tableStruct 规范建表语句
* @return
*/
public static List<ColumnStruct> getColumnStruct(String tableStruct) {
// 这个函数总是给人一种不爽的感觉,欢迎提出更优秀的方案
List<ColumnStruct> columnStructList = new ArrayList<>();
// 解析过程根据标准的ormlite建表语句设计
String subString = tableStruct.substring(tableStruct.indexOf("(") + 1, tableStruct.length() - 1);
String[] sub = subString.split(", ");
for (String str : sub) {
if (str.contains("(") || str.contains(")")) {
str = str.replace("(", "").replace(")", "");
}
str = removeUnlessSpace(str);
if (str.startsWith("`")) {
String[] column = str.split("` ");
columnStructList.add(new ColumnStruct(column[0].replace("`", ""), column[1]));
} else {
// 附加的额外字段限制,不是以`开始
if (str.contains(",")) {
String[] column = str.split(" `");
String[] columns = column[1].replace("`", "").split(",");
for (String str1 : columns) {
modifyColumnStruct(columnStructList, str1, "UniqueCombo");
}
} else {
String[] column = str.split(" `");
modifyColumnStruct(columnStructList, column[1].replace("`", ""), column[0]);
}
}
}
return columnStructList;
}
数据库中表的几种情况
有了表的升级前后的列结构就好办了,数据库表的变化存在这几种情况:
新增表。数据表原始的列结构为空时,此时即为新增表;
删除表。当数据表新的别结构为空时,此时即为删除表;
修改表。表的修改存在几种情况:1.增加列;2.删除列;3.修改列的限制条件。在这几种情况下我们进行优先级的排序,当限制条件被改变时,这是最麻烦的情况,简单做法:删除旧表重建新表(能力有限一直没想到好的方案)。如果原始列的限制条件没有被改变,此时我们就可以进行数据库的升级了。
第一步,我们需要得到哪些列是保留不变的。
/**
* 获得没有变化的列
*/
private String getCopyColumns(List<String> oldColumns, List<String> deleteList) {
StringBuilder columns = new StringBuilder("");
if (oldColumns == null || deleteList == null) {
return columns.toString();
}
int index = 0;
// 存在删除集合里的列不添加
for (String columnName : oldColumns) {
if (!CollectionUtil.existValue(columnName, deleteList)) {
if (index == 0) {
columns.append("`").append(columnName).append("`");
} else {
columns.append(", ").append("`").append(columnName).append("`");
}
index++;
}
}
return columns.toString();
}
第二步,进行数据表的更新,在这里我们使用的是先新建一个临时表,然后对数据进行拷贝。
/**
* 拷贝数据的方式更新
*
* @param columns 原始列减去删除的列
*/
private void upgradeByCopy(SQLiteDatabase db, ConnectionSource cs, String columns) throws SQLException {
db.beginTransaction();
try {
//Rename table
String tempTableName = tableName + "_temp";
String sql = "ALTER TABLE " + tableName + " RENAME TO " + tempTableName;
db.execSQL(sql);
//Create table
try {
sql = TableUtils.getCreateTableStatements(cs, clazz).get(0);
db.execSQL(sql);
} catch (Exception e) {
Timber.e("", e);
TableUtils.createTable(cs, clazz);
}
sql = "INSERT INTO " + tableName + " (" + columns + ") " +
" SELECT " + columns + " FROM " + tempTableName;
db.execSQL(sql);
//Drop temp table
sql = "DROP TABLE IF EXISTS " + tempTableName;
db.execSQL(sql);
db.setTransactionSuccessful();
} catch (Exception e) {
throw new SQLException("update fail");
} finally {
db.endTransaction();
}
}
升级方案
首先,我们只需要新建一个DatabaseHelper,继承我们的 OrmLiteDatabaseHelper,OrmLiteDatabaseHelper 封装了数据库升级的策略。
DatabaseHelper
public class DatabaseHelper extends OrmLiteDatabaseHelper {
/**
* 数据库名称
*/
private static final String DATABASE_NAME = "mydatabase.db";
/**
* 数据库版本号
*/
private static final int DATABASE_VERSION = 4;
private static DatabaseHelper instance;
private DatabaseHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
addTable();
}
public static DatabaseHelper getInstance(Context context) {
if (instance == null) {
synchronized (DatabaseHelper.class) {
if (instance == null) {
instance = new DatabaseHelper(context);
}
}
}
return instance;
}
/**
* 注册数据表
*/
private void addTable() {
registerTable(City.class);
}
}
DatabaseHelper 通过调用父类的 registerTable() 方法进行表的注册。当数据库需要升级时,只需要增加数据库版本号DATABASE_VERSION 的值即可。
/**
* 注册数据表
*
* @param clazz 表的列结构bean
* @param <T>
*/
public <T> void registerTable(Class<T> clazz) {
if (tableHandlers == null) {
tableHandlers = new ArrayList<>();
}
DatabaseHandler handler = new DatabaseHandler<>(clazz);
if (isValid(handler, tableHandlers)) {
tableHandlers.add(handler);
}
}
详细的实现流程可查看源码。
源码下载
示例源码下载:
https://github.com/wobuaihuangjun/DatabaseManager