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.Session和IdentityScopeType.None來配置是否開啟緩存玛界。
IdentityScope接口有兩個(gè)實(shí)現(xiàn)類,分別是IdentityScopeLong和IdentityScopeObject悼吱,它們的實(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)致的問題還是很多的。