Queries

Last updated 4 days ago

ObjectBox queries return persisted objects that match user defined criteria. With ObjectBox DB you use the QueryBuilder class to specify criteria and create Query objects. The Query class will actually run the query and return matching objects.

QueryBuilder

The QueryBuilder<T> class lets you build custom queries for your entities. You create a QueryBuilder instance via Box.query and then offers several methods to define query conditions. Those condition methods also require a property specification. In ObjectBox, properties are not references by their name (String), but using meta information classes that are generated during build time. The meta information classes are named like the corresponding entity class with a trailing underscore and hava a static field for each property. This allows to reference properties safely with compile time checks (no runtime errors because of typos).

This example assumes an entity class User and its generated meta info class User_.

Simple condition example: Query for all users with the first name “Joe”:

List<User> joes = userBox.query().equal(User_.firstName, "Joe").build().find();

Multiple conditions example: Get users with the first name “Joe” that are born later than 1970 and whose last name starts with “O”.

QueryBuilder<User> builder = userBox.query();
builder.equal(User_.firstName, "Joe")
.greater(User_.yearOfBirth, 1970)
.startsWith(User_.lastName, "O");
List<User> youngJoes = builder.build().find();

For an overview of all available criteria, please refer to the QueryBuilder class.

Query

Queries are created (and not yet executed) by calling build() on the QueryBuilder.

Query<User> query = builder.build();

Finding objects

There are a couple of find methods to retrieve objects matching the query:

// return all entities matching the query
List<User> joes = query.find();
// return only the first result or null if none
User joe = query.findFirst();
// return the only result or null if none, throw if more than one result
User joe = query.findUnique();

To return all entities matching the query simply call find().

To only return the first result, use findFirst().

If you expect a unique result call findUnique() instead. It will give you a single result or null, if no matching entity was found and throw an exception if there was more than one result.

Reusing Queries and Parameters

Query objects allow you to execute queries multiple times in an efficient way. To make queries more reusable all criteria values you previously set in the QueryBuilder can be changed. That’s why we call them query parameters.

Let’s look at an example:

Query<User> query = userBox.query().equal(User_.firstName, "").build();
List<User> joes = query.setParameter(User_.firstName, "Joe").find();
List<User> jakes = query.setParameter(User_.firstName, "Jake").find();

Here we used one query object to find two sets of users, each with a specific first name. Note that we still have to initialize the value for the firstName property when building the query. Because we always overwrite the value using .setParameter() we can just pass an empty string when initially building the query.

Caching the query object is good practice for queries that run frequently.

Limit, Offset, and Pagination

Sometimes you only need a subset of a query, for example the first 10 elements to display in your user interface. This is especially helpful (and resourceful) when you have a high number of entities and you cannot limit the result using query conditions only. The built Query<T> has a .find(long offset,long limit) method with offset and limit arguments:

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

offset: The first offset results are skipped.

limit: At most limit results of this query are returned.

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.

Removing Objects

To remove all objects matching a query, call query.remove() .

PropertyQuery

If you only want to return the values of a certain property and not a list of full objects you can use a PropertyQuery. After building a query simply call property(Property) to define the property followed by the appropriate find method.

For example to only get the email addresses of all users:

String[] emails = userBox.query().build().property(User_.email).findStrings();

In general there is always a find method to return the value of the first result, or the values of all results. For example findString() and findStrings() and so on.

Note: an array of property values is not in any particular order, also not if you have set one when building the query.

Handling null values

By default null values are not returned. However, you can specify a replacement value to use if a property is null:

// returns 'unknown' if email is null
String[] emails = userBox.query().build()
.property(User_.email).nullValue("unknown").findStrings();

Distinct and unique results

The property query can also only return distinct values:

// returns 'joe'
String[] names = userBox.query().build()
.property(User_.firstName).distinct().findStrings();

By default the case of strings is ignored. However, the distinct call can be overloaded to enable case sensitivity:

// returns 'Joe', 'joe', 'JOE'
String[] names = userBox.query().build().property(User_.firstName)
.distinct(StringOrder.CASE_SENSITIVE).findStrings();

If only a single value is expected to be returned the query can be configured to throw if that is not the case:

// throws if not exactly one name
String[] names = userBox.query().build().equal(User_.isAdmin, true)
.property(User_.firstName).unique().findStrings();

The distinct and unique flags may also be combined.

Aggregating values

Property queries (JavaDoc) also offer aggregate functions to directly calculate the minimum, maximum, average and sum of all found values:

  • min / minDouble: Finds the minimum value for the given property over all objects matching the query.

  • max / maxDouble: Finds the maximum value.

  • sum / sumDouble: Calculates the sum of all values. Note: the non-double version detects overflows and throws an exception in that case.

  • avg : Calculates the average (always a double) of all values.

Eager Loading of Relations

By default relations are loaded lazily: when you first access it it will perform a database lookup to get the data (subsequent accesses will get a cached values without). While this lookup is a fast operation with ObjectBox, there are situations in which you want to avoid this. ObjectBox also allows you to eagerly load relations to avoid those lookups completely. You can specify this using the QueryBuilder.eager()method.

Query filters

Query filters come into play when you are looking for objects that need to match complex conditions, which cannot be fully expressed with the QueryBuilder class. Filters are written in Java and thus can express any complexity. Needless to say that database conditions can be matched more efficiently than Java based filters. Thus you will get best results when you use both together:

  1. Narrow down results using standard database conditions to a reasonable number (use QueryBuilder to get “candidates”)

  2. Now filter those candidates using the QueryFilter Java interface to identify final results

A QueryFilter implementation looks at one candidate object at a time and returns true if the candidate is a result or false if not.

Example:

// Reduce object count to reasonable value
songBox.query().equal(Song_.bandId, bandId)
// Filter is performed on candidate objects
.filter((song) -> {
return song.starCount * 2 > song.downloads;
})

Notes on performance: 1) ObjectBox creates objects very fast. 2) The virtual machine is tuned to garbage collect short lived objects. Notes 1) and 2) combined makes a case for filtering because ObjectBox creates candidate objects of which some are not used and thus get garbage collected quickly after their creation.

Query filters and ToMany relation

The ToMany class offers additional methods that can be convenient in query filters:

  • hasA: returns true if one of elements matches the given QueryFilter

  • hasAll: returns true if all of elements match the given QueryFilter

  • getById: return the element with the given ID (value of the property with the @Id annotation)