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

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

  1. 不支持線程回調(diào)
  2. 不支持默認(rèn)數(shù)據(jù)類型

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

線程回調(diào)問題

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

ANR.jpg

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


項(xiàng)目崩潰前跑路.gif

看到這里不要崩潰,開玩笑的,事情當(dāng)然沒有那么嚴(yán)重,問題是可以解決的,我們大可以在外部包裹線程實(shí)現(xiàn)GreenDao的異步調(diào)用,無論是AsyncTask還是自己實(shí)現(xiàn)一個(gè)線程異步類,查詢時(shí)開啟一個(gè)線程,查詢結(jié)果出來后再回調(diào)到主線程即可,工作量也不大,不過還是覺得GreenDao能提供的話還是要方便很多

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

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

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

簡(jiǎn)單粗暴的升級(jí)方法.png

但是 這種方式畢竟過于簡(jiǎn)單粗暴且不夠優(yōu)雅,最重要的是并不適合我的模塊,我一開始的思路是先取出數(shù)據(jù)保留再內(nèi)存中,然后將舊數(shù)據(jù)通過添加新字段默認(rèn)值的方式升級(jí)為新表適用的數(shù)據(jù),然后刪除舊表,創(chuàng)建新標(biāo),將轉(zhuǎn)換后的數(shù)據(jù)插入到新表中,但是不知道為什么這種方式看似沒毛病(肯定有毛病)但是每次都會(huì)報(bào)錯(cuò)(報(bào)錯(cuò)信息沒保留下來),不得已只能尋找另外別的方法,找來找去,發(fā)現(xiàn)有一種方法是通過升級(jí)時(shí)創(chuàng)建一個(gè)臨時(shí)表將數(shù)據(jù)保留下來,然后進(jìn)行表的升級(jí)再,再將數(shù)據(jù)轉(zhuǎn)移到新表中來實(shí)現(xiàn)的,這種方式和我的做法其實(shí)有點(diǎn)象,但可能考慮得比我多,不是將數(shù)據(jù)留在內(nèi)存中,而且經(jīng)過實(shí)測(cè)實(shí)際有用,我將代碼貼出來大家可以方便取用


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)建臨時(shí)表的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í)表插入數(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", "銷毀臨時(shí)表的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;
    }
}

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


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

示例如下:

升級(jí)調(diào)用.png

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


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

這樣依賴我們可以一行代碼完成所有數(shù)據(jù)表的無損升級(jí),是不是感覺非常方便,但是這里也有一個(gè)問題,就是上面提到的,Greendao不支持默認(rèn)數(shù)據(jù),如果不賦值,那么在表中的字段都為null,也就是說我們升級(jí)之后的新表中從舊表轉(zhuǎn)移過去的數(shù)據(jù)新增的那個(gè)字段都為null,更坑爹的是因?yàn)槊看螌?shí)體類都需要手動(dòng)生成,所以我們也不太可能去實(shí)體類中設(shè)置默認(rèn)值,即使設(shè)置默認(rèn)值最后也還是會(huì)為null,因?yàn)閺臄?shù)據(jù)庫中讀取時(shí)會(huì)將null設(shè)置進(jìn)該字段而覆蓋默認(rèn)值,最后取到的還是null,所以我們需要在接下來的調(diào)用中對(duì)該實(shí)體類對(duì)象字段的使用謹(jǐn)慎地判空,我就被坑過,而且實(shí)在代碼被提交測(cè)試之后才發(fā)現(xiàn)的,這也是我不推薦使用Greendao的原因之一,greendao的團(tuán)隊(duì)太過傲嬌,居然連默認(rèn)數(shù)據(jù)這么重要的API都不提供

2017-08-15更新

上面的方法在實(shí)際生產(chǎn)中被驗(yàn)證可以解決問題,但存在缺陷:每次都要將需要保留的數(shù)據(jù)添加進(jìn)migrate()方法中,即使沒有升級(jí)數(shù)據(jù)表的Dao類,因?yàn)椴僮髦袝?huì)將添加進(jìn)去的表備份然后刪除所有的表,再將所有的備份過的表恢復(fù),長此以往要保留的數(shù)據(jù)表多了自然會(huì)有影響,且可能引發(fā)未知問題,替代解決方案請(qǐng)參考文尾--另外的解決方案

另外的解決辦法

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


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

實(shí)際代碼的方法封裝如下:


        /**
         * 升級(jí)數(shù)據(jù)庫時(shí)動(dòng)態(tài)插入一列
         *
         * @param db             數(shù)據(jù)庫實(shí)體
         * @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);
        }

也可以在插入新字段時(shí)指定字段位置,如插入于某字段之前,SQL代碼如下


alter table table_name add column 字段名 字段類型 after 某字段;--這個(gè)方法就不知道要不要帶括號(hào)了

當(dāng)然這個(gè)方法我也沒有嘗試是否有用,只是我的想法,有需求或有興趣的同學(xué)也可以去試一下(2017-08-15更新:實(shí)測(cè)可用)

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容