採坑記之greendao緩存
項目裏面ORM框架用的greendao.測試中出現一個問題,在一個界面獲取數據庫的一個對象,然後更改對象的屬性值,沒有點擊保存按鈕。再進入這個界面時,從數據庫同樣獲取的這個對象居然改變了。
之前有看到網上說greendao有緩存,所以獲取數據比較快,我猜想這裏碰到的應該也是這個問題。
我模擬獲取數據對象的示例代碼,首先拿到對象,然後設置一個屬性後,再去數據庫獲取對象打印這個屬性值
UserInfoDomain userInfoDomain=VeryFitPlusDao.getInstance().getUserInfoDomain();
DebugLog.d(userInfoDomain.getShowName());
userInfoDomain.setShowName("hahahah");
DebugLog.d(VeryFitPlusDao.getInstance().getUserInfoDomain().getShowName());
獲取用戶信息的代碼也很簡單,直接調用greendao的API
/**
* 獲取當前用戶信息
*/
public UserInfoDomain getUserInfoDomain(){
UserInfoDomainDao dao= daoSession.getUserInfoDomainDao();
UserInfoDomain userInfoDomain=dao.queryBuilder().where(UserInfoDomainDao.Properties.UserId.eq(getUserId())).unique();
if (userInfoDomain!=null){
DebugLog.d("獲取的用戶信息:"+userInfoDomain.toString());
return userInfoDomain;
}else{
userInfoDomain=new UserInfoDomain();
return userInfoDomain;
}
}
結果打印的值
到底經歷了啥??我都沒有調用保存數據庫的代碼啊!!!
憋急,先把greendao獲取數據的源碼追蹤看一遍。
首先根據條件獲取對象
HealthSleepDomain sleepDomain=dao.queryBuilder().where(dao.queryBuilder().and(conditionDate,conditionUserId)).unique()
調用的是QueryBuilder的unique方法
QueryBuilder
public T unique() {
return build().unique();
}
繼續查看.調用的是daoAccess對象的loadUniqueAndCloseCursor方法
QueryBuilder
public T unique() {
checkThread();
Cursor cursor = dao.getDatabase().rawQuery(sql, parameters);
return daoAccess.loadUniqueAndCloseCursor(cursor);
}
daoAccess是一個InternalQueryDaoAccess對象。裏面的方法去交給dao去處理了,而dao就算一個繼承了AbstractDao的類
InternalQueryDaoAccess
public T loadUniqueAndCloseCursor(Cursor cursor) {
return dao.loadUniqueAndCloseCursor(cursor);
}
繼續查看AbstractDao的loadUniqueAndCloseCursor方法。內部又調用了loadUnique方法
AbstractDao
protected T loadUniqueAndCloseCursor(Cursor cursor) {
try {
return loadUnique(cursor);
} finally {
cursor.close();
}
}
loadUnique方法,判斷是否有數據,如果沒有數據直接返回null,有數據裏面調用了loadCurrent方法
protected T loadUnique(Cursor cursor) {
boolean available = cursor.moveToFirst();
if (!available) {
return null;
} else if (!cursor.isLast()) {
throw new DaoException("Expected unique result, but count was " + cursor.getCount());
}
return loadCurrent(cursor, 0, true);
}
loadCurrent方法纔是關鍵,首先判斷了一個identityScopeLong對象是否爲null,如果不爲null,則根據key判斷identityScopeLong裏面是否有該對象,如果有直接返回,沒有就去數據庫查詢,查詢完再放入identityScopeLong。identityScopeLong並不是一個Map集合,但它裏面有個類似Map集合的對象,它實現了類似Map集合存放key-value方法。可以看到,這個identityScopeLong對象應該就是緩存元兇了。
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;
}
}
從源頭看看這個identityScopeLong是怎麼創建的,
首先daoSession初始化的地方
DaoMaster.OpenHelper daoMaster = new DaoMaster.DevOpenHelper(context.getApplicationContext(), Constant.DONGHA_DB_NAME, null);
daoSession = new DaoMaster(daoMaster.getWritableDb()).newSession();
DaoSession 直接是new了一個對象,傳入了IdentityScopeType.Session
public DaoSession newSession() {
return new DaoSession(db, IdentityScopeType.Session, daoConfigMap);
}
調用了DaoConfig的initIdentityScope方法
public DaoSession(Database db, IdentityScopeType type, Map<Class<? extends AbstractDao<?, ?>>, DaoConfig>
daoConfigMap) {
super(db);
aDLatLngDomainDaoConfig = daoConfigMap.get(ADLatLngDomainDao.class).clone();
aDLatLngDomainDaoConfig.initIdentityScope(type);
DaoConfig 的initIdentityScope方法根據類型判斷,keyIsNumeric變量判斷表裏面是否有主鍵
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);
}
}
keyIsNumeric變量賦值是在構造方法裏面進行的
public DaoConfig(Database db, Class<? extends AbstractDao<?, ?>> daoClass) {
this.db = db;
try {
this.tablename = (String) daoClass.getField("TABLENAME").get(null);
Property[] properties = reflectProperties(daoClass);
this.properties = properties;
allColumns = new String[properties.length];
List<String> pkColumnList = new ArrayList<String>();
List<String> nonPkColumnList = new ArrayList<String>();
Property lastPkProperty = null;
for (int i = 0; i < properties.length; i++) {
Property property = properties[i];
String name = property.columnName;
allColumns[i] = name;
//判斷是否有主鍵
if (property.primaryKey) {
pkColumnList.add(name);
lastPkProperty = property;
} else {
nonPkColumnList.add(name);
}
}
String[] nonPkColumnsArray = new String[nonPkColumnList.size()];
nonPkColumns = nonPkColumnList.toArray(nonPkColumnsArray);
String[] pkColumnsArray = new String[pkColumnList.size()];
pkColumns = pkColumnList.toArray(pkColumnsArray);
pkProperty = pkColumns.length == 1 ? lastPkProperty : null;
statements = new TableStatements(db, tablename, allColumns, pkColumns);
//如果有主鍵
if (pkProperty != null) {
Class<?> type = pkProperty.type;
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);
} else {
keyIsNumeric = false;
}
} catch (Exception e) {
throw new DaoException("Could not init DAOConfig", e);
}
}
每個Dao對象初始化的時候,都會傳入對應的config。而identityScopeLong就算從config裏面獲取的
public AbstractDao(DaoConfig config, AbstractDaoSession daoSession) {
this.config = config;
this.session = daoSession;
db = config.db;
isStandardSQLite = db.getRawDatabase() instanceof SQLiteDatabase;
identityScope = (IdentityScope<K, T>) config.getIdentityScope();
if (identityScope instanceof IdentityScopeLong) {
identityScopeLong = (IdentityScopeLong<T>) identityScope;
} else {
identityScopeLong = null;
}
statements = config.statements;
pkOrdinal = config.pkProperty != null ? config.pkProperty.ordinal : -1;
}
現給解決方法
1 既然有緩存,那麼我們不用緩存就可以了。
在生成DaoSession方法裏面傳入IdentityScopeType.None就行,這樣每次都從數據庫獲取。這樣子所有的獲取數據都會沒有緩存,除非每次使用都重新生成一個DaoSession。
public DaoSession newSession(IdentityScopeType type) {
return new DaoSession(db, type, daoConfigMap);
}
2 使用對象的clone方法。既然我們不想沒保存就修改greendao給我們的緩存對象,那我們就使用clone方法生成一個副本。這樣修改這個對象不影響greendao的緩存對象。
3 利用dao.detachAll方法.可以看到,此方法是清除緩存,這樣我們拿到的是數據庫獲取的數據對象
public void detachAll() {
if (identityScope != null) {
identityScope.clear();
}
}
使用第2種方式,只在獲取的時候調用clone方法,
當然,這個對象要實現cloneable接口
public UserInfoDomain getUserInfoDomain(){
UserInfoDomainDao dao= daoSession.getUserInfoDomainDao();
UserInfoDomain userInfoDomain=dao.queryBuilder().where(UserInfoDomainDao.Properties.UserId.eq(getUserId())).unique();
if (userInfoDomain!=null){
DebugLog.d("獲取的用戶信息:"+userInfoDomain.toString());
return userInfoDomain.clone();
}else{
userInfoDomain=new UserInfoDomain();
return userInfoDomain;
}
}
打印結果。正如所料,並沒有改變greendao緩存對象
使用第三種方法,在獲取前調用detachAll方法
public UserInfoDomain getUserInfoDomain(){
UserInfoDomainDao dao= daoSession.getUserInfoDomainDao();
dao.detachAll();
UserInfoDomain userInfoDomain=dao.queryBuilder().where(UserInfoDomainDao.Properties.UserId.eq(getUserId())).unique();
if (userInfoDomain!=null){
DebugLog.d("獲取的用戶信息:"+userInfoDomain.toString());
return userInfoDomain;
}else{
userInfoDomain=new UserInfoDomain();
return userInfoDomain;
}
}
打印結果也如第二種一樣。