GreenDAO 3.2 源码分析(2):获得查询结果

简介: 在前文GreenDAO 3.2 源码分析(1):查询语句与Query实例的构造中,我们分析了greenDAO是如何构建SQL语句并维护Query对象的。

在前文GreenDAO 3.2 源码分析(1):查询语句与Query实例的构造中,我们分析了greenDAO是如何构建SQL语句并维护Query对象的。为了提高在多线程的操作中的效率,greenDAO并没有采用上锁的机制,而是对每一个线程都单独分配一个Query对象来执行查询操作,那Query对象是获得查询结果的呢?让我们在本文中一探究竟吧。

2. 1查询的种类

Query类中一共定义了四种获得查询结果的方法:

  • list() : 将全部结果都放在内存中,以list形式返回;
  • listLazy() : 只用真正使用到数据时, 才回去访问数据库获得数据。注意需要主动关闭游标;
  • listLazyUncached(): 和listLazy()类似,但是没有使用cache,所以数据不能复用,每次都要去数据库中读取;
  • listIterator(): 以List迭代体的形式返回结果,当迭代体遍历会后,游标关闭。

看到这么多方法供选择,是不是满脑子都是问号:每种方法在实现上又有什么不同呢?在设计上有什么考虑呢?应该在哪种场景中使用呢?有问题就有意义,让我们从每个方法的源码上开始分析吧。

2.2 list()

简单粗暴,先来源码。

/** Executes the query 
 * and returns the result as a list 
 * containing all entities loaded into memory. */
public List<T> list() {
    //1. 检查当前线程和Query是否匹配;
    checkThread();
    //2. 获得游标;
    Cursor cursor = dao.getDatabase().rawQuery(sql, parameters);
    //3. 通过该游标将所有的数据都遍历读取存入内存中
    return daoAccess.loadAllAndCloseCursor(cursor);
}

正如源码中注释所说的,list()是将所有的查询结果都读入内存中,然后以List的形式返回查询结果,List中的每一个元素都是一行查询结果。代码的逻辑也很清晰,一共三行:

  • 首先检查当前线程和当前的Query实例是否匹配前文中已经提过了,greenDAO框架中,执行查询之前,会为不同线程分配不同Query, 以避免多线程查询的冲突,所以在创建Query实例时,Query实例就会把创建它的线程对象保存起来。当要执行查询时,首先要判断当前调用它的线程是否就是创建它的线程,如果不是,就会抛出异常,其代码如下:
    protected void checkThread() {
        if (Thread.currentThread() != ownerThread) {
            throw new DaoException(
                    "Method may be called only in owner thread, use forCurrentThread to get an instance for this thread");
        }
    }
  • 获得游标。这是数据库查询的标准动作,有了游标才能一行一行的处理查询结果,而重点就是如何处理;
  • 使用游标获得所有的查询结果,并把它放到LIst中list()通过游标遍历所欲的查询结果,把获取到的所有数据都存在内存中。执行的过程中,最终会调用* loadAllFromCursor*方法,其代码如下:
/** Reads all available rows from the given cursor and returns a list of entities. */
List<T> loadAllFromCursor(Cursor cursor){
.....
  // 1. 确保游标在开始处
  if (cursor.moveToFirst()) {
     if (identityScope != null) {
        identityScope.lock();
        identityScope.reserveRoom(count);
    }
    try {
        if (!useFastCursor && window != null && identityScope != null) {
            loadAllUnlockOnWindowBounds(cursor, window, list);
        } else {
           //2. 循环语句开始
            do {
                //3. 将查询结果放入list中
                list.add(loadCurrent(cursor, 0, false));
            } while (cursor.moveToNext());  //4. 通过循环遍历每一行数据,将其保存在list中;
        }
    } finally {
        if (identityScope != null) {
            identityScope.unlock();
        }
    }
}

该函数中有大量的维护查询操作的代码,以上只是节选的部分,请注意注释2,3,4所标识的do-while循环,正是这里把所有的查询结果都放入了List中。

3. listLazy() & listLazyUncached()

这两个方法关系紧密,所以放在一起说。通过方法名字可能有些读者已经猜到了:既然list方法是把所有结果都取出来放在内存中,那带有Lazy的方法就是比较“懒”的,并不一次性把结果取出来。事实上的确如此,这两个方法获得游标后,并不着急把全部结果都取出来,而是等到真正要使用某个结果时,再去数据库中读出数据。两种方法的差别在于,listLazy()使用缓存机制,一个结果被使用过后会被保留下来,下次再使用该结果时,就不用再去数据库中读取,而 listLazyUncached方法不使用缓存机制,所有结果在使用后不保存,每一次都需要去数据中读取。

/**
* Executes the query and returns the result as a list that lazy loads the entities
* on first access. Entities are cached, so accessing the same entity more than 
* once will not result in loading an entity from the underlying cursor again.
* Make sure to close it to close the underlying cursor.
*/
public LazyList<T> listLazy() {
    checkThread();
    Cursor cursor = dao.getDatabase().rawQuery(sql, parameters);
    return new LazyList<T>(daoAccess, cursor, true);
}

public LazyList<T> listLazyUncached() {
    checkThread();
    Cursor cursor = dao.getDatabase().rawQuery(sql, parameters);
    return new LazyList<T>(daoAccess, cursor, false);
}

二者的源码和list方法很像,只不过是最后一部返回的是自定义类型* LazyList,而listLazy* 和 listLazyUncached的区别就体现在如何构建LazyList。下面是其构造函数

* A thread-safe, unmodifiable list that reads entities 
* once they are accessed from an underlying database cursor.
LazyList(InternalQueryDaoAccess<E> daoAccess, Cursor cursor, boolean cacheEntities) {
    this.cursor = cursor;
    this.daoAccess = daoAccess;
    size = cursor.getCount();
    if (cacheEntities) { //1. 如果使用Cache
        //2. 虽然创建了大小和游标查询数量相同的list,但是没有添加对象
        entities = new ArrayList<E>(size);
        for (int i = 0; i < size; i++) {
            entities.add(null);//3. 都是null
        }
    } else {
        entities = null; //不适用cache
    }
    if (size == 0) {
        cursor.close();
    }

    lock = new ReentrantLock();
}

LazyList是一个不可修改的多线程安全的List。

由源码可以发现,当使用缓存时,LazyList构建了一个和查询结果个数相同大小的List entities,但是并没有将查询结果取出,而是把这个List里所有的元素都设为空引用;当不适用cache的模式下,entities直接为空。那何时真正获得要查询的结果呢?别忘记,LazyList也是当做List使用的,只有当真正去get(int position)时,entities才去加载内容。这是有些类似于代理模式,表面上是使用LazyList,实际上还是由List entities来完成操作的。

@Override
public E get(int location) {
    if (entities != null) {
        E entity = entities.get(location); 
        //1. 第一次获得对象
        if (entity == null) {
            lock.lock(); //上锁,在释放锁之前都要考虑多线程操作的问题
            try {
                //2. 第二次获得对象,因为在因上锁而等待期间,
                // 可能有别的进程已经获得了该对象,所以需要再判断一遍
                entity = entities.get(location); 
                if (entity == null) {
                    //3. 正真从数据库中获得对象
                    entity = loadEntity(location);
                    entities.set(location, entity);//将该对象添加到entities中
                    // Ignore FindBugs: increment of volatile is fine here because we use a lock
                     //4. 标记已经获得对象的个数,如果对象已经全部获得,则可以关闭游标
                    loadedCount++;
                   
                    if (loadedCount == size) {
                        cursor.close();
                    }
                }
            } finally {
                lock.unlock();//释放锁
            }
        }
        return entity;
    } else { //5. 不用cache
        lock.lock();
        try {
            return loadEntity(location);//6. 直接获取对象,并不使用缓存
        } finally {
            lock.unlock();
        }
    }
}

get方法正是体现出LazyList多线程安全的地方,它考虑到多个线程可能对其内部List所带来的影响:

  1. 首先判断要获取的对象是否有缓存;
  2. 如果没有则上锁,然后第二次查看该对象是否有缓存,以防止在上锁之前有线程已经获查询了该对象,从而避免重复获取;
  3. 两次判断都没有获得对象缓存之后,才正在从数据库中获得该对象,并将其放入缓存;
  4. 对象加入缓存后,记录当前已经获得对象的个数,如果entities已经被填充完毕之后,则关闭游标;
  5. 如果不使用缓存,每次都需要从数据库中获取数据。

在上锁之前之后都查询对象缓存是否存在,保证避免多线程操作中冲突和重复,这种设计通用而有效的。

也许有的读者会有疑问,greenDAO是不会通过给每个进程都分配Query对象来避免上锁吗,这里为什么还是加锁? 其实这个不矛盾,greanDAO避免的是数据库操作的上锁,而这里是对查询结果List上锁,因为懒加载的原因,listLazy的结果可能已经被使用者获得,而这个结果集合LazyList可能是被多个进程使用的,所以要在获得查询结果上加锁。

值得注意的是,LazyList是不可修改的,所以其覆盖了list中add,set以及remore方法,如果要修改list中的元素就会报错,比如:

@Override
public boolean add(E object) {
    throw new UnsupportedOperationException();
}

使用完毕之后如果需要关闭游标,需要自己手动调用LazyList对象的close方法

4. listIterator()

有了List,自然就有其迭代器。listIterator()就是返回listLazy中的迭代器。

    public CloseableListIterator<T> listIterator() {
        return listLazyUncached().listIteratorAutoClose();
    }

    public CloseableListIterator<E> listIteratorAutoClose() {
        return new LazyIterator(0, true);
    }

LazyIterator是LazyList中的内置迭代器,其源码如下:

protected class LazyIterator implements CloseableListIterator<E> {
        private int index;
        private final boolean closeWhenDone;

        public LazyIterator(int startLocation, boolean closeWhenDone) {
            index = startLocation;
            this.closeWhenDone = closeWhenDone;
        }

        @Override
        public void add(E object) {
            throw new UnsupportedOperationException();
        }

        @Override
        /** FIXME: before hasPrevious(), next() must be called. */
        public boolean hasPrevious() {
            return index > 0;
        }

        @Override
        public int nextIndex() {
            return index;
        }

        @Override
        /** FIXME: before previous(), next() must be called. */
        public E previous() {
            if (index <= 0) {
                throw new NoSuchElementException();
            }
            index--;
            E entity = get(index);
            // if (index == size && closeWhenDone) {
            // close();
            // }
            return entity;
        }

        @Override
        public int previousIndex() {
            return index - 1;
        }

        @Override
        public void set(E object) {
            throw new UnsupportedOperationException();
        }

        @Override
        public boolean hasNext() {
            return index < size;
        }

        @Override
        public E next() {
            if (index >= size) {
                throw new NoSuchElementException();
            }
            E entity = get(index);
            index++;
            if (index == size && closeWhenDone) {
                close();
            }
            return entity;
        }

        @Override
        public void remove() {
            throw new UnsupportedOperationException();
        }

        @Override
        public void close() {
            LazyList.this.close();
        }

    }

这里的代码就很直接明白了,在next方法中调用listLazyget方法,其他要改变list元素的方法都被定义成抛出异常。另外如参数closeWhenDone参数被设置为true的话,当迭代器遍历全部内容之后,就会自动关闭游标。

2.4 使用场景

  • list() : 将全部结果都放在内存中,这样最为直接和常见,但是如果结果集特别大的话,这样做对于内存的压力比较大;此外,如果只是使用结果集中的一小部分,内存就会很是浪费;
  • listLazy() : 只用真正使用到数据时, 才回去访问数据库获得数据,数据加载有延迟,但是比较节省内存,如果不是立刻使用数据结果集,可以考虑;
  • listLazyUncached(): 和listLazy()类似,但是没有使用cache,所以数据不能复用,每次都要去数据库中读取,如果结果集的数据不是被反复使用的话,这样做是最为节省内存的;
  • listIterator(): 以迭代体的形式返回结果,这样的做法更为自由, 如果需要自定义处理过程的话,可以考虑该方法遍历所有结果。
相关文章
|
SQL Java 数据库连接
SpringBoot从入门到精通(二十七)JPA实现自定义查询,完全不需要写SQL!
前面讲了Spring Boot 整合Spring Boot JPA,实现JPA 的增、删、改、查的功能。JPA使用非常简单,只需继承JpaRepository ,无需任何数据访问层和sql语句即可实现完整的数据操作方法。 JPA除了这些功能和优势之外,还有非常强大的查询的功能。以前复查的查询都需要拼接很多查询条件,JPA 有非常方便和优雅的方式来解决
SpringBoot从入门到精通(二十七)JPA实现自定义查询,完全不需要写SQL!
|
5月前
|
缓存 Java 数据库连接
MyBatis原理分析之查询单个对象-1
MyBatis原理分析之查询单个对象-1
67 0
|
5月前
|
SQL XML Java
MyBatis原理分析之查询单个对象-2
MyBatis原理分析之查询单个对象-2
44 0
|
11月前
|
SQL Java 数据库连接
Java 最常见的面试题: mybatis 分页插件的实现原理是什么?
Java 最常见的面试题: mybatis 分页插件的实现原理是什么?
122 0
|
SQL 存储 自然语言处理
MyBatis 学习笔记(八)---源码分析篇--SQL 执行过程详细分析
在面试中我们经常会被到MyBatis中 #{} 占位符与${}占位符的区别。大多数的小伙伴都可以脱口而出#{} 会对值进行转义,防止SQL注入。而${}则会原样输出传入值,不会对传入值做任何处理。本文将通过源码层面分析为啥#{} 可以防止SQL注入。
243 0
MyBatis 学习笔记(八)---源码分析篇--SQL 执行过程详细分析
|
存储 XML 缓存
Flowable基本使用介绍和Flowable数据库表解释
lowable是一个使用Java编写的轻量级业务流程引擎。Flowable流程引擎可用于部署BPMN 2.0流程定义(用于定义流程的行业XML标准), 创建这些流程定义的流程实例,进行查询,访问运行中或历史的流程实例与相关数据,等等。
4091 1
|
XML SQL 存储
源码分析Mybatis MappedStatement的创建流程
源码分析Mybatis MappedStatement的创建流程
源码分析Mybatis MappedStatement的创建流程
|
SQL Oracle Java
详细分析MyBatis框架中exists的基本使用
本篇文章中主要介绍了MyBatis框架中exists的基本用法。在介绍exists的用法的同时,也介绍了not exists的基本使用,说明了exists和in在使用过程中的区别。使用一个具体示例对MyBatis框架中使用exists进行具体详细的说明。最后对SQL中的in,not in,exists,not exists之间的区别进行总结说明。
828 0
详细分析MyBatis框架中exists的基本使用
|
XML Java 关系型数据库
MyBatis源码分析之——构建源码分析测试用例
一、源码准备 首先,到MyBatis官方GitHub地址将MyBatis源码Fork到自己的GitHub仓库中。
274 0
MyBatis源码分析之——构建源码分析测试用例
|
SQL Java 数据库
自己开发一个Java ORM框架(5)-CRUD操作源码
本文目录 1. 整体思路介绍 2. 注解设计与实现 3. 实体类结构解析器DataTable的实现 4. 通过表结构类DataTable生成sql语句 5. 通过EntityOperation封装数据库实体操作 6. 总结和展望
187 0
自己开发一个Java ORM框架(5)-CRUD操作源码