采坑记之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
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
List
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;
}
}
打印结果也如第二种一样。