26
2017
09

ObjectBox入门(续)

查询

前面也提到ObjectBox的查询方法有一下3大类:

get
find
query

get方法

先来看下get方法的重载:

getAll()
getAll2()
get(long id)
get(long[] ids)
get(Iterable ids)

getAll()

该方法在源码中的注释是:

/* Returns all stored Objects in this Box. /

用于获取Box中,即表中所有的数据。
代码如下:

List<Person> persons = mPersonBox.getAll();

getAll2()

看到这个方法名是不是有点想打人的冲动?别着急,先来看下这个方法在源码:

/** Does not work yet, also probably won't be faster than {@link Box#getAll()}. */
@Temporary
public List<T> getAll2() {
    Cursor<T> reader = getReader();
    try {
        return reader.getAll();
    } finally {
        releaseReader(reader);
    }
}

惊不惊喜?!意不意外?!而且,重要的是,用这个方法去查询还查不到数据!还真的是 Does not work yet!

get(long id)

这个方法比较简单,从形参就可以看出来是用来查某个id的:

Person person = mPersonBox.get(1);

get(long[] ids)

这个方法也不难,用于获取指定几个id的数据:

long[] ids = {1, 2};
List<Person> persons = mPersonBox.get(ids);

get(Iterable ids)

这个方法是这个5个里面相对比较晦涩的。先看下源码:

/**
 * Get the stored objects for the given IDs.
 *
 * @return null if not found
 */
public List<T> get(Iterable<Long> ids) {
    ArrayList<T> list = new ArrayList<>();
    Cursor<T> reader = getReader();
    try {
        for (Long id : ids) {
            T entity = reader.get(id);
            if (entity != null) {
                list.add(entity);
            }
        }
    } finally {
        releaseReader(reader);
    }
    return list;
}

方法说明很简单:获取给定ids的存储的数据。
可是要怎么使用呢?
别急,先来看下Iterable这个接口。

可以看出我们常用的Collection、List、Set都是Iterable的实现类,也就是说,其实这个方法适用于查询一个有 id组成的集合所对应数据库中的数据的。
那么下面就是简单的代码演示:

List<Long>idList=new ArrayList<>();
idList.add(1L);
idList.add(2L);
idList.add(3L);
List<Person> persons = mPersonBox.get(idList);

到此,get方法结束。

find方法

先看重载函数:

find(int propertyId, long value)
find(int propertyId, String value)
find(Property property, long value)
find(Property property, String value)
find(String propertyName, long value)
find(String propertyName, String value)

总共有三组方法,返回值都是List< Person >,而且从其第二个参数我们可以发现只有longString两种类型。也就说,find方法用于查询的话会有value值类型的限制,如果value是double,那么find方法将无法使用,只能依靠后面的query方法去进行查询。
关于存储数据类型的问题在ObjectBox的issues中也提到了https://github.com/objectbox/objectbox-java/issues/185
回过头来,让我们来测试下上面的3组重载函数怎么使用。

第一组:

find(Property property, long value)
find(Property property, String value)

从其参数可以看出,是通过传Property作为Key来查询的。
那么这个Property怎么获取呢?请看代码:

//根据name查询
List<Person> persons = mPersonBox.find(Person_.name, "HanMeiMei");
//根据age查询
List<Person> persons = mPersonBox.find(Person_.age, 2);

这里需要注意的是Person_.name中的下划线,这个Person_是ObjectBox内部生成的properties class,即属性类,通过它可以直接获取Person类的各个属性。

第二组:

find(String propertyName, long value)
find(String propertyName, String value)

这个也比较好理解,直接看代码:

//根据name查询
List<Person> persons = mPersonBox.find("name", "HanMeiMei");
//根据age查询 
List<Person> persons = mPersonBox.find("age", 2);

第三组:

find(int propertyId, long value)
find(int propertyId, String value)

在使用之前先来看下ObjectBox生成的model文件下下实体类的json文件:

再看看我们之前的Person类:

@Entity
public class Person {
    @Id
    private long id;
    @NameInDb("personName")
    private String name;
    private String address;
    private int age;

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
    // 省略get/set方法
}

大家可以看到,我在之前的name属性上增加了如下的注解:

@NameInDb("personName")

正如其名,我们知道这个注解的作用是把name属性在数据库中的存在名字改成personName。
下面回过头来看看前面的图片,按我的理解,每个红框对应着Person类的4个属性,每个属性它又有id和name属性。具体如下:

Person_.name:值得是最下方整个红框
Person_.name.id:就是该属性的propertyId,它的值是冒号前面的,后面的是refId。(可以看下以前版本的json文件内容,图在下方)
Person_.name.name:是该属性在数据库中的名称

以下是以前版本生成model的json文件:

以前版本的json文件

到此find方法结束。

query方法

在讲解query的一系列方法之前,我们先来认识下下面的2个类:

QueryBuilder< T> :用于构建查询条件
Query :由QueryBuilder调用build()生成,用于执行查询操作

看下简单的示例代码:

QueryBuilder<Person> personQueryBuilder = mPersonBox.query().equal(Person_.name, "HanMeiMei");//构建查询的条件
Query<Person> personQuery = personQueryBuilder.build();//生成Query对象
//personQuery.setParameter(Person_.name,"LiLei");//此处可以重新设置查询条件,重复利用一个Query
List<Person> persons = personQuery.find();//执行查询操作

接下来就开启我们的Query大法之旅。

QueryBuilder的构建

空设置

不带任何查询条件:

Query<Person> build = mPersonBox.query().build();
条件操作符

容易理解的的条件操作符有下面几个:

between :两者之间
contains :包含
equal : 相等
notEqual :不相等
startWith :以什么开头
endWith :以什么结尾
greater :大于
less :小于
in :包含
notIn :不包含
isNull :是否null
isNotNull :是否不null
and :条件与
or :条件或
order :按某属性升序排列返回
orderDesc :按某属性降序返回

以上这些都没什么难度,见其名,则知其义。
其中有个StringOrder的形参在这里简单的提一下。先看下源码:

public enum StringOrder {
    /** The default: case insensitive ASCII characters */
    CASE_INSENSITIVE,

    /** Case matters ('a' != 'A'). */
    CASE_SENSITIVE
}  

我们发现其实它就是一个枚举,说明如下:

StringOrder.CASE_INSENSITIVE :不区分大小写,默认使用
StringOrder.CASE_SENSITIVE :区分大小写

另外还有几个并不是这么直观的,如下:

filter :过滤
eager :官方的解释:ObjectBox solves this challenge by enabling queries to preload relations in the background – aka “eager loading”

简单的举个栗子来看下filter的使用,代码如下:

//增加过滤条件:以H开头的名字
QueryBuilder<Person> personQueryBuilder =
        mPersonBox.query().endsWith(Person_.name, "MEI").filter(new QueryFilter<Person>() {
            @Override
            public boolean keep(Person entity) {
                return entity.getName().startsWith("H");
            }
  });

无难点,pass。
接着来看看那个eager是怎么回事。
具体的使用在后面讲到 一对多等关系的时候再细说的,这里在官方博客的一篇文章中看到了一个例子(地址在文章末尾),就简单的记录下它的作用,代码如下:

//初始化数据--省略的多个Playlist的书写
Playlist playlist = new Playlist("My favs");
playlist.songs.add(new Song("Lalala");
playlist.songs.add(new Song("Lololo");
box.put(playlist);
//还有好几个Playlist

//获取所有的Playlist下所有的Song
box.query().eager(Playlist_.songs).build();

从上面可以看出,跳过了对每个Playlist的查询,这就是eager,详细的使用说明会在后续的章节中进行讲解。

Query的使用

find系列

find系列具体有如下这些方法:

find() :查询所有
findFirst() :返回第一个
findUnique() :返回唯一的,如果有多个就报错,不存在则返回null
findIds() :返回所有数据的id
find(long offset, long limit): 分页查询
findLazy() : 懒汉式加载不带缓存
findLazyCached() : 懒汉式加载带缓存

来看下find(long offset, long limit)方法的使用:

//官方实例
Query<User> query = userBox.query().equal(UserProperties.FirstName, "Joe").build();
List<User> joes = query.find(/** offset by */ 10, /** limit to */ 5 /** results */);

第一个参数是指前面跳过的个数,第二个参数指的是最多查询保存的个数。

再来谈谈findLazy()和findLazyCached()。
首先看下官方的说明:

Lazy loading results

To avoid loading query results right away, Query offers findLazy() and findLazyCached() which return a LazyList of the query results.
LazyList is a thread-safe, unmodifiable list that reads entities lazily only once they are accessed. Depending on the find method called, the lazy list will be cached or not. Cached lazy lists store the previously accessed objects to avoid loading entities more than once. Some features of the list are limited to cached lists (e.g. features that require the entire list). See the LazyList class documentation for more details.

再来看看LazyList的说明:

A thread-safe, unmodifiable list that reads entities lazily once they are accessed. A lazy list can be cached or not. Cached lazy lists store the previously accessed objects to avoid loading entities more than once. Some features of the list are limited to cached lists (e.g. features that require the entire list).

Note: this list gives an semiconsitent view on the data at the moment it was created. If you remove objects from their object box after this list was created, this list will null instead of an object. However, if you add objects to their object box after this list was created, this list will not be extended.

我来简单的解释下上面的内容以及我的个人理解:

findLazy():类似于快照,保存了当时的数据,如果你删除了其中的某个,对应的某个就会变为null,而如果新增,却不会增加。但是在我实际编写代码的时候,我发现,如果你删了某个object,就会像官方文档说的那样用一个null来代替。
findLazyCached():这个和findLazy其实差不多,就是多了个缓存的效果,就是如果你删了某个object,仍能获取到数据。

先看下上面2个方法的源码:

findLazy的源码:

public LazyList<T> findLazy() {
    ensureNoFilter();
    return new LazyList<>(box, findIds(), false);
}

findLazyCached的源码:

 /**
 * Find all Objects matching the query without actually loading the Objects. See @{@link LazyList} for details.
 */
@Nonnull
public LazyList<T> findLazyCached() {
    ensureNoFilter();
    return new LazyList<>(box, findIds(), true);
}

从注解上就可以看出,后者是不能返回null的;在返回值上看,二者都是返回一个LazyList的实例,只不过最后的一个是否缓存的boolean形参不同。

再来看下LazyList的源码:

private final List<E> entities;
private final long[] objectIds;

LazyList(Box<E> box, long[] objectIds, boolean cacheEntities) {
    if (box == null || objectIds == null) {
        throw new NullPointerException("Illegal null parameters passed");
    }
    this.box = box;
    this.objectIds = objectIds;
    size = objectIds.length;
    if (cacheEntities) {
        entities = new ArrayList<>(size);
        for (int i = 0; i < size; i++) {
            entities.add(null);
        }
    } else {
        entities = null;
    }
}

从上面可以看出,不管是findLazy还是findLazyCached,返回的结果中都没有我们需要的数据库中的对象。其中findLazy返回的实例中entities是null,而findLazyCached返回的实例中的entities则是一个有大小的ArrayList,只不过存储的内容都用null来填充(然而在Debug中发现了很奇怪的事,就是执行的事add(null),但可以看到entities中并不是null,而是我们真正要的数据,这个不太清楚是什么原因。同样的是的在get函数中,entities中的元素非空,可是在get(location)的时候又返回null)。二者返回实例中共有的是那些对象的objectId,用于后续的数据查询。
这种调用了方法而不直接返回所需数据的做法,就是Lazy的体现。

继续看源码:

@Override
public E get(int location) {
    if (location < 0 || location > size) {
        throw new IndexOutOfBoundsException("Illegal cursor location " + location);
    }

    if (entities != null) {//调用findLazyCached
        E entity = entities.get(location);
        if (entity == null) {
            // Do DB action outside of synchronized and check later if we use the new entity.
            E newEntity = box.get(objectIds[location]);
            synchronized (this) {
                // Check again to ensure that always the same entity is returned once cached
                entity = entities.get(location);
                if (entity == null) {
                    entity = newEntity;
                    entities.set(location, newEntity);
                    // Ignore FindBugs: increment of volatile is fine here because we use synchronized
                    loadedCount++;
                }
            }
        }
        return entity;
    } else {//调用findLazy
        synchronized (this) {
            return box.get(objectIds[location]);
        }
    }
}

不管是使用for-each还是直接调用get方法,最终都是通过上面的代码返回我们真正所需要的数据。

从上面的源码可以看到,如果调用findLazy函数,每次都会根据传入的objectId从box中获取数据;而如果调用findLazyCached函数,那么它就会用到之前的entities集合来对数据进行缓存处理。这就是二者带不带缓存的实现原理。

接着来说说为什么这个LazyList返回之后,新建的数据无法添加进去的原理。
其实看了源码,大家都注意到,在返回LazyList实例的时候,它里面就已经保存了之前查询到的数据所有的objectId,在get函数中又都是根据这个objectId去取值的,所以新增加objectId并不存在于之前返回的LayzList中,那就肯定查不到数据了。(那是不是说如果我能让新的数据的objectId存在于返回的实例中就可以呢?比如我删了某个id,而后用重新添加这个id,属性不同,而后再去调用findLazy就可以获取到这个新数据–待测试)

最后再说那个官网说在使用findLazy函数的时候,删除某个object之后会用null代替的实现。
从下面这部分源码来分析:

if(entities != null) {//调用findLazyCached
...
} else {//调用findLazy
        synchronized (this) {
            return box.get(objectIds[location]);
        }
}

当我们删除了某个id之后,box中就不存在了,而我们在调用findLazy函数的时候仍然通过下面的代码去获取数据:

return box.get(objectIds[location]);

可想而知,肯定是获取不到我们所需的数据。那为什么会返回null呢?我们继续来看下box.get函数的源码:

/**
 * Get the stored object for the given ID.
 *
 * @return null if not found
 */
public T get(long id) {
    Cursor<T> reader = getReader();
    try {
        return reader.get(id);
    } finally {
        releaseReader(reader);
    }
}  

注释已经告诉我们了,如果没找到就返回null。

而至于findLazyCached的删除后仍返回,这个就是其缓存实现的效果。

下面简单的模拟下使用findLazy和findLazyCached删除返回的场景:

//添加数据--此处代码省略
...
//查询
QueryBuilder<Person> personQueryBuilder = mPersonBox.query().endsWith(Person_.name, "MEI");
Query<Person> personQuery = personQueryBuilder.build();
persons = personQuery.findLazy();
//persons = personQuery.findLazyCached();
//删除第一个
mPersonBox.remove(persons.get(0));

//打印之前的第一个
Person person=persons.get(0);
if(person==null){
   Log.d("MainActivity", "Null");
}else {
   Log.d("MainActivity", person.toString());
}

//打印结果
log-->Null
//log-->以前的对象
其他

用过Rx的小伙伴都知道,map和flatmap操作符可以对原有的对象进行处理再返回,ObjectBox虽然没有这一类的操作符,但也直接提供了以下常用的函数并且也支持Rx:

min/minDouble :返回最小值
max/maxDouble :返回最大值
sum/sumDouble :返回所有值的总和
avg :返回平均值,通常是一个double类型的
count :返回查询结果的数量
remove :删除数据库中符合查询条件的数据
subscribe :订阅
publish :发布

前面几个都好理解,来看看subscribe的简单用法。

private DataSubscription subscription;
...

QueryBuilder<Person> personQueryBuilder = mPersonBox.query().endsWith(Person_.name, "MEI");
Query<Person> personQuery = personQueryBuilder.build();
SubscriptionBuilder<List<Person>> subscribe = personQuery.subscribe();
subscription=subscribe.observer(new DataObserver<List<Person>>() {
    @Override
    public void onData(List<Person> data) {
            //处理新返回的数据
    }
});
...

@Override
protected void onDestroy() {
    subscription.cancel();
    super.onDestroy();
}

上面的代码实现了只要数据更新就会调用onData(..)方法将最新的数据返回。

上面的代码块中有个SubscriptionBuilder类,它有多个操作符来构建其自身。具体如下:

week : 使用弱引用
single : 只执行这一次并返回当前查询结果
onlyChanges : 只有在数据变化的时候才进行回调

接下来让我们从源码来分析下这3个操作符的具体实原理。
先来看下最简单的week操作符:
源码如下:

private boolean weak;
 ...
/**
* Uses a weak reference for the observer.
* It is still advised to remove observers explicitly if possible: relying on the garbage collection may cause
* non-deterministic timing. Until the weak reference is actually cleared by GC, it may still receive notifications.
*/
public SubscriptionBuilder<T> weak() {
    weak = true;
    return this;
}

...
  */
public DataSubscription observer(DataObserver<T> observer) {
    WeakDataObserver<T> weakObserver = null;
    if (weak) {
        observer = weakObserver = new WeakDataObserver<>(observer);
    }
    this.observer = observer;
    ...
}

可以看出,week的作用就是对原来的observer进行弱引用化

接着看看singleonlyChanges操作符。
源码:

public SubscriptionBuilder<T> single() {
    single = true;
    return this;
}

public SubscriptionBuilder<T> onlyChanges() {
    onlyChanges = true;
    return this;
}

上面的源码并未给与我们足够的信息来认识这2个操作符。继续看下面的源码:

private boolean single;
private boolean onlyChanges;
...

/**
* The given observer is subscribed to the publisher. This method MUST be called to complete a subscription.
*
* @return an subscription object used for canceling further notifications to the observer
*/
public DataSubscription observer(DataObserver<T> observer) {
    WeakDataObserver<T> weakObserver = null;
    if (weak) {
        observer = weakObserver = new WeakDataObserver<>(observer);
    }
    this.observer = observer;
    DataSubscriptionImpl subscription = new DataSubscriptionImpl(publisher, publisherParam, observer);
    if (weakObserver != null) {
        weakObserver.setSubscription(subscription);
    }

    // TODO FIXME when an observer subscribes twice, it currently won't be added, but we return a new subscription

    // Trivial observers do not have to be wrapped
    if (transformer != null || scheduler != null || errorObserver != null) {
        observer = new ActionObserver(subscription);
    }

    if(single) {
        if(onlyChanges) {
            throw new IllegalStateException("Illegal combination of single() and onlyChanges()");
        }
        publisher.publishSingle(observer, publisherParam);
    } else {
        publisher.subscribe(observer, publisherParam);
        if(!onlyChanges) {
            publisher.publishSingle(observer, publisherParam);
        }
    }
    return subscription;
}

从前面2段源码,我们可以看出,使用single或者onlyChanges操作符的时候,其内部的一个标识转为true,而后在observer的函数中会根据这2个标识执行不同的方法。
在分析不同情形的调用情况之前,我们先来看看上面代码块中的2个方法:

publisher.publishSingle(observer, publisherParam);  
publisher.subscribe(observer, publisherParam);

先分析**publishSingl**e的源码:

@Override
public void publishSingle(final DataObserver<List<T>> observer, @Nullable Object param) {
    box.getStore().internalScheduleThread(new Runnable() {
        @Override
        public void run() {
            List<T> result = query.find();
            observer.onData(result);
        }
    });
}

可以看出,调用该方法最后会有一个回调,把查询的结果进行了返回。

再来分析subscribe的源码:

@Override
public synchronized void subscribe(DataObserver<List<T>> observer, @Nullable Object param) {
    final BoxStore store = box.getStore();
    if (objectClassObserver == null) {
        objectClassObserver = new DataObserver<Class<T>>() {
            @Override
            public void onData(Class<T> objectClass) {
                publish();
            }
        };
    }
    if (observers.isEmpty()) {
        if (objectClassSubscription != null) {
            throw new IllegalStateException("Existing subscription found");
        }
        objectClassSubscription = store.subscribe(box.getEntityClass())
                .weak()
                .onlyChanges()
                .observer(objectClassObserver);
    }
    observers.add(observer);
}

从上面可以看出,采用的是观察者模式,在调用subscribe函数的最后并未像publishSingle那样有对本次查询结果进行回调返回处理。

看完上面2个方法的内部实现,现在来小结下singleonlyChanges操作符的使用:

  1. 都不进行调用:publishSingle和subscribe都调用,所以本次的查询结果会进行返回,同时后续数据更新了也会进行返回;
  2. 只调用single:只执行了publishSingle,仅对本次查询结果进行了返回,后续数据更新了不会进行返回;
  3. 只调用onlyChanges:只执行了subscribe,本次的查询结果不进行返回,后续数据更新进行返回;
  4. 都调用:抛异常-“Illegal combination of single() and onlyChanges()”

最后还有个publish操作符。
源码如下:

void publish() {
    box.getStore().internalScheduleThread(new Runnable() {
        @Override
        public void run() {
            List<T> result = query.find();
            for (DataObserver<List<T>> observer : observers) {
                observer.onData(result);
            }
        }
    });
}

看过了publishSingle再来看这个,简直太简单了。
示例代码如下:

QueryBuilder<Person> personQueryBuilder = mPersonBox.query().endsWith(Person_.name, "MEI");
Query<Person> personQuery = personQueryBuilder.build();
SubscriptionBuilder<List<Person>> subscribe = personQuery.subscribe();

subscribe.onlyChanges().observer(mDataObserver);
subscribe.onlyChanges().observer(mDataObserver2);
personQuery.publish();

可是上面的代码其实和下面的效果是一样的:

subscribe.observer(mDataObserver);
subscribe.observer(mDataObserver2);

他们的效果都是实现第一次查询和后续更新都能进行返回,所以不是很理解这个publish的用,或许他另有用处需要我们去发现吧。


参考资料

  1. ObjectBox官方文档:http://objectbox.io/documentation/queries/
  2. ObjectBox Java 1.0.0 API:http://objectbox.io/files/objectbox-java/current/io/objectbox/query/LazyList.html
  3. ObjectBox的github:https://github.com/objectbox/objectbox-java
  4. 官方博客:http://objectbox.io/objectbox-1-0/
上一篇:gradle使用技巧 下一篇:gradle buildTypes