将项目从greenDAO从2.x版本升级到最新的3.2版本,最大变化是可以用注解代替以前的java生成器。实现这点,需要引入相应的gradle插件,具体配置参考官网。
图1是从官网盗来的主结构图,注解Entity后,只需要build工程,DaoMaster、DaoSession和对应的Dao文件就会自动生成。分析greenDAO的实现原理,将会依照这幅图的路线入手,分析各个部分的作用,最重要是研究清楚greenDAO是怎样调用数据库的CRUD。
DaoMaster
DaoMaster是greenDAO的入口,它的父类AbstractDaoMaster维护了数据库重要的参数,分别是实例、版本和Dao的信息。
//AbstractDaoMaster的参数
protected final Database db;
protected final int schemaVersion;
protected final Map<Class<? extends AbstractDao<?, ?>>, DaoConfig> daoConfigMap;
创建DaoMaster需要传入Android原生数据库SQLiteDatabase的实例,接着传递给StandardDatabase:
//DaoMaster的构造函数
public DaoMaster(SQLiteDatabase db) {
this(new StandardDatabase(db));
}
public DaoMaster(Database db) {
super(db, SCHEMA_VERSION);
registerDaoClass(UserDao.class);
}
public class StandardDatabase implements Database {
private final SQLiteDatabase delegate;
public StandardDatabase(SQLiteDatabase delegate) {
this.delegate = delegate;
}
@Override
public void execSQL(String sql) throws SQLException {
delegate.execSQL(sql);
}
//其余省略
}
StandardDatabase实现了Database接口,方法都是SQLiteDatabase提供的,所以SQLite的操作都委托给AbstractDaoMaster的参数db去调用。
protected void registerDaoClass(Class<? extends AbstractDao<?, ?>> daoClass) {
DaoConfig daoConfig = new DaoConfig(db, daoClass);
daoConfigMap.put(daoClass, daoConfig);
}
所有Dao都需要创建DaoConfig,通过AbstractDaoMaster的registerDaoClass注册进daoConfigMap,供后续使用。
数据库升级
DbUpgradeHelper helper = new DbUpgradeHelper(context, dbName, null);
DaoMaster daoMaster = new DaoMaster(helper.getReadableDatabase());
生成数据库可以使用类似上面的语句,通过getReadableDatabase获取数据库实例传递给DaoMaster。DbUpgradeHelper是自定义对象,向上查找父类,可以找到熟悉SQLiteOpenHelper。
DbUpgradeHelper --> DaoMaster.OpenHelper --> DatabaseOpenHelper --> SQLiteOpenHelper
SQLiteOpenHelper提供了onCreate、onUpgrade、onOpen等空方法。继承SQLiteOpenHelper,各层添加了不同的功能:
- DatabaseOpenHelper:使用EncryptedHelper加密数据库;
- DaoMaster.OpenHelper:onCreate时调用createAllTables,继而调用各Dao的createTable;
- DbUpgradeHelper:自定义,一般用来处理数据库升级。
DatabaseOpenHelper和DaoMaster.OpenHelper的代码简单,就不贴了。数据库升级涉及到表结构和表数据的变更,需要判断版本号处理各版本的差异,处理方法可以参考下面的DbUpgradeHelper:
public class DbUpgradeHelper extends DaoMaster.OpenHelper {
public DbUpgradeHelper(Context context, String name, SQLiteDatabase.CursorFactory factory) {
super(context, name, factory);
}
@Override
public void onUpgrade(Database db, int oldVersion, int newVersion) {
if (oldVersion == newVersion) {
LogUtils.d("数据库是最新版本" + oldVersion + ",不需要升级");
return;
}
LogUtils.d("数据库从版本" + oldVersion + "升级到版本" + newVersion);
switch (oldVersion) {
case 1:
String sql = "";
db.execSQL(sql);
case 2:
default:
break;
}
}
}
数据库变更语句的执行,可以利用switch-case没有break时连续执行的特性,实现数据库从任意旧版本升级到新版本。
DaoSession
public DaoSession newSession() {
return new DaoSession(db, IdentityScopeType.Session, daoConfigMap);
}
public DaoSession newSession(IdentityScopeType type) {
return new DaoSession(db, type, daoConfigMap);
}
DaoSession通过调用DaoMaster的newSession创建。对同一个数据库,可以根据需要创建多个Session分别操作。参数IdentityScopeType涉及到是否启用greenDAO的缓存机制,后文会进一步分析。
public DaoSession(Database db, IdentityScopeType type, Map<Class<? extends AbstractDao<?, ?>>, DaoConfig> daoConfigMap) {
super(db);
userDaoConfig = daoConfigMap.get(UserDao.class).clone();
userDaoConfig.initIdentityScope(type);
userDao = new UserDao(userDaoConfig, this);
registerDao(User.class, userDao);
}
创建DaoSession时,将会获取每个Dao的DaoConfig,这是从之前的daoConfigMap中直接clone出来。并且Dao还需要在DaoSession注册,registerDao在父类AbstractDaoSession中的实现:
public class AbstractDaoSession {
private final Database db;
private final Map<Class<?>, AbstractDao<?, ?>> entityToDao;
public AbstractDaoSession(Database db) {
this.db = db;
this.entityToDao = new HashMap<Class<?>, AbstractDao<?, ?>>();
}
protected <T> void registerDao(Class<T> entityClass, AbstractDao<T, ?> dao) {
entityToDao.put(entityClass, dao);
}
/** Convenient call for {@link AbstractDao#insert(Object)}. */
public <T> long insert(T entity) {
@SuppressWarnings("unchecked")
AbstractDao<T, ?> dao = (AbstractDao<T, ?>) getDao(entity.getClass());
return dao.insert(entity);
}
public AbstractDao<?, ?> getDao(Class<? extends Object> entityClass) {
AbstractDao<?, ?> dao = entityToDao.get(entityClass);
if (dao == null) {
throw new DaoException("No DAO registered for " + entityClass);
}
return dao;
}
//其余略
}
registerDao将使用Map维持Class->Dao的关系。AbstractDaoSession提供了insert、update、delete等泛型方法,支持对数据库表的CURD。原理就是从Map获取对应的Dao,再调用Dao对应的操作方法。
Dao
每个Dao都有一个对应的DaoConfig,创建时通过反射机制,为Dao准备好TableName、Property、Pk等一系列具体的参数。所有Dao都继承自AbstractDao,表的通用操作方法就定义在这里。
表的新增和删除
public static void createTable(Database db, boolean ifNotExists) {
String constraint = ifNotExists? "IF NOT EXISTS ": "";
db.execSQL("CREATE TABLE " + constraint + "\"USER\" (" + //
"\"ID\" INTEGER PRIMARY KEY ," + // 0: id
"\"USER_NAME\" TEXT NOT NULL ," + // 1: user_name
"\"REAL_NAME\" TEXT NOT NULL ," + // 2: real_name
"\"EMAIL\" TEXT," + // 3: email
"\"MOBILE\" TEXT," + // 4: mobile
"\"UPDATE_AT\" INTEGER," + // 5: update_at
"\"DELETE_AT\" INTEGER);"); // 6: delete_at
}
public static void dropTable(Database db, boolean ifExists) {
String sql = "DROP TABLE " + (ifExists ? "IF EXISTS " : "") + "\"USER\"";
db.execSQL(sql);
}
简单的先讲,每个Dao里都有表的新增和删除方法,很直接地拼Sql执行,注意传参可以支持判断表是否存在。
SQLiteStatement
下面开始研究greenDAO如何调用SQLite的CRUD,首先要理解什么是ORM。简单来说,SQLite是一个关系数据库,Java用的是对象,对象和关系之间的数据交互需要一个东西去转换,这就是greenDAO的作用。转换过程也不复杂,数据库的列对应Java对象里的参数就行。
SQLiteStatement是封装了对数据库操作和相关数据的对象
SQLiteStatement由Android提供,它的父类SQLiteProgram有两个重要的参数,是执行数据库操作前要提供的:
private final String mSql; //操作数据库用的Sql
private final Object[] mBindArgs; //列和数据值的关系
参数mBindArgs描述了数据库列和数据的关系,SQLiteStatement为不同数据类型提供bind方法,结果保存在mBindArgs,最终交给SQLite处理。
和StandardDatabase一样,SQLiteStatement的方法委托给DatabaseStatement调用,所以greenDAO操作数据库前需要先获取DatabaseStatement。
生成Sql
sql的获取需要用到TableStatements,它的对象维护在DaoConfig里,由它负责创建和缓存DatabaseStatement,下面是insert的DatabaseStatement获取过程:
public DatabaseStatement getInsertStatement() {
if (insertStatement == null) {
String sql = SqlUtils.createSqlInsert("INSERT INTO ", tablename, allColumns);
DatabaseStatement newInsertStatement = db.compileStatement(sql);
synchronized (this) {
if (insertStatement == null) {
insertStatement = newInsertStatement;
}
}
if (insertStatement != newInsertStatement) {
newInsertStatement.close();
}
}
return insertStatement;
}
sql语句通过SqlUtils工具拼接,由Database调用compileStatement将sql存入DatabaseStatement。可知,DatabaseStatement的实现类是StandardDatabaseStatement:
@Override
public DatabaseStatement compileStatement(String sql) {
return new StandardDatabaseStatement(delegate.compileStatement(sql));
}
拼接出来的sql是包括表名和字段名的通用插入语句,生成的DatabaseStatement是可以复用的,所以第一次获取的DatabaseStatement会缓存在insertStatement参数,下次直接使用。
其他例如count、update、delete等操作获取DatabaseStatement原理是一样的,就不介绍了。
执行insert
insert和insertOrReplace都调用了executeInsert,区别之处是入参DatabaseStatement的获取方法不同。
private long executeInsert(T entity, DatabaseStatement stmt, boolean setKeyAndAttach) {
long rowId;
if (db.isDbLockedByCurrentThread()) {
rowId = insertInsideTx(entity, stmt);
} else {
// Do TX to acquire a connection before locking the stmt to avoid deadlocks
db.beginTransaction();
try {
rowId = insertInsideTx(entity, stmt);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
if (setKeyAndAttach) {
updateKeyAfterInsertAndAttach(entity, rowId, true);
}
return rowId;
}
private long insertInsideTx(T entity, DatabaseStatement stmt) {
synchronized (stmt) {
if (isStandardSQLite) {
SQLiteStatement rawStmt = (SQLiteStatement) stmt.getRawStatement();
bindValues(rawStmt, entity);
return rawStmt.executeInsert();
} else {
bindValues(stmt, entity);
return stmt.executeInsert();
}
}
}
当前线程获取数据库锁的情况下,直接执行insert操作即可,否则需要使用事务保证操作的原子性和一致性。insertInsideTx方法里,isStandardSQLite判断当前是不是SQLite数据库(留下扩展的伏笔?)。关键来了,获取原始的SQLiteStatement,调用了bindValues。
@Override
protected final void bindValues(SQLiteStatement stmt, User entity) {
stmt.clearBindings();
Long id = entity.getId();
if (id != null) {
stmt.bindLong(1, id);
}
stmt.bindString(2, entity.getUser_name());
stmt.bindString(3, entity.getReal_name());
}
bindValues由各自的Dao实现,描述index和数据的关系,最终保存进mBindArgs。到这里,应该就能明白greenDao的核心作用。greenDao将我们熟悉的对象,转换成sql语句和执行参数,再提交SQLite执行。
update和delete的操作和insert大同小异,推荐自行分析。
数据Load与缓存机制
userDaoConfig = daoConfigMap.get(UserDao.class).clone();
userDaoConfig.initIdentityScope(type);
创建DaoSession并获取DaoConfig时,调用了initIdentityScope,这里是greenDAO缓存的入口。
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);
}
}
DaoSession的入参IdentityScopeType现在可以解释了,None时不启用缓存,Session时启用缓存。缓存接口IdentityScope根据主键是不是数字,分为两个实现类IdentityScopeLong和IdentityScopeObject。两者的实现类似,选IdentityScopeObject来研究。
private final HashMap<K, Reference<T>> map;
缓存机制很简单,一个保存pk和entity关系的Map,再加上get、put、detach、remove、clear等操作方法。其中get、put方法分无锁版本和加锁版本,对应当前线程是否获得锁的情况。
map.put(key, new WeakReference<T>(entity));
注意,将entity加入Map时使用了弱引用,资源不足时GC会主动回收对象。
下面是load方法,看缓存扮演了什么角色。
public T load(K key) {
assertSinglePk();
if (key == null) {
return null;
}
//1
if (identityScope != null) {
T entity = identityScope.get(key);
if (entity != null) {
return entity;
}
}
//2
String sql = statements.getSelectByKey();
String[] keyArray = new String[]{key.toString()};
Cursor cursor = db.rawQuery(sql, keyArray);
return loadUniqueAndCloseCursor(cursor);
}
在执行真正的数据加载前,标记1处先查找缓存,如果有就直接返回,无就去查数据库。标记2处准备sql语句和参数,交给rawQuery查询,得到Cursor。
用主键查询,只可能有一个结果,调用loadUnique,最终调用loadCurrent。loadCurrent会先尝试从缓存里获取数据,代码很长,分析identityScopeLong != null这段就可以体现原理:
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;
}
}
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);
}
}
}
AbstractDao同时维护identityScope和identityScopeLong对象,entity会同时put进它们两者。如果主键是数字,优先从identityScopeLong获取缓存,速度更快;如果主键不是数字,就尝试从IdentityScopeObject获取;如果没有缓存,只能通过游标读取数据库。
数据Query
QueryBuilder使用链式结构构建Query,灵活地支持where、or、join等约束的添加。具体代码是简单的数据操作,没必要细说,数据最终会拼接成sql。Query的unique操作和上面的load一样,而list操作在调用rawQuery获取Cursor后,最终调用AbstractDao的loadAllFromCursor:
protected List<T> loadAllFromCursor(Cursor cursor) {
int count = cursor.getCount();
if (count == 0) {
return new ArrayList<T>();
}
List<T> list = new ArrayList<T>(count);
//1
CursorWindow window = null;
boolean useFastCursor = false;
if (cursor instanceof CrossProcessCursor) {
window = ((CrossProcessCursor) cursor).getWindow();
if (window != null) { // E.g. Robolectric has no Window at this point
if (window.getNumRows() == count) {
cursor = new FastCursor(window);
useFastCursor = true;
} else {
DaoLog.d("Window vs. result size: " + window.getNumRows() + "/" + count);
}
}
}
//2
if (cursor.moveToFirst()) {
if (identityScope != null) {
identityScope.lock();
identityScope.reserveRoom(count);
}
try {
if (!useFastCursor && window != null && identityScope != null) {
loadAllUnlockOnWindowBounds(cursor, window, list);
} else {
do {
list.add(loadCurrent(cursor, 0, false));
} while (cursor.moveToNext());
}
} finally {
if (identityScope != null) {
identityScope.unlock();
}
}
}
return list;
}
标记1处尝试使用Android提供的CursorWindow以获取一个更快的Cursor。SQLiteDatabase将查询结果保存在CursorWindow所指向的共享内存中,然后通过Binder把这片共享内存传递到查询端。Cursor不是本文要讨论的内容,详情可以参考其他资料。
标记2处通过移动Cursor,利用loadCurrent进行批量操作,结果保存在List中返回。
一对一和一对多
greenDAO支持一对一和一对多,但并不支持多对多。
@ToOne(joinProperty = "father_key")
private CheckItem father;
@Generated
public CheckItem getFather() {
String __key = this.father_key;
if (father__resolvedKey == null || father__resolvedKey != __key) {
__throwIfDetached();
CheckItemDao targetDao = daoSession.getCheckItemDao();
CheckItem fatherNew = targetDao.load(__key);
synchronized (this) {
father = fatherNew;
father__resolvedKey = __key;
}
}
return father;
}
一对一,使用@ToOne标记,greenDAO会自动生成get方法,并标记为@Generated,代表是自动生成的,不要动代码。get方法利用主键load出对应的entity即可。
@ToMany(joinProperties = {
@JoinProperty(name = "key", referencedName = "father_key")
})
private List<CheckItem> children;
一对多的形式和一对一类似,使用@ToMany标记,get方法是利用QueryBuild查询目标List,代码简单就不贴了。
后记
到此,过了一遍greenDAO主要功能,还有些高级特性用到再研究吧。纵观下来,greenDAO还是挺简单的,但也很实用,简化了数据库调用的复杂度,具体的执行就交给原生的Android数据库管理类。
欢迎留言交流,如果有纰漏,请通知我,谢谢。