GreenDAO 3.2 源碼分析(1):查詢語句與Query實(shí)例的構(gòu)造

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ù)和大家分享,敬請期待。

歡迎大家留言討論與指正。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容