greenDAO是一款優(yōu)秀的對象關(guān)系映射(ORM)框架,能夠提供一個接口通過操作對象的方式去操作關(guān)系型數(shù)據(jù)庫,它能夠讓你操作數(shù)據(jù)庫時更簡單、更方便。和復(fù)雜麻煩的Android原生數(shù)據(jù)庫API相比較,greenDAO可謂是簡單實(shí)用,功能強(qiáng)大,不僅性能突出,而且有著豐富文檔資料,是當(dāng)前最為活躍的Android ORM框架。正因?yàn)間reenDAO框架突出表現(xiàn),其源碼值得深入的研究。
本文的后續(xù)內(nèi)容請見GreenDAO 3.2 源碼分析(2):獲得查詢結(jié)果
查詢(select)操作是數(shù)據(jù)庫操作中最為重要的一個部分,本文將針對“GreenDAO中查詢結(jié)果是如何生成”為線索,探究greenDAO的源碼。
greenDAO框架中,針對每一個注解為@Entity的類,都會自動生成生成一個DAO(Database Access Object,數(shù)據(jù)訪問接口)類,此類繼承自抽象類AbstractDao<T, K>,包含方法queryBuilder(),來輔助生成Query類,以獲得查詢結(jié)果。
比如,對于一個Student類,greenDAO生成StudentDAO類,如果要獲得數(shù)據(jù)庫中所有student的信息,可以如下操作:
StudentDao.queryBuilder().list();
該函數(shù)將返回一個List<Student>,即數(shù)據(jù)庫Student表中全部的數(shù)據(jù)。
下面我看看一下queryBuilder()的代碼:
public QueryBuilder<T> queryBuilder() {
return QueryBuilder.internalCreate(this);
}
原來是生成一個QueryBuilder<T>對象,該對象是通過QueryBuilder.internalCreate(AbstractDao<T2, ?> dao)方法生成的,需要注意的是this指的是StudentDAO類,其繼承自AbstractDao<T, K>。
QueryBuilder.internalCreate方法實(shí)際上是調(diào)用了QueryBuilder的構(gòu)造器。
protected QueryBuilder(AbstractDao<T, ?> dao) {
this(dao, "T");
}
protected QueryBuilder(AbstractDao<T, ?> dao, String tablePrefix) {
this.dao = dao;
this.tablePrefix = tablePrefix;
values = new ArrayList<Object>();
joins = new ArrayList<Join<T, ?>>();
whereCollector = new WhereCollector<T>(dao, tablePrefix);
stringOrderCollation = " COLLATE NOCASE";
}
讓繼續(xù)看看QueryBuilder對象的list()方法:
public List<T> list() {
return build().list();
}
這個方法其實(shí)分為兩部分:
- 首先調(diào)用build方構(gòu)建SQL以及Query實(shí)例;
- 然后調(diào)用list方法得到查詢救過
下面我們就分為這兩個部分來講解,本文的重點(diǎn)在第一部分,第二部分將在后續(xù)的文章中為大家介紹。
1. 如何生成select語句
/**
* Builds a reusable query object
* Query objects can be executed more efficiently
* than creating a QueryBuilder for each execution.
*/
public Query<T> build() {
//生成查詢語句
StringBuilder builder = createSelectBuilder();
//獲得返回結(jié)果的限制數(shù),即最多返回幾行數(shù)據(jù)
int limitPosition = checkAddLimit(builder);
//獲得開始查詢的位移行數(shù),即從第幾行開始查詢
int offsetPosition = checkAddOffset(builder);
String sql = builder.toString();
//檢查是否輸出所生成的sql語句,以及對應(yīng)的參數(shù)
checkLog(sql);
//創(chuàng)建Query對象,通過該對象來正真地查詢數(shù)據(jù)
return Query.create(dao, sql, values.toArray(), limitPosition, offsetPosition);
}
bulid()方法中做了好多事情,簡而言之就是在生成了查詢語句,并以此返回Query對象:
- 生成包含select語句的StringBuilder;
- 為select語句添加LIMIT部分和OFFSET部分;
- 以String形式產(chǎn)生完整的select sql語句,并根據(jù)設(shè)置在log中輸出sql語句;
- 以線程為單位生成Query類實(shí)例。
咱們一一來看。
1.1 createSelectBuilder()
首先createSelectBuilder()方法用來生成select語句,并將生成的select語句保存在StringBuilder中:
private StringBuilder createSelectBuilder() {
//構(gòu)造select語句主題部分,包括從那個表中查詢哪些字段
String select = SqlUtils.createSqlSelect(dao.getTablename(), tablePrefix, dao.getAllColumns(), distinct);
StringBuilder builder = new StringBuilder(select);
//向select語句中添加聯(lián)合查詢以及條件查詢部分
appendJoinsAndWheres(builder, tablePrefix);
//向select語句中添加order部分
if (orderBuilder != null && orderBuilder.length() > 0) {
builder.append(" ORDER BY ").append(orderBuilder);
}
return builder;
}
如果讀者對于SQL語句很熟悉,應(yīng)該已經(jīng)想到* SqlUtils.createSqlSelect*方法在做些什么了,就是根據(jù)要查詢的字段生成sql語句,以下是源碼:
/** Creates an select for given columns with a trailing space */
public static String createSqlSelect(String tablename, String tableAlias, String[] columns, boolean distinct) {
//判斷表的別名是否存在,如果表別名無效,則拋出異常
if (tableAlias == null || tableAlias.length() < 0) {
throw new DaoException("Table alias required");
}
// 根據(jù)distinct的值,來判斷是否要在select語句中加入“ DISTINCT”關(guān)鍵字
StringBuilder builder = new StringBuilder(distinct ? "SELECT DISTINCT " : "SELECT ");
// 添加要查詢的列,以及關(guān)鍵字“FROM”
SqlUtils.appendColumns(builder, tableAlias, columns).append(" FROM ");
// 添加表名和表的別名
builder.append('"').append(tablename).append('"').append(' ').append(tableAlias).append(' ');
return builder.toString();
}
createSqlSelect方法返回的sql語句是select語句,相當(dāng)于是產(chǎn)生“select * from tableName ”,其本質(zhì)就是通過StringBuilder構(gòu)造String對象。這里需要解釋的是為什么表的別名* tableAlias*是必須的,這是為了在處理多表聯(lián)合查詢時方便處理,多表的聯(lián)合查詢中往往需要給表取別名以方便構(gòu)建SQL,因此在此處要求必須有別名,所以對于單表查詢沒有意義,但是在聯(lián)合查詢中卻很有幫助,這里也體現(xiàn)了greenDAO設(shè)計團(tuán)隊的良苦用心。
下面我們來看下* appendJoinsAndWheres*方法,顧名思義這個方法是為selelct語句添加聯(lián)合查詢和where語句部分:
private void appendJoinsAndWheres(StringBuilder builder, String tablePrefixOrNull) {
//清空sql語句參數(shù)
values.clear();
//添加表連接部分
for (Join<T, ?> join : joins) {
builder.append(" JOIN ").append(join.daoDestination.getTablename()).append(' ');
builder.append(join.tablePrefix).append(" ON ");
SqlUtils.appendProperty(builder, join.sourceTablePrefix, join.joinPropertySource).append('=');
SqlUtils.appendProperty(builder, join.tablePrefix, join.joinPropertyDestination);
}
//根據(jù)whereAppended的值,添加where條件
boolean whereAppended = !whereCollector.isEmpty();
if (whereAppended) {
builder.append(" WHERE ");
whereCollector.appendWhereClause(builder, tablePrefixOrNull, values);
}
// 添加連接條件
for (Join<T, ?> join : joins) {
if (!join.whereCollector.isEmpty()) {
if (!whereAppended) {
builder.append(" WHERE ");
whereAppended = true;
} else {
builder.append(" AND ");
}
join.whereCollector.appendWhereClause(builder, join.tablePrefix, values);
}
}
}
這里都是純粹的SQL語句的生成,相當(dāng)于是一種編譯器,各種條件參數(shù)轉(zhuǎn)化為標(biāo)注的SQL語句,不難理解,就不在贅言了。如果理解有困難,建議去參看關(guān)于SQL查詢的文章。
1.2 LIMIT & OFFSET
首先要說明下LIMIT & OFFSET的意義,這部分在SQL語句中不是那么常見。假設(shè)數(shù)據(jù)庫表student存在13條數(shù)據(jù)。
語句1:select * from student limit 9,4
語句2:slect * from student limit 4 offset 9
語句1和2均返回表student的第10、11、12、13行,語句2中的4表示返回4行,9表示從表的第十行開始。也就是說 LIMIT表示查詢結(jié)果返回的行數(shù)的限制,OFFSET表示開始從第幾行開始查詢。注意OFFSET關(guān)鍵字必須和LIMIT聯(lián)合使用,不能單獨(dú)使用。
checkAddLimit和* checkAddOffset*方法的代碼相似,就放在一起分析:
private int checkAddLimit(StringBuilder builder) {
int limitPosition = -1; //limitPosition表示 LIMIT的值在List<Object> values中的位置
if (limit != null) { //如果QueryBuilder中設(shè)置了limit的值
//為select語句添加 LIMIT部分
builder.append(" LIMIT ?");
// 將limit的值保存在values中
values.add(limit);
//記錄下limit值values中的位置
limitPosition = values.size() - 1
}
return limitPosition;
}
private int checkAddOffset(StringBuilder builder) {
//offsetPosition表示OFFSET的值在List<Object> values中的位置
int offsetPosition = -1;
if (offset != null) {
if (limit == null) { //如果沒有這事limit, 是不能設(shè)置offset
throw new IllegalStateException("Offset cannot be set without limit");
}
//為select語句添加 OFFSET部分
builder.append(" OFFSET ?");
//將offset的值保存在values中
values.add(offset);
//記錄下offset值values中的位置
offsetPosition = values.size() - 1;
}
return offsetPosition;
}
代碼很直觀并不復(fù)雜,需要注意的有兩點(diǎn):
- 為什么前面提到的中的createSelectBuilder方法要返回StringBuilder?這是因?yàn)楹竺婵赡苓€要繼續(xù)構(gòu)建select語句添加LIMIT&OFFSET部分,如果返回String類型再對其修改,效率較低;
- 為什么要把limit和offset的值都寫入到valuses中,而不是直接寫入sql語句?因?yàn)樵赒uery實(shí)例向數(shù)據(jù)庫查詢數(shù)據(jù)時還需要用到limit和offset的值,那時再從sql語句中提取出反而麻煩。
1.3 顯示SQL語句和查詢參數(shù)
因?yàn)間reenDAO是自動生成數(shù)據(jù)庫和查詢語句,用戶是不直接操控數(shù)據(jù)庫的,所以一旦遇到什么問題不是很容易定位。如果在調(diào)試的過程中想要觀察產(chǎn)生的sql語句是否和預(yù)期一致,可以是設(shè)置參數(shù)* QueryBuilder.LOG_SQL和 QueryBuilder.LOG_VALUES*
QueryBuilder.LOG_SQL = true;
QueryBuilder.LOG_VALUES = true;
checkLog方法會根據(jù)這兩個值來判斷是否要在日志中輸出所生成的SQL和查詢參數(shù)
private void checkLog(String sql) {
if (LOG_SQL) {
DaoLog.d("Built SQL for query: " + sql);
}
if (LOG_VALUES) {
DaoLog.d("Values for query: " + values);
}
}
1.4 生成Query類對象
有了要查詢的SQL語句,下面就是要執(zhí)行SQL操作并得到結(jié)果集。Query類就是負(fù)責(zé)具體執(zhí)行SQL語句的實(shí)體對象。下面我們來重點(diǎn)分析Query類的源碼。
static <T2> Query<T2> create(AbstractDao<T2, ?> dao, String sql, Object[] initialValues, int limitPosition, int offsetPosition) {
//QueryData類,是Query類的靜態(tài)內(nèi)部類,用來保存查詢的數(shù)據(jù)
QueryData<T2> queryData = new QueryData<T2>(dao, sql, toStringArray(initialValues), limitPosition,
offsetPosition);
// 獲得為當(dāng)前線程分配的Query對象,也就是為每個線程分配單獨(dú)的Query實(shí)例
return queryData.forCurrentThread();
}
Query.create方法是工廠模式,以Query的靜態(tài)方法返回Query類對象。值得注意的是,create方法沒有直接去創(chuàng)建Query對象,而是先創(chuàng)建QueryData對象,再通過QueryData對象創(chuàng)建Query對象,如此“大費(fèi)周章”其實(shí)大有深意,原因在于為了實(shí)現(xiàn)Query對象的復(fù)用與進(jìn)程獨(dú)立,這也是greenDAO設(shè)計的精巧之處,讓我們快來一探究竟吧。
private final static class QueryData<T2> extends AbstractQueryData<T2, Query<T2>> {
private final int limitPosition;
private final int offsetPosition;
QueryData(AbstractDao<T2, ?> dao, String sql, String[] initialValues, int limitPosition, int offsetPosition) {
super(dao, sql, initialValues);
this.limitPosition = limitPosition;
this.offsetPosition = offsetPosition;
}
@Override
protected Query<T2> createQuery() {
//調(diào)用Query的構(gòu)造函數(shù)時,將自身也傳入其中,將QueryData和Query聯(lián)系在一起
return new Query<T2>(this, dao, sql, initialValues.clone(), limitPosition, offsetPosition);
}
}
QueryData類繼承自抽象類AbstractQueryData,其中Q createQuery()方法為抽象方法,要求QueryData必須實(shí)現(xiàn),從上面的代碼中可以看出,QueryData在實(shí)現(xiàn)createQuery()中調(diào)用了Query的構(gòu)造函數(shù),其參數(shù)中也包括QueryData對象本身(this),Query對象將會保存這個QueryData對象到自己的queryData域中,這樣就將二者綁定了起來。
createQuery方法將會在forCurrentThread中被調(diào)用,該方法用來獲得為當(dāng)前線程所分配的Query對象。
在greenDAO的多線程查詢機(jī)制中,會為每一個查詢線程都分配一個單獨(dú)的Query對象,在一個線程中如果使用和該線程不匹配的Query對象去查詢,將會報錯。這種設(shè)計機(jī)制避免引入上鎖解鎖,不光提高了效率,還讓代碼會更為簡潔。
接下來就到了本文的重點(diǎn)* forCurrentThread*方法的代碼分析:
/**
* Note: all parameters are reset to their initial values specified in {@link QueryBuilder}.
*/
Q forCurrentThread() {
// Process.myTid() seems to have issues on some devices (see Github #376) and Robolectric (#171):
// We use currentThread().getId() instead (unfortunately return a long, can not use SparseArray).
// PS.: thread ID may be reused, which should be fine because old thread will be gone anyway.
// 獲得當(dāng)前進(jìn)程號
long threadId = Thread.currentThread().getId();
//queriesForThreads是保存進(jìn)程ID和Query對象之間對應(yīng)關(guān)系的Map
synchronized (queriesForThreads) {
//嘗試獲取當(dāng)前進(jìn)程號所對應(yīng)的Query對象
WeakReference<Q> queryRef = queriesForThreads.get(threadId);
//如果進(jìn)程號有對應(yīng)的Query對象,則將其賦值給query,否者賦為空引用
Q query = queryRef != null ? queryRef.get() : null;
//如果query為空引用
if (query == null) {
//垃圾回收
gc();
//創(chuàng)建新的Query對象
query = createQuery();
//將線程號和query對象保存到queriesForThreads中
queriesForThreads.put(threadId, new WeakReference<Q>(query));
} else {//如果query不為空
//initialValues中的查詢參數(shù)直接拷貝到query.parameters
System.arraycopy(initialValues, 0, query.parameters, 0, initialValues.length);
}
return query;
}
}
forCurrentThread工作流程如下:
- 依據(jù)當(dāng)前ThreadID, 嘗試從Map queriesForThreads取出對應(yīng)的Query對象;
- 如果能獲得Query對象,則將查詢參數(shù)拷貝給該Query對象
- 如果不能獲得Query對象,則說明目前還沒有為該線程分配Query對象,需要新建Query實(shí)例,并將該實(shí)例和線程號保存到queriesForThreads中;
- 返回Query對象
正如上文所說,QueryData中維護(hù)著線程號和Query對象的對應(yīng)表
final Map<Long, WeakReference<Q>> queriesForThreads;
新線程的將會分配新的Query對象,舊的線程將會取回原來為它分配Query對象,從而保證各個進(jìn)程所操作的Query對象是獨(dú)立的,避免多線程的沖突。在給新線程分配新的Query對象時,采用了對queriesForThreads的同步操作,避免多線程沖突。
這里要特別說明下queriesForThreads的類型是**Map<Long, WeakReference<Q>> **,Map中的value對應(yīng)的類型是Query對象的弱引用,這樣是為了防止內(nèi)存泄露。如果不用弱引用而是直接引用,會發(fā)生如下問題:
- QueryData實(shí)例QD中的Map對象queriesForThreads包含著一系列的Query對象;
- Query類中存在一個域保存QueryData變量,故而queriesForThreads中的Query對象的queryData域都會是QD的引用;
- Query對象和QueryData對象將會相互引用;
- Java GC機(jī)制將永遠(yuǎn)不會回收Query對象,即使它已經(jīng)執(zhí)行完畢,不再使用。
WeakReference的使用就是保證queriesForThreads中對Query的引用不會影響垃圾回收,破除相互引用帶來的內(nèi)存泄露。當(dāng)Query對象被回收之后,QueryData類可以通過gc方法回收queriesForThreads中無用的鍵值對:
void gc() {
synchronized (queriesForThreads) {
//獲得迭代器
Iterator<Entry<Long, WeakReference<Q>>> iterator = queriesForThreads.entrySet().iterator();
while (iterator.hasNext()) {
Entry<Long, WeakReference<Q>> entry = iterator.next();
//如果發(fā)現(xiàn)是query對象為null
if (entry.getValue().get() == null) {
iterator.remove();//刪除該鍵值對
}
}
}
}
總結(jié)
上文已經(jīng)介紹太多內(nèi)容了,是時候進(jìn)行下總結(jié):
- QueryBuilder用建造者模式(Builder Pattern)幫助構(gòu)建查詢所用的SQL語句,并以此來生成Query對象;
- 但是QueryBuilder并不是直接生成Query實(shí)例,而是通過Query類的內(nèi)部靜態(tài)類QueryData生成Query對象,內(nèi)部靜態(tài)類的優(yōu)勢在于不用依靠外部類實(shí)例就能單獨(dú)使用,同時又可以使用外部類的靜態(tài)生產(chǎn)成員;
- 為了解決多線程查詢的問題,greenDAO并不是為查詢表上鎖,而是通過QueryData對象為每個線程都創(chuàng)建獨(dú)立的Query對象;
- 線程和Query對象的對應(yīng)關(guān)系,被以鍵值對的形式保存在QueryData對象內(nèi)部,同一個線程中,Query對象將被復(fù)用;
- 為了避免由于相互引用而帶來的內(nèi)存泄漏,Map<Long, WeakReference<Q>> queriesForThreads中以弱引用的形式引用Query對象。
當(dāng)?shù)玫絈uery對象之后,下一步就應(yīng)該是向數(shù)據(jù)庫查詢數(shù)據(jù),得到游標(biāo)返回結(jié)果集,但是由于篇幅的關(guān)系,這部分內(nèi)容將在下一篇文章GreenDAO 3.2 源碼分析(2):獲得查詢結(jié)果中繼續(xù)和大家分享,敬請期待。
歡迎大家留言討論與指正。