GreenDao緩存機(jī)制探索

GreenDao是Android中使用比較廣泛的一個(gè)orm數(shù)據(jù)庫,以高效和便捷著稱蔫浆。在項(xiàng)目開發(fā)過程中遇到過好幾次特別奇葩的問題韧掩,最后排查下來,發(fā)現(xiàn)還是由于不熟悉它的緩存機(jī)制引起的。下面是自己稍微閱讀了下它的源碼后做的記錄俄删,避免以后發(fā)現(xiàn)類似的問題。

緩存機(jī)制相關(guān)源碼

DaoMaster

DaoMaster是GreenDao的入口,它的繼承自AbstractDaoMaster畴椰,有三個(gè)重要的參數(shù)臊诊,分別是實(shí)例、版本和Dao的信息斜脂。

//數(shù)據(jù)庫示例
protected final SQLiteDatabase db;

//數(shù)據(jù)庫版本
protected final int schemaVersion;

//dao和daoconfig的配置
protected final Map<Class<? extends AbstractDao<?, ?>>, DaoConfig> daoConfigMap;

DaoMaster中還有兩個(gè)重要的方法:createAllTables和dropAllTables抓艳,和一個(gè)抽象的OpenHelper類,該類繼承自系統(tǒng)的SQLiteOpenHelper類帚戳,主要用于數(shù)據(jù)庫創(chuàng)建的時(shí)候初始化所有數(shù)據(jù)表玷或。

創(chuàng)建DaoMaster需要傳入SQLiteDatabase的實(shí)例,一般如下創(chuàng)建:

 mDaoMaster = new DaoMaster(helper.getWritableDatabase())
 

跟蹤代碼可知數(shù)據(jù)庫的初始化和升降級都是在調(diào)用helper.getWritableDatabase()時(shí)執(zhí)行的片任。相關(guān)代碼在SQLiteOpenHelper類中偏友。

在getWritableDatabase方法中會調(diào)用getDatabaseLocked方法

 public SQLiteDatabase getWritableDatabase() {
        synchronized (this) {
            return getDatabaseLocked(true);
        }
    }

getDatabaseLocked方法如下

    private SQLiteDatabase getDatabaseLocked(boolean writable) {
         // 首先方法接收一個(gè)是否可讀的參數(shù)
        if (mDatabase != null) {
            if (!mDatabase.isOpen()) {
                //數(shù)據(jù)庫沒有打開,關(guān)閉并且置空
                mDatabase.close().
                mDatabase = null;
            } else if (!writable || !mDatabase.isReadOnly()) {
                //只讀或者數(shù)據(jù)庫已經(jīng)是讀寫狀態(tài)了对供,則直接返回實(shí)例
                return mDatabase;
            }
        }

        if (mIsInitializing) {
            throw new IllegalStateException("getDatabase called recursively");
        }

        SQLiteDatabase db = mDatabase;
        try {
            mIsInitializing = true;

            if (db != null) {
                if (writable && db.isReadOnly()) {
                    //只讀狀態(tài)的時(shí)候打開讀寫
                    db.reopenReadWrite();
                }
            } else if (mName == null) {
                db = SQLiteDatabase.create(null);
            } else {
                //創(chuàng)建數(shù)據(jù)庫實(shí)例
                位他。。产场。代碼省略鹅髓。。涝动。

            //調(diào)用子類的onConfigure方法
            onConfigure(db);

            final int version = db.getVersion();
            if (version != mNewVersion) {
                if (db.isReadOnly()) {
                    throw new SQLiteException("Can't upgrade read-only database from version " +
                            db.getVersion() + " to " + mNewVersion + ": " + mName);
                }

                db.beginTransaction();
                try {
                                if (version == 0) {
                        // 如果版本為0的時(shí)候初始化數(shù)據(jù)庫迈勋,調(diào)用子類的onCreate方法。
                        onCreate(db);
                    } else {
                        //處理升降級
                        if (version > mNewVersion) {
                            onDowngrade(db, version, mNewVersion);
                        } else {
                            onUpgrade(db, version, mNewVersion);
                        }
                              db.setVersion(mNewVersion);
                    db.setTransactionSuccessful();
                } finally {
                    db.endTransaction();
                }
            }

            onOpen(db);

            if (db.isReadOnly()) {
                Log.w(TAG, "Opened " + mName + " in read-only mode");
            }

            mDatabase = db;
            return db;
        } finally {
            mIsInitializing = false;
            if (db != null && db != mDatabase) {
                db.close();
            }
        }}
醋粟。靡菇。。代碼省略米愿。厦凤。。
    }

greendao的緩存到底是如何實(shí)現(xiàn)的呢育苟?

DaoMaster構(gòu)造方法中會把所有的Dao類注冊到Map中较鼓,每個(gè)Dao對應(yīng)一個(gè)DaoConfig配置類。

protected void registerDaoClass(Class<? extends AbstractDao<?, ?>> daoClass) {
        DaoConfig daoConfig = new DaoConfig(db, daoClass);
        daoConfigMap.put(daoClass, daoConfig);
    }
    

DaoConfig是對數(shù)據(jù)庫表的一個(gè)抽象违柏,有數(shù)據(jù)庫實(shí)例博烂、表名、字段列表漱竖、SQL statements等類變量禽篱,最重要的是IdentityScope,它是GreenDao實(shí)現(xiàn)數(shù)據(jù)緩存的關(guān)鍵馍惹。在DaoSession類初始化的時(shí)候IdentityScope初始化躺率,可以根據(jù)參數(shù)IdentityScopeType.SessionIdentityScopeType.None來配置是否開啟緩存玛界。

IdentityScope接口有兩個(gè)實(shí)現(xiàn)類,分別是IdentityScopeLongIdentityScopeObject悼吱,它們的實(shí)現(xiàn)類似慎框,都是維護(hù)一個(gè)Map存放key和value,然后有一些put、get后添、remove笨枯、clear等方法,最主要的區(qū)別是前者的key是long吕朵,可以實(shí)現(xiàn)更高的讀寫效率猎醇,后面的key是Object窥突。

判斷主鍵字段類型是否是數(shù)字類型努溃,如果是的話則使用IdentityScopeLong類型來緩存數(shù)據(jù),否則使用IdentityScopeObject類型阻问。

keyIsNumeric = type.equals(long.class) || type.equals(Long.class) || type.equals(int.class)|| type.equals(Integer.class) || type.equals(short.class) || type.equals(Short.class)|| type.equals(byte.class) || type.equals(Byte.class);
                        
                        
public void initIdentityScope(IdentityScopeType type) {
        if (type == IdentityScopeType.None) {
            identityScope = null;
        } else if (type == IdentityScopeType.Session) {
            if (keyIsNumeric) {
                identityScope = new IdentityScopeLong();
            } else {
                identityScope = new IdentityScopeObject();
            }
        } else {
            throw new IllegalArgumentException("Unsupported type: " + type);
        }
    }

緩存的使用

數(shù)據(jù)讀取

以Query類中l(wèi)ist方法為例梧税,跟蹤代碼可知,最后會調(diào)用AbstractDao的loadCurrent方法称近,它首先會根據(jù)主鍵判斷dentityScope中有沒有對應(yīng)的緩存第队,如何有直接返回,如果沒有才會讀取Cursor里面的數(shù)據(jù)刨秆。

final protected T loadCurrent(Cursor cursor, int offset, boolean lock) {
        if (identityScopeLong != null) {
            if (offset != 0) {
                // Occurs with deep loads (left outer joins)
                if (cursor.isNull(pkOrdinal + offset)) {
                    return null;
                }
            }
           //讀取主鍵
            long key = cursor.getLong(pkOrdinal + offset);
            //讀取緩存
            T entity = lock ? identityScopeLong.get2(key) :identityScopeLong.get2NoLock(key);
            if (entity != null) {
            //如果有凳谦,直接返回
                return entity;
            } else {
                //如果沒有,讀取游標(biāo)中的值
                entity = readEntity(cursor, offset);
                attachEntity(entity);
                //把數(shù)據(jù)更新到緩存中
                if (lock) {
                    identityScopeLong.put2(key, entity);
                } else {
                    identityScopeLong.put2NoLock(key, entity);
                }
                return entity;
            }
        } else if (identityScope != null) {
            K key = readKey(cursor, offset);
            if (offset != 0 && key == null) {
                // Occurs with deep loads (left outer joins)
                return null;
            }
            T entity = lock ? identityScope.get(key) : identityScope.getNoLock(key);
            if (entity != null) {
                return entity;
            } else {
                entity = readEntity(cursor, offset);
                attachEntity(key, entity, lock);
                return entity;
            }
        } else {
            // Check offset, assume a value !=0 indicating a potential outer join, so check PK
            if (offset != 0) {
                K key = readKey(cursor, offset);
                if (key == null) {
                    // Occurs with deep loads (left outer joins)
                    return null;
                }
            }
            T entity = readEntity(cursor, offset);
            attachEntity(entity);
            return entity;
        }
    }
    

數(shù)據(jù)刪除

我們經(jīng)常使用DeleteQuery的executeDeleteWithoutDetachingEntities來?xiàng)l件刪除數(shù)據(jù)衡未,這時(shí)候是不清除緩存的尸执,當(dāng)用主鍵查詢的時(shí)候,還是會返回緩存中的數(shù)據(jù)缓醋。

Deletes all matching entities without detaching them from the identity scope (aka session/cache). Note that this method may lead to stale entity objects in the session cache. Stale entities may be returned when loaded by their primary key, but not using queries.

使用對象的方式刪除數(shù)據(jù)的時(shí)候如失,比如deleteInTx()等面向?qū)ο蟮姆椒〞r(shí),會刪除對應(yīng)的緩存送粱。在AbstractDao中deleteInTxInternal方法里面褪贵,會調(diào)用identityScope的remove方法。

if (keysToRemoveFromIdentityScope != null && identityScope != null) {
    identityScope.remove(keysToRemoveFromIdentityScope);
}
            

數(shù)據(jù)插入

以insert方法為例抗俄,它會在插入成功之后脆丁,調(diào)用attachEntity方法,存放緩存數(shù)據(jù)动雹。

protected final void attachEntity(K key, T entity, boolean lock) {
        attachEntity(entity);
        if (identityScope != null && key != null) {
            if (lock) {
                identityScope.put(key, entity);
            } else {
                identityScope.putNoLock(key, entity);
            }
        }
    }

數(shù)據(jù)更新

數(shù)據(jù)update的時(shí)候也會調(diào)用attachEntity方法槽卫。

緩存帶來的坑和脫坑方案

1.觸發(fā)器引起的數(shù)據(jù)不同步

我們在項(xiàng)目中有這么一個(gè)需求,當(dāng)改變A對象的a字段的時(shí)候洽胶,要同時(shí)改變B對象的b字段晒夹,觸發(fā)器代碼類似如下裆馒。

String sql = "create trigger 觸發(fā)器名 after insert on 表B "
                    + "begin update 表A set 字段A.a = NEW. 字段B.b where 字段A.b = NEW.字段B.c; end;";
db.execSQL(sql);

b是A的外鍵,映射到表B的b字段丐怯。

這樣設(shè)置觸發(fā)器之后喷好,更新表B數(shù)據(jù)的時(shí)候,會自動把更新同步到表A读跷,但是這樣其實(shí)沒有更新表A對應(yīng)DAO的緩存梗搅,當(dāng)查詢表A的時(shí)候還是更新前的數(shù)據(jù)。

解決方案:
1.在greendao2.x版本中效览,可以暴露DaoSession中對應(yīng)的DaoConfig
,然后調(diào)用daoConfig.clearIdentityScope()无切;在3.x版本中可以直接調(diào)用dao類的detachAll方法,它會清除所有的緩存丐枉。 同時(shí)也可以調(diào)用Entity的refresh方法來刷新緩存哆键。

 public void detachAll() {
        if (identityScope != null) {
            identityScope.clear();
        }
    }

上面的方法都是通過清除緩存來保證數(shù)據(jù)的同步性,但是頻繁的清除緩存就大大影響數(shù)據(jù)查詢效率瘦锹,不建議這么使用籍嘹。

2.盡量不要使用觸發(fā)器,最好使用greenDao自帶的一些接口弯院,絕大部分情況下都是能滿足要求的辱士。對于能否使用觸發(fā)器,開發(fā)者做了解釋听绳。

greenDAO uses a plain SQLite database. To use triggers you have to do a regular raw SQL query on your database. greenDAO can not help you there.

2.自定義SQL帶來的數(shù)據(jù)不同步問題

項(xiàng)目中即使使用了GreenDao颂碘,我們還是免不了使用自定義的sql語句來操作數(shù)據(jù)庫,類似下面較復(fù)雜的查詢功能椅挣。

  String sql = "select *, count(distinct " + columnPkgName + ") from " + tableName + " where STATUS = 0" + " group by " + columnPkgName
                + " order by " + columnTimestamp + " desc limit " + limitCount + " offset 0;";
        Cursor query = mDaoMaster.getDatabase().rawQuery(sql, new String[] {});

這種查詢語句除了沒有使用GreenDao的緩存头岔,其它倒是沒有什么問題。但是一旦使用update或者delete等接口時(shí)贴妻,就會引起數(shù)據(jù)的不同步切油,因?yàn)閿?shù)據(jù)庫里面的數(shù)據(jù)更新了,但是greenDao里面的緩存還是舊的名惩。

總結(jié):使用第三方庫的時(shí)候澎胡,最好能夠深入理解它的代碼,不然遇到坑了都不知道怎么爬出來娩鹉,像greendao這種攻谁,由于自己不合理使用導(dǎo)致的問題還是很多的。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末弯予,一起剝皮案震驚了整個(gè)濱河市戚宦,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌锈嫩,老刑警劉巖受楼,帶你破解...
    沈念sama閱讀 217,084評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件垦搬,死亡現(xiàn)場離奇詭異,居然都是意外死亡艳汽,警方通過查閱死者的電腦和手機(jī)猴贰,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,623評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來河狐,“玉大人米绕,你說我怎么就攤上這事〔鲆眨” “怎么了栅干?”我有些...
    開封第一講書人閱讀 163,450評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長捐祠。 經(jīng)常有香客問我碱鳞,道長,這世上最難降的妖魔是什么雏赦? 我笑而不...
    開封第一講書人閱讀 58,322評論 1 293
  • 正文 為了忘掉前任劫笙,我火速辦了婚禮芙扎,結(jié)果婚禮上星岗,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,370評論 6 390
  • 文/花漫 我一把揭開白布熄浓。 她就那樣靜靜地躺著辨萍,像睡著了一般。 火紅的嫁衣襯著肌膚如雪胀屿。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,274評論 1 300
  • 那天,我揣著相機(jī)與錄音召耘,去河邊找鬼。 笑死褐隆,一個(gè)胖子當(dāng)著我的面吹牛污它,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播庶弃,決...
    沈念sama閱讀 40,126評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼衫贬,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了歇攻?” 一聲冷哼從身側(cè)響起固惯,我...
    開封第一講書人閱讀 38,980評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎缴守,沒想到半個(gè)月后葬毫,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體镇辉,經(jīng)...
    沈念sama閱讀 45,414評論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,599評論 3 334
  • 正文 我和宋清朗相戀三年贴捡,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了摊聋。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,773評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡栈暇,死狀恐怖麻裁,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情源祈,我是刑警寧澤煎源,帶...
    沈念sama閱讀 35,470評論 5 344
  • 正文 年R本政府宣布,位于F島的核電站香缺,受9級特大地震影響手销,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜图张,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,080評論 3 327
  • 文/蒙蒙 一锋拖、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧祸轮,春花似錦兽埃、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,713評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至苦酱,卻和暖如春售貌,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背疫萤。 一陣腳步聲響...
    開封第一講書人閱讀 32,852評論 1 269
  • 我被黑心中介騙來泰國打工颂跨, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人扯饶。 一個(gè)月前我還...
    沈念sama閱讀 47,865評論 2 370
  • 正文 我出身青樓恒削,卻偏偏與公主長得像,于是被迫代替她去往敵國和親帝际。 傳聞我的和親對象是個(gè)殘疾皇子蔓同,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,689評論 2 354

推薦閱讀更多精彩內(nèi)容

  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法,類相關(guān)的語法蹲诀,內(nèi)部類的語法斑粱,繼承相關(guān)的語法,異常的語法脯爪,線程的語...
    子非魚_t_閱讀 31,625評論 18 399
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理则北,服務(wù)發(fā)現(xiàn)矿微,斷路器,智...
    卡卡羅2017閱讀 134,654評論 18 139
  • GreenDao 介紹:greenDAO是一個(gè)對象關(guān)系映射(ORM)的框架尚揣,能夠提供一個(gè)接口通過操作對象的方式去操...
    小董666閱讀 732評論 0 1
  • 將項(xiàng)目從greenDAO從2.x版本升級到最新的3.2版本涌矢,最大變化是可以用注解代替以前的java生成器。實(shí)現(xiàn)這點(diǎn)...
    展翅而飛閱讀 9,036評論 6 21
  • greenDAO greenDAO 是一個(gè)將對象映射到 SQLite 數(shù)據(jù)庫中的輕量且快速的 ORM 解決方案快骗。它...
    蕉下孤客閱讀 16,095評論 18 104