greenDAO自動(dòng)化升級(jí)探索

前言

前段時(shí)間研究了一下greendao數(shù)據(jù)庫升級(jí)模塊损同,發(fā)現(xiàn)了一些存在的一些問題痛點(diǎn)帐偎,特拿來晾曬一下捂人,以防發(fā)霉御雕。


問題現(xiàn)狀

話說為什么要做數(shù)據(jù)庫自動(dòng)升級(jí)這塊的探索呢,主要有以下幾點(diǎn)原因:

  • 現(xiàn)有的數(shù)據(jù)庫升級(jí)方式過于繁瑣滥搭,每個(gè)版本都需要進(jìn)行一次手動(dòng)升級(jí)酸纲,每次升級(jí)都要寫一大推if else判斷新舊數(shù)據(jù)庫版本,一不小心就容易出錯(cuò)瑟匆。
  • 出現(xiàn)跨版本升級(jí)數(shù)據(jù)庫的時(shí)候闽坡,偶爾會(huì)出現(xiàn)數(shù)據(jù)庫字段丟失的情況,造成一些用戶閃退現(xiàn)象愁溜。
  • 主要還是人懶疾嗅,不想每次都寫一大堆重復(fù)的代碼

思考

話說有沒有一種方式能夠比較優(yōu)雅地解決這個(gè)問題呢?一波搜索后冕象,發(fā)現(xiàn)很多解決方案基本都是類似的代承,分為兩類:

第一類:根據(jù)當(dāng)前版本依次遞歸的常規(guī)升級(jí)方式,即每個(gè)新版發(fā)布都在對(duì)應(yīng)的版本號(hào)下面加入新增的表或者字段渐扮。這種傳統(tǒng)的升級(jí)方式论悴,顯得不夠“自動(dòng)化”,寫起來比較麻煩墓律,而且有時(shí)候還容易遺漏掉部分新增字段意荤,造成應(yīng)用的崩潰問題。
第二類:基本上參考了stackoverflow上面一位大佬的自動(dòng)化升級(jí)方式只锻。他的思路是這樣的:
1.拷貝原有數(shù)據(jù)表,新建temp表備份數(shù)據(jù)
2.刪除原有數(shù)據(jù)表
3.新建現(xiàn)有數(shù)據(jù)表
4.把temp表備份數(shù)據(jù)插入到新建的現(xiàn)有表中
5.刪除備份temp表
6.balabalabla...

反正就是一頓操作猛如虎紫谷,數(shù)據(jù)搬過來搬過去齐饮,刪完再建捐寥、各種反射,看起來很炫酷的樣子祖驱。
我就在想握恳,為什么就不直接遍歷檢測(cè) 缺失表 + 缺失表字段,然后直接插入缺失的表或字段呢捺僻?如果可以這樣操作的話乡洼,那么性能方面肯定會(huì)有一個(gè)顯著的提升,極大的減少了數(shù)據(jù)庫操作開銷匕坯,豈不是看起來很棒棒束昵?

窩草,這圖怎么這么大

解決方案

這個(gè)時(shí)候葛峻,一個(gè)熱乎的方案新鮮出爐了锹雏。主要思路還是遍歷數(shù)據(jù)庫尋找缺失的表和表字段。然后完善對(duì)應(yīng)的表結(jié)構(gòu)术奖。

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) {
        Database database = new StandardDatabase(db);
        migrate(database, daoClasses);
    }


    public static void migrate(Database database, Class<? extends AbstractDao<?, ?>>... daoClasses) {
        generateTempTables(database, daoClasses);

        for (int i = 0; i < daoClasses.length; i++) {
            DaoConfig daoConfig = new DaoConfig(database, daoClasses[i]);
            dropTable(database, true, daoConfig);
            createTable(database, false, daoConfig);
        }

        restoreData(database, daoClasses);
    }

    private static void dropTable(Database database, boolean ifExists, DaoConfig daoConfig) {
        String sql = String.format("DROP TABLE %s\"%s\"", ifExists ? "IF EXISTS " : "", daoConfig.tablename);
        database.execSQL(sql);
    }

    private static void generateTempTables(Database db, Class<? extends AbstractDao<?, ?>>... daoClasses) {
        for (int i = 0; i < daoClasses.length; i++) {
            String tempTableName = null;

            DaoConfig daoConfig = new DaoConfig(db, daoClasses[i]);
            String tableName = daoConfig.tablename;
            if (!isTableExists(db, false, tableName)) {
                continue;
            }
            try {
                tempTableName = daoConfig.tablename.concat("_TEMP");
                StringBuilder dropTableStringBuilder = new StringBuilder();
                dropTableStringBuilder.append("DROP TABLE IF EXISTS ").append(tempTableName).append(";");
                db.execSQL(dropTableStringBuilder.toString());

                StringBuilder insertTableStringBuilder = new StringBuilder();
                insertTableStringBuilder.append("CREATE TEMPORARY TABLE ").append(tempTableName);
                insertTableStringBuilder.append(" AS SELECT * FROM ").append(tableName).append(";");
                db.execSQL(insertTableStringBuilder.toString());
            } catch (SQLException e) {
                Log.e(TAG, "【Failed to generate temp table】" + tempTableName, e);
            }
        }
    }

    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 String getColumnsStr(DaoConfig daoConfig) {
        if (daoConfig == null) {
            return "no columns";
        }
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < daoConfig.allColumns.length; i++) {
            builder.append(daoConfig.allColumns[i]);
            builder.append(",");
        }
        if (builder.length() > 0) {
            builder.deleteCharAt(builder.length() - 1);
        }
        return builder.toString();
    }


    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");

            if (!isTableExists(db, true, tempTableName)) {
                continue;
            }

            try {
                // get all columns from tempTable, take careful to use the columns list
                List<String> columns = getColumns(db, tempTableName);
                ArrayList<String> 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);
                    }
                }
                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());
                }
                StringBuilder dropTableStringBuilder = new StringBuilder();
                dropTableStringBuilder.append("DROP TABLE ").append(tempTableName);
                db.execSQL(dropTableStringBuilder.toString());
            } catch (SQLException e) {
                Log.e(TAG, "【Failed to restore data from temp table 】" + tempTableName, e);
            }
        }
    }

    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();
    }

    /**
     * 根據(jù)字段類型返回對(duì)應(yīng)的數(shù)據(jù)庫字段語句
     * @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 ''";
    }

}

接下來是創(chuàng)建一個(gè)關(guān)聯(lián)數(shù)據(jù)庫的實(shí)體類Demo礁遵,比如當(dāng)前有一個(gè)存放關(guān)鍵字的表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;
    }


}

接下來是創(chuàng)建/升級(jí)數(shù)據(jù)庫時(shí)候需要完成的操作步驟,很簡(jiǎn)單采记,只需要修改兩個(gè)地方
1.build.gradle 文件下greenDAO schemaVersion版本號(hào)+1
2.將新增或者修改后的EntityDao 依次放在onCreate和onUpgrade KeywordHistoryEntityDao 對(duì)應(yīng)的位置佣耐,即完成數(shù)據(jù)庫的升級(jí)。剩下的表和字段的創(chuàng)建工作MigrationHelper這個(gè)類幫你自動(dòng)完成唧龄。


1.build.gradle文件
greendao {
    schemaVersion 1 //每次更新數(shù)據(jù)庫兼砖,這個(gè)地方版本號(hào)都要加1
}

...

2.數(shù)據(jù)庫OpenHelper 管理類
public class DBOpenHelper extends DaoMaster.OpenHelper {


    public DBOpenHelper(Context context, String name) {
        super(context, name);
    }

    @Override
    public void onCreate(Database db) {
        super.onCreate(db);
        startMigrate(db);
    }

    @Override
    public void onUpgrade(Database db, int oldVersion, int newVersion) {
        super.onUpgrade(db, oldVersion, newVersion);
        startMigrate(db);
    }

    private void startMigrate(Database db) {
        MigrationHelper.migrate(db, KeywordHistoryEntityDao.class);
    }

}

什么?這點(diǎn)操作 就完成數(shù)據(jù)庫升級(jí)了选侨?沒錯(cuò)啊掖鱼,自動(dòng)化升級(jí)就是這么easy~ 如果有需要趕緊也試試看吧

使用方法

對(duì)了,還有就是使用了自動(dòng)升級(jí)之后援制,調(diào)用方法和以前的greenDAO有什么區(qū)別呢戏挡?其實(shí)調(diào)用的方法其實(shí)和greendao的日常操作一致,如下所示:

DBOpenHelper helper = new DBOpenHelper(getApplicationContext(), "test.db");
DaoMaster daoMaster = new DaoMaster(helper.getWritableDatabase());
final KeywordHistoryEntityDao dao = daoMaster.newSession().getKeywordHistoryEntityDao();
KeywordHistoryEntity historyEntity = new KeywordHistoryEntity(1, "關(guān)鍵字" , 1);
dao.insert(historyEntity);

自動(dòng)升級(jí)的日志如下

11-19 14:49:43.873 5967-5967/com.github.mhlistener.greendaoupgradeapp D/MigrationHelper: 【Generate temp table】start
11-19 14:49:43.874 5967-5967/com.github.mhlistener.greendaoupgradeapp D/MigrationHelper: 【Generate temp table】 dropTableStringBuilder:DROP TABLE IF EXISTS KeywordHistory_TEMP;
    【Generate temp table】 insertTableStringBuilder:CREATE TEMPORARY TABLE KeywordHistory_TEMP AS SELECT * FROM KeywordHistory;
    【Table】KeywordHistory
     ---Columns-->Id,Keyword,QueryTime
11-19 14:49:43.875 5967-5967/com.github.mhlistener.greendaoupgradeapp D/MigrationHelper: 【Generate temp table】KeywordHistory_TEMP
    【Generate temp table】complete
    【Drop all table and recreate all table】
    【createTable】 sql:CREATE TABLE KeywordHistory ("Id" INTEGER DEFAULT (0),"Keyword" TEXT DEFAULT '',"QueryTime" INTEGER DEFAULT (0)); 
    【Restore data】start
11-19 14:49:43.876 5967-5967/com.github.mhlistener.greendaoupgradeapp D/MigrationHelper: 【Restore data】 db sql: INSERT INTO KeywordHistory (Id,Keyword,QueryTime) SELECT Id,Keyword,QueryTime FROM KeywordHistory_TEMP;
    【Restore data】 to KeywordHistory
    【Drop temp table】KeywordHistory_TEMP
    【Restore data】complete

以上便是自動(dòng)化升級(jí)的全部代碼晨仑,代碼已上傳https://github.com/mhlistener/GreenDaoUpgrade褐墅,喜歡的話可以star一下,大佬們?nèi)绻懈玫慕ㄗh歡迎提一波issue洪己。

參考資料

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末妥凳,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子答捕,更是在濱河造成了極大的恐慌逝钥,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,406評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件拱镐,死亡現(xiàn)場(chǎng)離奇詭異艘款,居然都是意外死亡持际,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,395評(píng)論 3 398
  • 文/潘曉璐 我一進(jìn)店門哗咆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蜘欲,“玉大人,你說我怎么就攤上這事晌柬±逊荩” “怎么了?”我有些...
    開封第一講書人閱讀 167,815評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵年碘,是天一觀的道長(zhǎng)澈歉。 經(jīng)常有香客問我,道長(zhǎng)盛泡,這世上最難降的妖魔是什么闷祥? 我笑而不...
    開封第一講書人閱讀 59,537評(píng)論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮傲诵,結(jié)果婚禮上凯砍,老公的妹妹穿的比我還像新娘。我一直安慰自己拴竹,他們只是感情好悟衩,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,536評(píng)論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著栓拜,像睡著了一般座泳。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上幕与,一...
    開封第一講書人閱讀 52,184評(píng)論 1 308
  • 那天挑势,我揣著相機(jī)與錄音,去河邊找鬼啦鸣。 笑死潮饱,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的诫给。 我是一名探鬼主播香拉,決...
    沈念sama閱讀 40,776評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼中狂!你這毒婦竟也來了凫碌?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,668評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤胃榕,失蹤者是張志新(化名)和其女友劉穎盛险,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,212評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡枉层,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,299評(píng)論 3 340
  • 正文 我和宋清朗相戀三年泉褐,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鸟蜡。...
    茶點(diǎn)故事閱讀 40,438評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖挺邀,靈堂內(nèi)的尸體忽然破棺而出揉忘,到底是詐尸還是另有隱情,我是刑警寧澤端铛,帶...
    沈念sama閱讀 36,128評(píng)論 5 349
  • 正文 年R本政府宣布泣矛,位于F島的核電站,受9級(jí)特大地震影響禾蚕,放射性物質(zhì)發(fā)生泄漏您朽。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,807評(píng)論 3 333
  • 文/蒙蒙 一换淆、第九天 我趴在偏房一處隱蔽的房頂上張望哗总。 院中可真熱鬧,春花似錦倍试、人聲如沸讯屈。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,279評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽涮母。三九已至,卻和暖如春躁愿,著一層夾襖步出監(jiān)牢的瞬間叛本,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,395評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工彤钟, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留来候,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,827評(píng)論 3 376
  • 正文 我出身青樓样勃,卻偏偏與公主長(zhǎng)得像吠勘,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子峡眶,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,446評(píng)論 2 359