將項目從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數據庫管理類。
歡迎留言交流,如果有紕漏,請通知我,謝謝。