GreenDao是Android中使用比较广泛的一个orm数据库,以高效和便捷著称。在项目开发过程中遇到过好几次特别奇葩的问题,最后排查下来,发现还是由于不熟悉它的缓存机制引起的。下面是自己稍微阅读了下它的源码后做的记录,避免以后发现类似的问题。
缓存机制相关源码 DaoMaster DaoMaster是GreenDao的入口,它的继承自AbstractDaoMaster,有三个重要的参数,分别是实例、版本和Dao的信息。
1 2 3 4 5 6 7 8 //数据库示例 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的实例,一般如下创建:
1 mDaoMaster = new DaoMaster(helper.getWritableDatabase())
跟踪代码可知数据库的初始化和升降级都是在调用helper.getWritableDatabase()时执行的。相关代码在SQLiteOpenHelper类中。
在getWritableDatabase方法中会调用getDatabaseLocked方法
1 2 3 4 5 public SQLiteDatabase getWritableDatabase() { synchronized (this) { return getDatabaseLocked(true); } }
getDatabaseLocked方法如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 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配置类。
1 2 3 4 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.Session和IdentityScopeType.None来配置是否开启缓存。
IdentityScope接口有两个实现类,分别是IdentityScopeLong和IdentityScopeObject,它们的实现类似,都是维护一个Map存放key和value,然后有一些put、get、remove、clear等方法,最主要的区别是前者的key是long,可以实现更高的读写效率,后面的key是Object。
判断主键字段类型是否是数字类型,如果是的话则使用IdentityScopeLong类型来缓存数据,否则使用IdentityScopeObject类型。
1 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);
1 2 3 4 5 6 7 8 9 10 11 12 13 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里面的数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 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方法。
1 2 3 if (keysToRemoveFromIdentityScope != null && identityScope != null) { identityScope.remove(keysToRemoveFromIdentityScope); }
数据插入 以insert方法为例,它会在插入成功之后,调用attachEntity方法,存放缓存数据。
1 2 3 4 5 6 7 8 9 10 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字段,触发器代码类似如下。
1 2 3 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方法来刷新缓存。
1 2 3 4 5 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语句来操作数据库,类似下面较复杂的查询功能。
1 2 3 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这种,由于自己不合理使用导致的问题还是很多的。