GreenDao使用經(jīng)驗分享-數(shù)據(jù)庫無損升級

因為Greendao的高性能以及ORM框架的特點許多項目都是用了Greendao做為數(shù)據(jù)庫組件毡证,不僅提升了開發(fā)效率并且使很多程序員擺脫了枯燥的SQL語句(包括我),這個框架也是非常受歡迎的须鼎,但是我在使用過程中也發(fā)現(xiàn)了這個框架的一些不足

  1. 不支持線程回調
  2. 不支持默認數(shù)據(jù)類型

這兩點基本上是我使用這個ORM數(shù)據(jù)庫框架碰到的最失望的問題了,但是因為項目之前使用的就是該框架且不已我的意志為轉移的情況下只能繼續(xù)使用府蔗,并且在我這一段時間的使用中對這個框架也有一些經(jīng)驗和想法分享出來希望能幫到大家晋控,我來說一下針對這兩個問題有些什么方法

線程回調問題

這個問題不是這篇文章的重點,實際上我也沒有實際解決這個問題姓赤,但是有一些見解赡译,如果是我們自己手動為項目編寫數(shù)據(jù)庫組件的花一般都會遵循經(jīng)驗講數(shù)據(jù)庫操作放在子線程中進行并添加回調,但是遺憾的是Greendao項目組可能出于對該組件效率的自信并沒有這么做(事實也證明Greendao確實是所有ORM框架中效率最高的)模捂,但是 我們還是有這個需求的捶朵,比如筆者的項目中就有需求是直接從服務器獲取上萬條數(shù)據(jù)然后插入到本地或者直接從本地獲取上萬條數(shù)據(jù)(此處只想吐槽產(chǎn)品經(jīng)理的腦回路),這個時候如果在UI線程插入或讀取雖然相對來講其實也還算快狂男,但用戶總歸是會感覺到卡頓(大概一到兩秒综看,視實際機器性能不定),這總歸是很不爽的岖食,且不被接受红碑,存在ANR的風險

ANR.jpg

但是如果直接使用Greendao提供的異步方法我們又不知道何時插入完成,何時更新UI(這就很尷尬了),我們團隊的解決方案是犧牲部分用戶體驗后臺控制數(shù)據(jù)分批傳送,當然析珊,這其實是不得以的鴕鳥做法羡鸥,正確的做法是:研究一下Greendao的源碼并添加回調接口,當然這可能需要的不止一點時間忠寻,而且一般項目很少存在我這種一次性操作上萬條數(shù)據(jù)的情況惧浴,如果有,且在你們的項目還沒有上馬Greendao的情況下奕剃,趕緊棄暗投明 如果已經(jīng)上馬 趁項目崩潰前跑路吧


項目崩潰前跑路.gif

看到這里不要崩潰衷旅,開玩笑的,事情當然沒有那么嚴重纵朋,問題是可以解決的柿顶,我們大可以在外部包裹線程實現(xiàn)GreenDao的異步調用,無論是AsyncTask還是自己實現(xiàn)一個線程異步類操软,查詢時開啟一個線程嘁锯,查詢結果出來后再回調到主線程即可,工作量也不大聂薪,不過還是覺得GreenDao能提供的話還是要方便很多

當然也可以向大家介紹一款國產(chǎn)ORM數(shù)據(jù)庫框架LitePay家乘,由國內Android開發(fā)大神郭霖開源,在最近最新的一版更新中該框架已支持異步回調胆建,當然烤低,對于Greendao的異步本文不再多講肘交,畢竟本文最關心的另一個問題

數(shù)據(jù)庫升級問題

在項目中我們總會由各種各樣的問題需要對原有數(shù)據(jù)進行升級例如增加新的字段笆载,但是有的時候我們可能需又可能需要保留原來的數(shù)據(jù),在這里我不得不羨慕我的項目中負責另外的模塊的伙伴涯呻,他們的數(shù)據(jù)不僅能從服務器獲取且數(shù)據(jù)量極小凉驻,每次刪除后都可以從服務器重新獲取,根本不用擔心保留數(shù)據(jù)的問題复罐,所以他們一般采用直接刪除舊表再創(chuàng)建新表的方法應對數(shù)據(jù)庫版本升級涝登,有點小羨慕

簡單粗暴的升級方法.png

但是 這種方式畢竟過于簡單粗暴且不夠優(yōu)雅,最重要的是并不適合我的模塊效诅,我一開始的思路是先取出數(shù)據(jù)保留再內存中胀滚,然后將舊數(shù)據(jù)通過添加新字段默認值的方式升級為新表適用的數(shù)據(jù),然后刪除舊表乱投,創(chuàng)建新標咽笼,將轉換后的數(shù)據(jù)插入到新表中,但是不知道為什么這種方式看似沒毛财蒽拧(肯定有毛步P獭)但是每次都會報錯(報錯信息沒保留下來),不得已只能尋找另外別的方法,找來找去施掏,發(fā)現(xiàn)有一種方法是通過升級時創(chuàng)建一個臨時表將數(shù)據(jù)保留下來钮惠,然后進行表的升級再,再將數(shù)據(jù)轉移到新表中來實現(xiàn)的七芭,這種方式和我的做法其實有點象素挽,但可能考慮得比我多,不是將數(shù)據(jù)留在內存中狸驳,而且經(jīng)過實測實際有用毁菱,我將代碼貼出來大家可以方便取用


import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.text.TextUtils;

import com.oppo.community.util.LogUtil;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import de.greenrobot.dao.AbstractDao;
import de.greenrobot.dao.internal.DaoConfig;

public class MigrationHelper {
    private static final String CONVERSION_CLASS_NOT_FOUND_EXCEPTION = "MIGRATION HELPER - CLASS DOESN'T MATCH WITH THE CURRENT PARAMETERS";
    private static MigrationHelper instance;

    public static MigrationHelper getInstance() {
        if (instance == null) {
            instance = new MigrationHelper();
        }
        return instance;
    }

    private static List<String> getColumns(SQLiteDatabase db, String tableName) {
        List<String> columns = new ArrayList<>();
        Cursor cursor = null;
        try {
            cursor = db.rawQuery("SELECT * FROM " + tableName + " limit 1", null);
            if (cursor != null) {
                columns = new ArrayList<>(Arrays.asList(cursor.getColumnNames()));
            }
        } catch (Exception e) {
            LogUtil.d(tableName, e.getMessage());
            e.printStackTrace();
        } finally {
            if (cursor != null)
                cursor.close();
        }
        return columns;
    }

    public void migrate(SQLiteDatabase db, Class<? extends AbstractDao<?, ?>>... daoClasses) {
        generateTempTables(db, daoClasses);
        DaoMaster.dropAllTables(db, true);
        DaoMaster.createAllTables(db, false);
        restoreData(db, daoClasses);
    }

    private void generateTempTables(SQLiteDatabase db, Class<? extends AbstractDao<?, ?>>... daoClasses) {
        for (int i = 0; i < daoClasses.length; i++) {
            DaoConfig daoConfig = new DaoConfig(db, daoClasses[i]);

            String divider = "";
            String tableName = daoConfig.tablename;
            String tempTableName = daoConfig.tablename.concat("_TEMP");
            ArrayList<String> properties = new ArrayList<>();

            StringBuilder createTableStringBuilder = new StringBuilder();

            createTableStringBuilder.append("CREATE TABLE ").append(tempTableName).append(" (");

            for (int j = 0; j < daoConfig.properties.length; j++) {
                String columnName = daoConfig.properties[j].columnName;

                if (getColumns(db, tableName).contains(columnName)) {
                    properties.add(columnName);

                    String type = null;

                    try {
                        type = getTypeByClass(daoConfig.properties[j].type);
                    } catch (Exception exception) {
                        exception.printStackTrace();
                    }

                    createTableStringBuilder.append(divider).append(columnName).append(" ").append(type);

                    if (daoConfig.properties[j].primaryKey) {
                        createTableStringBuilder.append(" PRIMARY KEY");
                    }

                    divider = ",";
                }
            }
            createTableStringBuilder.append(");");
            LogUtil.d("TAG", "創(chuàng)建臨時表的SQL語句: " + createTableStringBuilder.toString());
            db.execSQL(createTableStringBuilder.toString());

            StringBuilder insertTableStringBuilder = new StringBuilder();

            insertTableStringBuilder.append("INSERT INTO ").append(tempTableName).append(" (");
            insertTableStringBuilder.append(TextUtils.join(",", properties));
            insertTableStringBuilder.append(") SELECT ");
            insertTableStringBuilder.append(TextUtils.join(",", properties));
            insertTableStringBuilder.append(" FROM ").append(tableName).append(";");
            LogUtil.d("TAG", "在臨時表插入數(shù)據(jù)的SQL語句:" + insertTableStringBuilder.toString());
            db.execSQL(insertTableStringBuilder.toString());
        }
    }

    private void restoreData(SQLiteDatabase 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");
            ArrayList<String> properties = new ArrayList();
            ArrayList<String> propertiesQuery = new ArrayList();
            for (int j = 0; j < daoConfig.properties.length; j++) {
                String columnName = daoConfig.properties[j].columnName;

                if (getColumns(db, tempTableName).contains(columnName)) {
                    properties.add(columnName);
                    propertiesQuery.add(columnName);
                } else {
                    try {
                        if (getTypeByClass(daoConfig.properties[j].type).equals("INTEGER")) {
                            propertiesQuery.add("0 as " + columnName);
                            properties.add(columnName);
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }

            StringBuilder insertTableStringBuilder = new StringBuilder();

            insertTableStringBuilder.append("INSERT INTO ").append(tableName).append(" (");
            insertTableStringBuilder.append(TextUtils.join(",", properties));
            insertTableStringBuilder.append(") SELECT ");
            insertTableStringBuilder.append(TextUtils.join(",", propertiesQuery));
            insertTableStringBuilder.append(" FROM ").append(tempTableName).append(";");

            StringBuilder dropTableStringBuilder = new StringBuilder();

            dropTableStringBuilder.append("DROP TABLE ").append(tempTableName);
            LogUtil.d("TAG", "插入正式表的SQL語句:" + insertTableStringBuilder.toString());
            LogUtil.d("TAG", "銷毀臨時表的SQL語句:" + dropTableStringBuilder.toString());
            db.execSQL(insertTableStringBuilder.toString());
            db.execSQL(dropTableStringBuilder.toString());
        }
    }

    private String getTypeByClass(Class<?> type) throws Exception {
        if (type.equals(String.class)) {
            return "TEXT";
        }
        if (type.equals(Long.class) || type.equals(Integer.class) || type.equals(long.class) || type.equals(int.class)) {
            return "INTEGER";
        }
        if (type.equals(Boolean.class) || type.equals(boolean.class)) {
            return "BOOLEAN";
        }

        Exception exception = new Exception(CONVERSION_CLASS_NOT_FOUND_EXCEPTION.concat(" - Class: ").concat(type.toString()));
        exception.printStackTrace();
        throw exception;
    }
}

注意代碼沒有加鎖(其實這個太大必要),然后取用也十分簡單锌历,只需要到自己實現(xiàn)的繼承了DaoMaster.OpenHelper的實現(xiàn)類中的onUpgrade()方法中調用這句代碼即可


MigrationHelper.getInstance().migrate(db, PrivateMsgNoticeDao.class);

示例如下:

升級調用.png

值得注意的是方法中第二個參數(shù)是一個可變參數(shù)贮庞,再這一個升級中可以填入多個我們需要升級數(shù)據(jù)表的實體類的Dao類,如:


MigrationHelper.getInstance().migrate(db, TestDao1.class, TestDao2.class);

這樣依賴我們可以一行代碼完成所有數(shù)據(jù)表的無損升級究西,是不是感覺非常方便窗慎,但是這里也有一個問題,就是上面提到的卤材,Greendao不支持默認數(shù)據(jù)遮斥,如果不賦值,那么在表中的字段都為null扇丛,也就是說我們升級之后的新表中從舊表轉移過去的數(shù)據(jù)新增的那個字段都為null术吗,更坑爹的是因為每次實體類都需要手動生成,所以我們也不太可能去實體類中設置默認值帆精,即使設置默認值最后也還是會為null较屿,因為從數(shù)據(jù)庫中讀取時會將null設置進該字段而覆蓋默認值,最后取到的還是null卓练,所以我們需要在接下來的調用中對該實體類對象字段的使用謹慎地判空隘蝎,我就被坑過,而且實在代碼被提交測試之后才發(fā)現(xiàn)的襟企,這也是我不推薦使用Greendao的原因之一嘱么,greendao的團隊太過傲嬌,居然連默認數(shù)據(jù)這么重要的API都不提供

2017-08-15更新

上面的方法在實際生產(chǎn)中被驗證可以解決問題顽悼,但存在缺陷:每次都要將需要保留的數(shù)據(jù)添加進migrate()方法中曼振,即使沒有升級數(shù)據(jù)表的Dao類,因為操作中會將添加進去的表備份然后刪除所有的表蔚龙,再將所有的備份過的表恢復冰评,長此以往要保留的數(shù)據(jù)表多了自然會有影響,且可能引發(fā)未知問題府蛇,替代解決方案請參考文尾--另外的解決方案

另外的解決辦法

我沒有試過這個方法集索,是我想過的方案之一,但沒有嘗試,思路是在數(shù)據(jù)庫升級時調用SQL語句為目標表動態(tài)創(chuàng)建一列數(shù)據(jù)务荆,為此我特意問過我們數(shù)據(jù)組的同事妆距,他告訴我是可以的,我查了一下SQL代碼如下(2017-08-15更新:經(jīng)過實際驗證方法可行)


alter table table_name add column (字段名 字段類型); ----此方法帶括號指定字段插入的位置:

實際代碼的方法封裝如下:


        /**
         * 升級數(shù)據(jù)庫時動態(tài)插入一列
         *
         * @param db             數(shù)據(jù)庫實體
         * @param tabName        要操作的表名  如UserInfo
         * @param columnName     要生成的列名  如UserId
         * @param columnNameType 字段類型      如integer
         */
        private static void insertColumn(SQLiteDatabase db, String tabName, String columnName, String columnNameType) {
            db.execSQL("alter table \"" + tabName + "\" add column \"" + columnName + "\" " + columnNameType);
        }

也可以在插入新字段時指定字段位置函匕,如插入于某字段之前娱据,SQL代碼如下


alter table table_name add column 字段名 字段類型 after 某字段;--這個方法就不知道要不要帶括號了

當然這個方法我也沒有嘗試是否有用盅惜,只是我的想法中剩,有需求或有興趣的同學也可以去試一下(2017-08-15更新:實測可用)

這些就是我遇到的問題以及解決辦法,也希望能幫到有需要的同學抒寂。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末结啼,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子屈芜,更是在濱河造成了極大的恐慌郊愧,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,386評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件井佑,死亡現(xiàn)場離奇詭異属铁,居然都是意外死亡,警方通過查閱死者的電腦和手機躬翁,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,142評論 3 394
  • 文/潘曉璐 我一進店門焦蘑,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人盒发,你說我怎么就攤上這事例嘱。” “怎么了迹辐?”我有些...
    開封第一講書人閱讀 164,704評論 0 353
  • 文/不壞的土叔 我叫張陵蝶防,是天一觀的道長甚侣。 經(jīng)常有香客問我明吩,道長,這世上最難降的妖魔是什么殷费? 我笑而不...
    開封第一講書人閱讀 58,702評論 1 294
  • 正文 為了忘掉前任印荔,我火速辦了婚禮,結果婚禮上详羡,老公的妹妹穿的比我還像新娘仍律。我一直安慰自己,他們只是感情好实柠,可當我...
    茶點故事閱讀 67,716評論 6 392
  • 文/花漫 我一把揭開白布水泉。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪草则。 梳的紋絲不亂的頭發(fā)上钢拧,一...
    開封第一講書人閱讀 51,573評論 1 305
  • 那天,我揣著相機與錄音炕横,去河邊找鬼源内。 笑死,一個胖子當著我的面吹牛份殿,可吹牛的內容都是我干的膜钓。 我是一名探鬼主播,決...
    沈念sama閱讀 40,314評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼卿嘲,長吁一口氣:“原來是場噩夢啊……” “哼颂斜!你這毒婦竟也來了?” 一聲冷哼從身側響起拾枣,我...
    開封第一講書人閱讀 39,230評論 0 276
  • 序言:老撾萬榮一對情侶失蹤焚鲜,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后放前,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體忿磅,經(jīng)...
    沈念sama閱讀 45,680評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,873評論 3 336
  • 正文 我和宋清朗相戀三年凭语,在試婚紗的時候發(fā)現(xiàn)自己被綠了葱她。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,991評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡似扔,死狀恐怖吨些,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情炒辉,我是刑警寧澤豪墅,帶...
    沈念sama閱讀 35,706評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站黔寇,受9級特大地震影響偶器,放射性物質發(fā)生泄漏。R本人自食惡果不足惜缝裤,卻給世界環(huán)境...
    茶點故事閱讀 41,329評論 3 330
  • 文/蒙蒙 一屏轰、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧憋飞,春花似錦霎苗、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,910評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽内狸。三九已至,卻和暖如春厘擂,著一層夾襖步出監(jiān)牢的瞬間答倡,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,038評論 1 270
  • 我被黑心中介騙來泰國打工驴党, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留瘪撇,地道東北人。 一個月前我還...
    沈念sama閱讀 48,158評論 3 370
  • 正文 我出身青樓港庄,卻偏偏與公主長得像倔既,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子鹏氧,可洞房花燭夜當晚...
    茶點故事閱讀 44,941評論 2 355

推薦閱讀更多精彩內容