greenDAO自动化升级探索

作者: JohnsonZZZ | 来源:发表于2018-01-09 13:20 被阅读102次

    前言

    好像有好久没写博客的样子~ 行,那今天就再找个理由吹一波。 前段时间研究了一下greendao数据库升级模块,发现了一些存在的一些问题痛点,特拿来晾晒一下,以防发霉。


    问题现状

    话说为什么要做数据库自动升级这块的探索呢,主要有以下几点原因:

    • 现有的数据库升级方式过于繁琐,每个版本都需要进行一次手动升级。
    • 出现跨版本升级数据库的时候,偶尔会出现数据库字段丢失的情况,造成一些用户闪退现象。
    • 主要还是人懒,不想每次都写一大堆重复的代码

    思考

    话说有没有一种方式能够比较优雅地解决这个问题呢?一波搜索后,发现很多解决方案基本都是类似的,分为两类:

    第一类:根据当前版本依次递归的常规升级方式,即每个新版发布都在对应的版本号下面加入新增的表或者字段。这种传统的升级方式,显得不够“自动化”,写起来比较麻烦,而且有时候还容易遗漏掉部分新增字段,造成应用的崩溃问题。
    第二类:基本上参考了stackoverflow上面一位大佬的自动化升级方式。他的思路是这样的:
    1.拷贝原有数据表,新建temp表备份数据
    2.删除原有数据表
    3.新建现有数据表
    4.把temp表备份数据插入到新建的现有表中
    5.删除备份temp表
    6.balabalabla...

    反正就是一顿操作猛如虎,数据搬过来搬过去,删完再建、各种反射,看起来很炫酷的样子。
    我就在想,为什么就不直接遍历检测 缺失表 + 缺失表字段,然后直接插入缺失的表或字段呢?如果可以这样操作的话,那么性能方面肯定会有一个显著的提升,极大的减少了数据库操作开销,岂不是看起来很棒棒?

    窝草,这图怎么这么大

    解决方案

    这个时候,一个热乎的方案新鲜出炉了。主要思路还是遍历数据库寻找缺失的表和表字段。然后完善对应的表结构。

    public final class MigrationHelper {
    
        private static final String TAG = "MigrationHelper";
        private static final String SQLITE_MASTER = "sqlite_master";
        private static final String SQLITE_TEMP_MASTER = "sqlite_temp_master";
    
        public static void migrate(SQLiteDatabase db, Class<? extends AbstractDao<?, ?>>... daoClasses) {
            LogUtil.d(TAG,"【The Old Database Version】" + db.getVersion());
            Database database = new StandardDatabase(db);
            migrate(database, daoClasses);
        }
    
    
        public static void migrate(Database database, Class<? extends AbstractDao<?, ?>>... daoClasses) {
            for (int i = 0; i < daoClasses.length; i++) {
                DaoConfig daoConfig = new DaoConfig(database, daoClasses[i]);
                createTable(database, true, daoConfig);
            }
            LogUtil.d(TAG,"【check Tables】start");
            checkTablesIfNeedAlter(database, daoClasses);
            LogUtil.d(TAG,"【check Tables】complete");
        }
    
        private static boolean isTableExists(Database db, boolean isTemp, String tableName) {
            if (db == null || TextUtils.isEmpty(tableName)) {
                return false;
            }
            String dbName = isTemp ? SQLITE_TEMP_MASTER : SQLITE_MASTER;
            String sql = "SELECT COUNT(*) FROM " + dbName + " WHERE type = ? AND name = ?";
            Cursor cursor = null;
            int count = 0;
            try {
                cursor = db.rawQuery(sql, new String[]{"table", tableName});
                if (cursor == null || !cursor.moveToFirst()) {
                    return false;
                }
                count = cursor.getInt(0);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                if (cursor != null)
                    cursor.close();
            }
            return count > 0;
        }
    
        private static void checkTablesIfNeedAlter(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;
                if (!isTableExists(db, false, tableName)) {
                    continue;
                }
    
                try {
                    List<String> columns = getColumns(db, tableName);
                    List<String> lowcaseColumns = parseToLowcase(columns);
                    for (int j = 0; j < daoConfig.properties.length; j++) {
                        String columnName = daoConfig.properties[j].columnName;
                        if (!lowcaseColumns.contains(columnName.toLowerCase())) {
                            StringBuilder alterTableStringBuilder = new StringBuilder();
                            alterTableStringBuilder.append("ALTER TABLE ").append(tableName).append(" ADD ");
                            alterTableStringBuilder.append(columnName).append(" ");
                            alterTableStringBuilder.append(getPropertyType(daoConfig.properties[j].type));
                            db.execSQL(alterTableStringBuilder.toString());
                            LogUtil.d(TAG,"【ALTER TABLE】" + alterTableStringBuilder.toString());
                        }
                    }
                } catch (SQLException e) {
                    Log.e(TAG, "【Failed to ALTER TABLE 】" + tableName, e);
                }
            }
        }
    
        private static List<String> parseToLowcase(List<String> columnList) {
            List<String> newList = new ArrayList<>();
            for (String columnName : columnList) {
                newList.add(columnName.toLowerCase());
            }
            return newList;
        }
    
        private static List<String> getColumns(Database db, String tableName) {
            List<String> 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;
        }
    
        public static void createTable(Database db, boolean ifNotExists, DaoConfig daoConfig) {
            String tableName = daoConfig.tablename;
            StringBuilder builder = new StringBuilder();
            builder.append("CREATE TABLE ");
            builder.append(ifNotExists ? "IF NOT EXISTS ": "");
            builder.append(tableName);
            builder.append(getColumnsSql(daoConfig));
            LogUtil.d(TAG,"【createTable】 sql:" + builder.toString());
            db.execSQL(builder.toString()); // 6: Description
        }
    
        private static String getColumnsSql(DaoConfig daoConfig) {
            if (daoConfig == null) {
                return "";
            }
            StringBuilder builder = new StringBuilder(" (");
            for (int i = 0; i < daoConfig.properties.length; i++) {
                builder.append(String.format("\"%s\" %s,", daoConfig.properties[i].columnName,
                        getPropertyType(daoConfig.properties[i].type)));
            }
            if (daoConfig.properties.length > 0 && builder.length() > 0) {
                builder.deleteCharAt(builder.length() - 1);
            }
            builder.append("); ");
            return builder.toString();
        }
    
        /**
         * 根据字段类型返回对应的数据库字段语句,这边返回的默认属性可以根据项目需求而定
         * 如果有需要可以直接拷贝下来,修改某些类型的默认属性
         * @param type
         * @return
         */
        private static String getPropertyType(Class<?> type) {
            if (type.equals(byte[].class)) {
                return "BLOB";
            } else if (type.equals(String.class)) {
                return "TEXT DEFAULT ''";
            } else if (type.equals(boolean.class) || type.equals(Boolean.class)
                    || type.equals(int.class) || type.equals(Integer.class)
                    || type.equals(long.class) || type.equals(Long.class)
                    || type.equals(Date.class) || type.equals(Byte.class)) {
                return "INTEGER DEFAULT (0)";
            } else if (type.equals(float.class) || type.equals(Float.class)
                    || type.equals(double.class) || type.equals(Double.class)){
                return "REAL DEFAULT (0)";
            }
            return "TEXT DEFAULT ''";
        }
    
    }
    

    接下来是创建一个关联数据库的实体类Demo,比如当前有一个存放关键字的表KeywordHistory

    @Entity(nameInDb = "KeywordHistory")
    public class KeywordHistoryEntity {
    
        @Id(autoincrement = true)
        @Property(nameInDb = "Id")
        public Long Id;
    
        @Property(nameInDb = "Keyword")
        public String Keyword;
    
        @Property(nameInDb = "QueryTime")
        public long QueryTime;
    
        @Generated(hash = 4193202)
        public KeywordHistoryEntity(Long Id, String Keyword, long QueryTime) {
            this.Id = Id;
            this.Keyword = Keyword;
            this.QueryTime = QueryTime;
        }
    
        @Generated(hash = 462930205)
        public KeywordHistoryEntity() {
        }
    
        public Long getId() {
            return this.Id;
        }
    
        public void setId(Long Id) {
            this.Id = Id;
        }
    
        public String getKeyword() {
            return this.Keyword;
        }
    
        public void setKeyword(String Keyword) {
            this.Keyword = Keyword;
        }
    
        public long getQueryTime() {
            return this.QueryTime;
        }
    
        public void setQueryTime(long QueryTime) {
            this.QueryTime = QueryTime;
        }
    
    
    }
    

    接下来是创建/升级数据库时候需要完成的操作步骤,很简单,只需要修改两个地方
    1.build.gradle 文件下greenDAO schemaVersion版本号+1
    2.将新增或者修改后的EntityDao 依次放在onCreate和onUpgrade KeywordHistoryEntityDao 对应的位置,即完成数据库的升级。剩下的表和字段的创建工作MigrationHelper这个类帮你自动完成。

    public class DBOpenHelper extends DaoMaster.OpenHelper {
    
    
        public DBOpenHelper(Context context, String name) {
            super(context, name);
        }
    
        @Override
        public void onCreate(Database db) {
            super.onCreate(db);
            MigrationHelper.migrate(db, KeywordHistoryEntityDao.class);
        }
    
        @Override
        public void onUpgrade(Database db, int oldVersion, int newVersion) {
            super.onUpgrade(db, oldVersion, newVersion);
            MigrationHelper.migrate(db, KeywordHistoryEntityDao.class);
        }
    
    }
    

    什么?这点操作 就完成数据库升级了?没错啊,自动化升级就是这么easy~ 如果有需要赶紧也试试看吧

    使用方法

    对了,还有就是如何使用呢?其实调用的方法其实和greendao的日常操作一致,相信大家都很熟悉了。

    DBOpenHelper helper = new DBOpenHelper(getApplicationContext(), "test.db");
    DaoMaster daoMaster = new DaoMaster(helper.getWritableDatabase());
    KeywordHistoryEntityDao dao = daoMaster.newSession().getKeywordHistoryEntityDao();
    
    KeywordHistoryEntity historyEntity = new KeywordHistoryEntity(1L, "关键字" , 1L);
    dao.insert(historyEntity);
    

    以上便是自动化升级的全部代码,代码已上传https://github.com/mhlistener/GreenDaoUpgrade,喜欢的话可以star一下,大佬们如果有更好的建议欢迎提一波issue。

    参考资料

    相关文章

      网友评论

        本文标题:greenDAO自动化升级探索

        本文链接:https://www.haomeiwen.com/subject/owfgmxtx.html