GreenDao缓存机制探索

GreenDao是Android中使用比较广泛的一个orm数据库,以高效和便捷著称。在项目开发过程中遇到过好几次特别奇葩的问题,最后排查下来,发现还是由于不熟悉它的缓存机制引起的。下面是自己稍微阅读了下它的源码后做的记录,避免以后发现类似的问题。

缓存机制相关源码

DaoMaster

DaoMaster是GreenDao的入口,它的继承自AbstractDaoMaster,有三个重要的参数,分别是实例、版本和Dao的信息。

//数据库示例
protected final SQLiteDatabase db;

//数据库版本
protected final int schemaVersion;

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

DaoMaster中还有两个重要的方法:createAllTables和dropAllTables,和一个抽象的OpenHelper类,该类继承自系统的SQLiteOpenHelper类,主要用于数据库创建的时候初始化所有数据表。

创建DaoMaster需要传入SQLiteDatabase的实例,一般如下创建:

 mDaoMaster = new DaoMaster(helper.getWritableDatabase())
 

跟踪代码可知数据库的初始化和升降级都是在调用helper.getWritableDatabase()时执行的。相关代码在SQLiteOpenHelper类中。

在getWritableDatabase方法中会调用getDatabaseLocked方法

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

getDatabaseLocked方法如下

    private SQLiteDatabase getDatabaseLocked(boolean writable) {
         // 首先方法接收一个是否可读的参数
        if (mDatabase != null) {
            if (!mDatabase.isOpen()) {
                //数据库没有打开,关闭并且置空
                mDatabase.close().
                mDatabase = null;
            } else if (!writable || !mDatabase.isReadOnly()) {
                //只读或者数据库已经是读写状态了,则直接返回实例
                return mDatabase;
            }
        }

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

        SQLiteDatabase db = mDatabase;
        try {
            mIsInitializing = true;

            if (db != null) {
                if (writable && db.isReadOnly()) {
                    //只读状态的时候打开读写
                    db.reopenReadWrite();
                }
            } else if (mName == null) {
                db = SQLiteDatabase.create(null);
            } else {
                //创建数据库实例
                。。。代码省略。。。

            //调用子类的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的时候初始化数据库,调用子类的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的缓存到底是如何实现的呢?

DaoMaster构造方法中会把所有的Dao类注册到Map中,每个Dao对应一个DaoConfig配置类。

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

DaoConfig是对数据库表的一个抽象,有数据库实例、表名、字段列表、SQL statements等类变量,最重要的是IdentityScope,它是GreenDao实现数据缓存的关键。在DaoSession类初始化的时候IdentityScope初始化,可以根据参数IdentityScopeType.SessionIdentityScopeType.None来配置是否开启缓存。

IdentityScope接口有两个实现类,分别是IdentityScopeLongIdentityScopeObject,它们的实现类似,都是维护一个Map存放key和value,然后有一些put、get、remove、clear等方法,最主要的区别是前者的key是long,可以实现更高的读写效率,后面的key是Object。

判断主键字段类型是否是数字类型,如果是的话则使用IdentityScopeLong类型来缓存数据,否则使用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);
        }
    }

缓存的使用

数据读取

以Query类中list方法为例,跟踪代码可知,最后会调用AbstractDao的loadCurrent方法,它首先会根据主键判断dentityScope中有没有对应的缓存,如何有直接返回,如果没有才会读取Cursor里面的数据。

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 {
                //如果没有,读取游标中的值
                entity = readEntity(cursor, offset);
                attachEntity(entity);
                //把数据更新到缓存中
                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;
        }
    }
    

数据删除

我们经常使用DeleteQuery的executeDeleteWithoutDetachingEntities来条件删除数据,这时候是不清除缓存的,当用主键查询的时候,还是会返回缓存中的数据。

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.

使用对象的方式删除数据的时候,比如deleteInTx()等面向对象的方法时,会删除对应的缓存。在AbstractDao中deleteInTxInternal方法里面,会调用identityScope的remove方法。

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

数据插入

以insert方法为例,它会在插入成功之后,调用attachEntity方法,存放缓存数据。

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

数据更新

数据update的时候也会调用attachEntity方法。

缓存带来的坑和脱坑方案

1.触发器引起的数据不同步

我们在项目中有这么一个需求,当改变A对象的a字段的时候,要同时改变B对象的b字段,触发器代码类似如下。

String sql = "create trigger 触发器名 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字段。

这样设置触发器之后,更新表B数据的时候,会自动把更新同步到表A,但是这样其实没有更新表A对应DAO的缓存,当查询表A的时候还是更新前的数据。

解决方案:
1.在greendao2.x版本中,可以暴露DaoSession中对应的DaoConfig
,然后调用daoConfig.clearIdentityScope();在3.x版本中可以直接调用dao类的detachAll方法,它会清除所有的缓存。 同时也可以调用Entity的refresh方法来刷新缓存。

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

上面的方法都是通过清除缓存来保证数据的同步性,但是频繁的清除缓存就大大影响数据查询效率,不建议这么使用。

2.尽量不要使用触发器,最好使用greenDao自带的一些接口,绝大部分情况下都是能满足要求的。对于能否使用触发器,开发者做了解释。

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带来的数据不同步问题

项目中即使使用了GreenDao,我们还是免不了使用自定义的sql语句来操作数据库,类似下面较复杂的查询功能。

  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等接口时,就会引起数据的不同步,因为数据库里面的数据更新了,但是greenDao里面的缓存还是旧的。

总结:使用第三方库的时候,最好能够深入理解它的代码,不然遇到坑了都不知道怎么爬出来,像greendao这种,由于自己不合理使用导致的问题还是很多的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容

  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,577评论 18 399
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,596评论 18 139
  • GreenDao 介绍:greenDAO是一个对象关系映射(ORM)的框架,能够提供一个接口通过操作对象的方式去操...
    小董666阅读 726评论 0 1
  • 将项目从greenDAO从2.x版本升级到最新的3.2版本,最大变化是可以用注解代替以前的java生成器。实现这点...
    展翅而飞阅读 9,004评论 6 21
  • greenDAO greenDAO 是一个将对象映射到 SQLite 数据库中的轻量且快速的 ORM 解决方案。它...
    蕉下孤客阅读 16,080评论 18 104