目前公司android项目普遍使用框架对数据库进行操作,数据库表与数据实体都具有严格的对应的关系,但是数据库的升依赖不同版本间的升级脚本,如果应用跨多版本进行升级时,当缺失部分升级脚本时就会导致应用异常。
依赖脚本升级方案的缺点:
1、如果缺失某段升级脚本,覆盖安装程序后,应用运行异常。
2、项目跨版本升级管理复杂,多版本升级支持难度较大。
3、数据库导致的问题不容易排查,很难定位问题所在。
基于以上情况并以现有项目为背景(ormlite框架)构建android数据库自动升级维护方案,该方案需实现功能:
1、根据对应实体升级数据库表结构,保证实体中每个数据库属性均正常创建。保证项目启动后数据库表结构与本版本设计一致。
2、方案实施与项目所用数据库框架不存在冲突问题,可并行执行。
3、方案执行效率不可对应用流程产生明显影响。
4、本方案可作为数据库框架独立存在于项目中,完成项目数据库表的创建、升级操作。
引用此方案需要执行的操作:在项目启动后,数据库创建或升级后首先进行数据库实体注册然后调用更新接口即可。数据库升级规则:遍历注册实体,如果实体对应的表不存在则创建表(与现有框架兼容难于处理,本例不提供具体实现);如果表存在,则依次校验数据库属性,如果该字段不存在则创建该字段。
注意:兼容不同框架时需对源码进行一定修改,但是处理逻辑一致!
代码执行流流程图如下:
代码实现如下:以下代码经过部分修改后可兼容ormlite修改,需修改内容:1、获取数据库链接 2、String工具类判断字符串是否为空及多字符串相等的方法替换
1 /** @ClassName: DbUtils 2 * 3 * @Description: TODO 4 * 封装数据库常用操作工具方法 5 * @author walker 6 * 7 * @date 2017年7月26日 上午11:36:33 8 * 9 */ 10 public class DbUtils { 11 static DbUtils dbUtils; 12 SQLiteDatabase db; 13 Cursor cursor = null; 14 /**单例模式,私有化构造*/ 15 private DbUtils() {} 16 public static DbUtils getInstance(){ 17 if(dbUtils == null){ 18 dbUtils = new DbUtils(); 19 } 20 dbUtils.init(); 21 return dbUtils; 22 } 23 /** 24 * @Description: TODO 25 * 初始化所需资源 26 */ 27 public void init(){ 28 if(db == null){ 29 db = AppContext.getAppContext().getDatabaseHelper().getWritableDatabase(); 30 } 31 } 32 33 ArrayListtableObjList = new ArrayList (); 34 /** 35 * @Description: TODO 36 * 注册表实体:被注册实体与数据库表字段一一对应 37 * @param clazz 数据库表对应实体的class对象 38 */ 39 public void registTable(Class clazz){ 40 if(tableObjList == null){ 41 tableObjList = new ArrayList (); 42 } 43 tableObjList.add(clazz); 44 } 45 46 /** 47 * @Description: TODO 48 * 遍历已注册实体,更新表字段,数据库更新后调用 49 */ 50 public void update(){ 51 HashMap > tableList = getSqliteTables(); 52 HashMap columnMap; 53 /**表名称*/ 54 String tableName = ""; 55 /**每个实体数据库注解*/ 56 DatabaseTable tableAnnotation; 57 /**实体属性集合*/ 58 Field[] files ; 59 /**实体属性数据库注解*/ 60 DatabaseField columnAnnotation; 61 String columnName; 62 /**sql语句容器*/ 63 StringBuilder sqlStrBuilder = new StringBuilder(); 64 com.j256.ormlite.field.DataType dataType; 65 Object defaultValue = ""; 66 /**字段类型*/ 67 String columnType=""; 68 //遍历已注册实体,更新或创建表 69 for (Class objClazz : tableObjList) { 70 tableAnnotation = (DatabaseTable) objClazz.getAnnotation(DatabaseTable.class); 71 if (objClazz != null && !StringUtils.isEmpty(tableAnnotation.tableName())) { 72 tableName = tableAnnotation.tableName(); 73 } else { 74 tableName = objClazz.getSimpleName(); 75 } 76 columnMap = tableList.get(tableName); 77 //表未创建,则执行建表逻辑,此处代码适配框架,表创建业务在框架内执行 78 if(columnMap == null){ 79 //新建表应走第三方数据库框架。 80 // createTable(objClazz); 81 continue; 82 } 83 files = objClazz.getDeclaredFields(); 84 for (Field field : files) { 85 // 获取字段注解:表名、类型 86 columnAnnotation = field.getAnnotation(DatabaseField.class); 87 // 字段被注解:数据库字段 88 if (columnAnnotation != null) { 89 // 字段名称 90 if (StringUtils.isEmpty(columnAnnotation.columnName())) { 91 columnName = field.getName(); 92 } else { 93 columnName = columnAnnotation.columnName(); 94 } 95 // 该字段已经创建,进行下一次循环 96 if (columnMap.containsKey(columnName)) { 97 continue; 98 } 99 //字段未创建,构建创建字段sql100 if (sqlStrBuilder == null) {101 sqlStrBuilder = new StringBuilder();102 }103 sqlStrBuilder.delete(0, sqlStrBuilder.length());104 sqlStrBuilder.append("ALTER TABLE " + tableName);105 sqlStrBuilder.append(" ADD COLUMN " + columnName);106 dataType = columnAnnotation.dataType();107 defaultValue = "";108 if (StringUtils.strInStrs(dataType + "", dataType.STRING + "", dataType.UNKNOWN + "")) {109 columnType = "varchar";110 if(StringUtils.isEmptyUnNull(defaultValue+"")){111 defaultValue = "''"; 112 }113 } else if (StringUtils.strInStrs(dataType + "", dataType.INTEGER + "")) {114 if(StringUtils.isEmptyUnNull(defaultValue+"")){115 defaultValue = 0; 116 }117 columnType = "INTEGER";118 } else if (StringUtils.strInStrs(dataType + "", dataType.DOUBLE + "")) {119 if(StringUtils.isEmptyUnNull(defaultValue+"")){120 defaultValue = 0;121 }122 columnType = "double";123 } else if (StringUtils.strInStrs(dataType + "", dataType.FLOAT + "")) {124 if(StringUtils.isEmptyUnNull(defaultValue+"")){125 defaultValue = 0;126 }127 columnType = "FLOAT";128 } else if (StringUtils.strInStrs(dataType + "", dataType.LONG + "")) {129 if(StringUtils.isEmptyUnNull(defaultValue+"")){130 defaultValue = 0;131 }132 columnType = "Long";133 }134 sqlStrBuilder.append(" " + columnType);135 // 非空设置136 if (!columnAnnotation.canBeNull()) {137 sqlStrBuilder.append(" NOT NULL DEFAULT " + defaultValue);138 }139 executeSql(sqlStrBuilder+"");140 }141 }142 }143 }144 145 /**146 * @Description: TODO147 * 根据数据库实体类构建数据库表结构 :仅支持根据实体名称为表名,属性名称为字段,属性类型为字段类型方式建表。148 * @param objClazz 数据库表实体类149 */150 private void createTable(Class objClazz) {151 StringBuilder sb = new StringBuilder();152 sb.append(" CREATE TABLE " + objClazz.getSimpleName()+ " (");153 String filedName = "";154 //指定类的字段集合155 Field[] files = objClazz.getDeclaredFields();156 for (Field field : files) {157 filedName = field.getName();158 sb.append(filedName + " ");159 sb.append(field.getType().getSimpleName());160 sb.append(",");161 }162 if (sb.lastIndexOf(",") != -1) {163 sb.replace(sb.lastIndexOf(","), sb.length(), ")");164 }else{ //拼接插入表语句失败165 }166 executeSql(sb+"");167 }168 /**169 * @Description: TODO170 * 获取数据库中已存在表信息 171 * @return 返回数据库集合,数据库名称为键,数据库字段集合(字段名)为值。172 */173 public HashMap > getSqliteTables(){174 HashMap > resMap = new HashMap >();175 ArrayList > tableList= query("select name,sql from sqlite_master where type = 'table'");176 ArrayList > columnMapList;177 HashMap columnMap;178 for (HashMap table : tableList) {179 try {180 columnMap = new HashMap<>();181 columnMapList = query("PRAGMA table_info(" + table.get("name") + ")");182 if (columnMapList != null) {183 for (HashMap column : columnMapList) {184 columnMap.put(column.get("name"), column.get("type"));185 }186 }187 resMap.put(table.get("name"), columnMap);188 } catch (Exception e) {189 e.printStackTrace();190 }191 }192 return resMap;193 }194 195 196 /**197 * @Description: TODO198 * 数据库查询方法199 * @param sql 数据库查询sql语句200 * @return 返回sql查询结果集合,如果产生异常或无内容则返回空集合201 */202 public ArrayList > query(String sql) {203 HashMap res;204 ArrayList > resList = new ArrayList >();205 try {206 cursor = db.rawQuery(sql, null);207 while (cursor.moveToNext()) {208 res = new HashMap ();209 for (int i = 0; i < cursor.getColumnCount(); i++) {210 res.put(cursor.getColumnName(i) + "", cursor.getString(i) + "");211 }212 resList.add(res);213 }214 } catch (Exception e) {215 resList.clear();216 } finally {217 closeCursor();218 }219 return resList;220 }221 222 /**223 * @Description: TODO224 * 将游标cursor中的数据转换成Map列表数据 225 * @param cursor 数据库查询结果集合226 * @return 返回结果集对应的数据列表227 */228 public ArrayList > cursorToMap(Cursor cursor){229 HashMap res;230 ArrayList > resList = new ArrayList >();231 try {232 while (cursor.moveToNext()) {233 res = new HashMap ();234 for (int i = 0; i < cursor.getColumnCount(); i++) {235 res.put(cursor.getColumnName(i)+"", cursor.getString(i)+"");236 }237 resList.add(res);238 }239 } catch (Exception e) {240 resList.clear();241 }finally {242 closeCursor();243 }244 return resList;245 }246 247 /**248 * @Description: TODO249 * 关闭数据库游标250 */251 private void closeCursor(){252 if(cursor != null){253 cursor.close();254 cursor = null;255 }256 }257 258 /**259 * @Description: TODO260 * 执行sql语句261 * @param sql sql语句262 * @return 执行成功返回true,否则返回false263 */264 public boolean executeSql(String sql) {265 try {266 db.execSQL(sql);267 return true;268 } catch (Exception e) {269 }270 return false;271 }272 }
以上为android项目兼容现有数据库框架进行自动化升级的改造方案思路,后续会将本方案进行优化,实现数据库创建、升级、常用查询管理等共能。