GreenDAO 3.2 源码分析(1):查询语句与Query实例的构造

greenDAO是一款优秀的对象关系映射(ORM)框架,能够提供一个接口通过操作对象的方式去操作关系型数据库,它能够让你操作数据库时更简单、更方便。和复杂麻烦的Android原生数据库API相比较,greenDAO可谓是简单实用,功能强大,不仅性能突出,而且有着丰富文档资料,是当前最为活跃的Android ORM框架。正因为greenDAO框架突出表现,其源码值得深入的研究。

本文的后续内容请见GreenDAO 3.2 源码分析(2):获得查询结果

查询(select)操作是数据库操作中最为重要的一个部分,本文将针对“GreenDAO中查询结果是如何生成”为线索,探究greenDAO的源码。

greenDAO框架中,针对每一个注解为@Entity的类,都会自动生成生成一个DAO(Database Access Object,数据访问接口)类,此类继承自抽象类AbstractDao<T, K>,包含方法queryBuilder(),来辅助生成Query类,以获得查询结果。
比如,对于一个Student类,greenDAO生成StudentDAO类,如果要获得数据库中所有student的信息,可以如下操作:

StudentDao.queryBuilder().list();

该函数将返回一个List<Student>,即数据库Student表中全部的数据。
下面我看看一下queryBuilder()的代码:

public QueryBuilder<T> queryBuilder() {
        return QueryBuilder.internalCreate(this);
    }

原来是生成一个QueryBuilder<T>对象,该对象是通过QueryBuilder.internalCreate(AbstractDao<T2, ?> dao)方法生成的,需要注意的是this指的是StudentDAO类,其继承自AbstractDao<T, K>。

QueryBuilder.internalCreate方法实际上是调用了QueryBuilder的构造器。

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";
    }

让继续看看QueryBuilder对象的list()方法:

public List<T> list() {
        return build().list();
    }

这个方法其实分为两部分:

  • 首先调用build方构建SQL以及Query实例;
  • 然后调用list方法得到查询救过

下面我们就分为这两个部分来讲解,本文的重点在第一部分,第二部分将在后续的文章中为大家介绍。

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();
        //获得返回结果的限制数,即最多返回几行数据
        int limitPosition = checkAddLimit(builder);
        //获得开始查询的位移行数,即从第几行开始查询
        int offsetPosition = checkAddOffset(builder);

        String sql = builder.toString();
        //检查是否输出所生成的sql语句,以及对应的参数
        checkLog(sql);
        //创建Query对象,通过该对象来正真地查询数据
        return Query.create(dao, sql, values.toArray(), limitPosition, offsetPosition);
    }

bulid()方法中做了好多事情,简而言之就是在生成了查询语句,并以此返回Query对象:

  • 生成包含select语句的StringBuilder;
  • 为select语句添加LIMIT部分和OFFSET部分;
  • 以String形式产生完整的select sql语句,并根据设置在log中输出sql语句;
  • 以线程为单位生成Query类实例。

咱们一一来看。

1.1 createSelectBuilder()

首先createSelectBuilder()方法用来生成select语句,并将生成的select语句保存在StringBuilder中:

private StringBuilder createSelectBuilder() {
        //构造select语句主题部分,包括从那个表中查询哪些字段
        String select = SqlUtils.createSqlSelect(dao.getTablename(), tablePrefix, dao.getAllColumns(), distinct);
        StringBuilder builder = new StringBuilder(select);
        //向select语句中添加联合查询以及条件查询部分
        appendJoinsAndWheres(builder, tablePrefix);
       //向select语句中添加order部分
        if (orderBuilder != null && orderBuilder.length() > 0) {
            builder.append(" ORDER BY ").append(orderBuilder);
        }
        return builder;
}

如果读者对于SQL语句很熟悉,应该已经想到* SqlUtils.createSqlSelect*方法在做些什么了,就是根据要查询的字段生成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");
        }
        // 根据distinct的值,来判断是否要在select语句中加入“ DISTINCT”关键字
        StringBuilder builder = new StringBuilder(distinct ? "SELECT DISTINCT " : "SELECT ");
        // 添加要查询的列,以及关键字“FROM”
        SqlUtils.appendColumns(builder, tableAlias, columns).append(" FROM ");
        // 添加表名和表的别名
        builder.append('"').append(tablename).append('"').append(' ').append(tableAlias).append(' ');
        return builder.toString();
}

createSqlSelect方法返回的sql语句是select语句,相当于是产生“select * from tableName ”,其本质就是通过StringBuilder构造String对象。这里需要解释的是为什么表的别名* tableAlias*是必须的,这是为了在处理多表联合查询时方便处理,多表的联合查询中往往需要给表取别名以方便构建SQL,因此在此处要求必须有别名,所以对于单表查询没有意义,但是在联合查询中却很有帮助,这里也体现了greenDAO设计团队的良苦用心。

下面我们来看下* appendJoinsAndWheres*方法,顾名思义这个方法是为selelct语句添加联合查询和where语句部分:

  private void appendJoinsAndWheres(StringBuilder builder, String tablePrefixOrNull) {
        //清空sql语句参数
        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);
        }
        //根据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语句的生成,相当于是一种编译器,各种条件参数转化为标注的SQL语句,不难理解,就不在赘言了。如果理解有困难,建议去参看关于SQL查询的文章。

1.2 LIMIT & OFFSET

首先要说明下LIMIT & OFFSET的意义,这部分在SQL语句中不是那么常见。假设数据库表student存在13条数据。

语句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表示查询结果返回的行数的限制,OFFSET表示开始从第几行开始查询。注意OFFSET关键字必须和LIMIT联合使用,不能单独使用。

checkAddLimit和* checkAddOffset*方法的代码相似,就放在一起分析:

    private int checkAddLimit(StringBuilder builder) {
        int limitPosition = -1; //limitPosition表示 LIMIT的值在List<Object> values中的位置
        if (limit != null) { //如果QueryBuilder中设置了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, 是不能设置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;
    }

代码很直观并不复杂,需要注意的有两点:

  • 为什么前面提到的中的createSelectBuilder方法要返回StringBuilder?这是因为后面可能还要继续构建select语句添加LIMIT&OFFSET部分,如果返回String类型再对其修改,效率较低;
  • 为什么要把limit和offset的值都写入到valuses中,而不是直接写入sql语句?因为在Query实例向数据库查询数据时还需要用到limit和offset的值,那时再从sql语句中提取出反而麻烦。

1.3 显示SQL语句和查询参数

因为greenDAO是自动生成数据库和查询语句,用户是不直接操控数据库的,所以一旦遇到什么问题不是很容易定位。如果在调试的过程中想要观察产生的sql语句是否和预期一致,可以是设置参数* QueryBuilder.LOG_SQL QueryBuilder.LOG_VALUES*

QueryBuilder.LOG_SQL = true;
QueryBuilder.LOG_VALUES = true;

checkLog方法会根据这两个值来判断是否要在日志中输出所生成的SQL和查询参数

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语句,下面就是要执行SQL操作并得到结果集。Query类就是负责具体执行SQL语句的实体对象。下面我们来重点分析Query类的源码。

    static <T2> Query<T2> create(AbstractDao<T2, ?> dao, String sql, Object[] initialValues, int limitPosition, int offsetPosition) {
        //QueryData类,是Query类的静态内部类,用来保存查询的数据
        QueryData<T2> queryData = new QueryData<T2>(dao, sql, toStringArray(initialValues), limitPosition,
                offsetPosition);
        // 获得为当前线程分配的Query对象,也就是为每个线程分配单独的Query实例
        return queryData.forCurrentThread();
    }

Query.create方法是工厂模式,以Query的静态方法返回Query类对象。值得注意的是,create方法没有直接去创建Query对象,而是先创建QueryData对象,再通过QueryData对象创建Query对象,如此“大费周章”其实大有深意,原因在于为了实现Query对象的复用与进程独立,这也是greenDAO设计的精巧之处,让我们快来一探究竟吧。

    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() {
            //调用Query的构造函数时,将自身也传入其中,将QueryData和Query联系在一起
            return new Query<T2>(this, dao, sql, initialValues.clone(), limitPosition, offsetPosition);
        }

    }

QueryData类继承自抽象类AbstractQueryData,其中Q createQuery()方法为抽象方法,要求QueryData必须实现,从上面的代码中可以看出,QueryData在实现createQuery()中调用了Query的构造函数,其参数中也包括QueryData对象本身(this),Query对象将会保存这个QueryData对象到自己的queryData域中,这样就将二者绑定了起来。

createQuery方法将会在forCurrentThread中被调用,该方法用来获得为当前线程所分配的Query对象。

在greenDAO的多线程查询机制中,会为每一个查询线程都分配一个单独的Query对象,在一个线程中如果使用和该线程不匹配的Query对象去查询,将会报错。这种设计机制避免引入上锁解锁,不光提高了效率,还让代码会更为简洁。

接下来就到了本文的重点* 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.
        // 获得当前进程号
        long threadId = Thread.currentThread().getId();
        //queriesForThreads是保存进程ID和Query对象之间对应关系的Map
        synchronized (queriesForThreads) {
            //尝试获取当前进程号所对应的Query对象
            WeakReference<Q> queryRef = queriesForThreads.get(threadId);
            //如果进程号有对应的Query对象,则将其赋值给query,否者赋为空引用
            Q query = queryRef != null ? queryRef.get() : null;
            //如果query为空引用
            if (query == null) {
                //垃圾回收
                gc();
                //创建新的Query对象
                query = createQuery();
                //将线程号和query对象保存到queriesForThreads中
                queriesForThreads.put(threadId, new WeakReference<Q>(query));
            } else {//如果query不为空
                //initialValues中的查询参数直接拷贝到query.parameters
                System.arraycopy(initialValues, 0, query.parameters, 0, initialValues.length);
            }
            return query;
        }
    }

forCurrentThread工作流程如下:

  • 依据当前ThreadID, 尝试从Map queriesForThreads取出对应的Query对象;
  • 如果能获得Query对象,则将查询参数拷贝给该Query对象
  • 如果不能获得Query对象,则说明目前还没有为该线程分配Query对象,需要新建Query实例,并将该实例和线程号保存到queriesForThreads中;
  • 返回Query对象

正如上文所说,QueryData中维护着线程号和Query对象的对应表

final Map<Long, WeakReference<Q>> queriesForThreads;

新线程的将会分配新的Query对象,旧的线程将会取回原来为它分配Query对象,从而保证各个进程所操作的Query对象是独立的,避免多线程的冲突。在给新线程分配新的Query对象时,采用了对queriesForThreads的同步操作,避免多线程冲突。

这里要特别说明下queriesForThreads的类型是**Map<Long, WeakReference<Q>> **,Map中的value对应的类型是Query对象的弱引用,这样是为了防止内存泄露。如果不用弱引用而是直接引用,会发生如下问题:

  • QueryData实例QD中的Map对象queriesForThreads包含着一系列的Query对象;
  • Query类中存在一个域保存QueryData变量,故而queriesForThreads中的Query对象的queryData域都会是QD的引用;
  • Query对象和QueryData对象将会相互引用;
  • Java GC机制将永远不会回收Query对象,即使它已经执行完毕,不再使用。

WeakReference的使用就是保证queriesForThreads中对Query的引用不会影响垃圾回收,破除相互引用带来的内存泄露。当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();
                //如果发现是query对象为null
                if (entry.getValue().get() == null) {
                    iterator.remove();//删除该键值对
                }
            }
        }
    }

总结

上文已经介绍太多内容了,是时候进行下总结:

  • QueryBuilder用建造者模式(Builder Pattern)帮助构建查询所用的SQL语句,并以此来生成Query对象;
  • 但是QueryBuilder并不是直接生成Query实例,而是通过Query类的内部静态类QueryData生成Query对象,内部静态类的优势在于不用依靠外部类实例就能单独使用,同时又可以使用外部类的静态生产成员;
  • 为了解决多线程查询的问题,greenDAO并不是为查询表上锁,而是通过QueryData对象为每个线程都创建独立的Query对象;
  • 线程和Query对象的对应关系,被以键值对的形式保存在QueryData对象内部,同一个线程中,Query对象将被复用;
  • 为了避免由于相互引用而带来的内存泄漏,Map<Long, WeakReference<Q>> queriesForThreads中以弱引用的形式引用Query对象。

当得到Query对象之后,下一步就应该是向数据库查询数据,得到游标返回结果集,但是由于篇幅的关系,这部分内容将在下一篇文章GreenDAO 3.2 源码分析(2):获得查询结果中继续和大家分享,敬请期待。

欢迎大家留言讨论与指正。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,012评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,628评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,653评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,485评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,574评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,590评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,596评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,340评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,794评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,102评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,276评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,940评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,583评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,201评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,441评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,173评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,136评论 2 352

推荐阅读更多精彩内容