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

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

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):获得查询结果中继续和大家分享,敬请期待。

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

相关文章
|
3月前
|
SQL
访问者模式问题之构造一个包含 select、from 和 where 子句的 SQL 节点树,如何解决
访问者模式问题之构造一个包含 select、from 和 where 子句的 SQL 节点树,如何解决
|
6月前
|
SQL 关系型数据库 数据库连接
Hasor【环境搭建 03】Dataway接口配置服务使用DataQL聚合查询引擎(SQL执行器实现分页查询举例说明+报错 Query dialect missing 原因分析及解决)
Hasor【环境搭建 03】Dataway接口配置服务使用DataQL聚合查询引擎(SQL执行器实现分页查询举例说明+报错 Query dialect missing 原因分析及解决)
142 0
|
6月前
|
SQL XML Java
MyBatis Plus通用CRUD与条件构造器使用及SQL自动注入原理分析
MyBatis Plus通用CRUD与条件构造器使用及SQL自动注入原理分析
267 0
|
6月前
|
SQL Java 数据库连接
Hibernate - QBC和本地SQL对象检索详解
Hibernate - QBC和本地SQL对象检索详解
59 0
|
6月前
|
SQL Java 数据库
JPQL语言和Query接口简解
JPQL语言和Query接口简解
46 0
|
Java 数据库连接 数据库
使用JDBC(Dbutils工具包)来从数据库拿取map类型数据来动态生成insert语句
前言: 大家在使用JDBC来连接数据库时,我们通过Dbutils工具来拿取数据库中的数据,可以使用new BeanListHandler<>(所映射的实体类.class),这样得到的数据,不知道表的字段名字,我们在往数据库里添加时,需要自己来挨个写字段,非常麻烦! 于是,小编想到通过MapListHandler(),结果集为一个List<Map<String, Object>>,map中key为数据库字段名字,value为对应的值,这样就可以实现insert语句动态拼接了!!
216 3
|
SQL Java 数据库连接
hibernate自定义sql关联查询结果组装为对象
hibernate自定义sql关联查询后没有对应的entity,如何映射为对应的bean
246 0
|
SQL Java 数据库连接
Hibernate中执行NativeSQL语句查询返回自定义类型的POJO实例的List(多表查询)
Hibernate中定义了hql的概念,简单地说就是,为java的码农提供了一套类似于sql的语法,但是数据表名变成了PO名,数据字段名变成了PO中属性成员名,并把这种语法称为hql。优点就是:hql看上去是面向对象的,码农不需要知道数据库中数据表的结构,只需要依据PO编写面向对象的数据库增删改查的语句。
4162 0