Queries

Last updated last month

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. Create an instance via Box.query() .

QueryBuilder offers several methods to define query conditions for properties of an entity. To specify a property ObjectBox does not use Strings but meta information "underscore" classes (like User_) that are generated during build time. The meta information classes have a static field for each property (like User_.firstName). This allows to reference properties safely with compile time checks to prevent runtime errors, for example because of typos.

Here are two examples using a simple and a more complicated query:

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();

Notable conditions

In addition to expected conditions like equal(), notEqual(), greater() and less() there are also conditions like:

  • isNull() and notNull(),

  • between() to filter for values that are between the given two,

  • in() and notIn() to filter for values that match any in the given array,

  • startsWith(), endsWith() and contains() for extended String filtering.

In addition, there is and() and or() to build more complex combinations of conditions.

For an overview of all available criteria, please refer to the QueryBuilder class and its method documentations.

Ordering results

In addition to specifying conditions you can order the returned results using the order() method:

userBox.query().equal(User_.firstName, "Joe")
.order(User_.lastName) // in ascending order, ignoring case
.find();

You can also pass flags to order() to sort in descending order, to sort case sensitive or to specially treat null values. For example to sort the above results in descending order and case sensitive instead:

userBox.query().equal(User_.firstName, "Joe")
.order(User_.lastName, QueryBuilder.DESCENDING | QueryBuilder.CASE_SENSITIVE)
.find();

Order conditions can also be chained. Check the method documentation for details.

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

If you frequently run a Query you should cache the Query object and re-use it. To make a Query more reusable you can change the values, or query parameters, of each condition you added even after the Query is built. Let's see how.

Assume we want to find a list of User with specific firstName values. First, we build a regular Query with an equal() condition for firstName. Because we have to pass an initial parameter value to equal() but plan to override it before running the Query later, we just pass an empty string:

// build query
Query<User> query = userBox.query().equal(User_.firstName, "").build();

Now at some later point we want to actually run the Query. To set a value for the firstName parameter we call setParameter() on the Query and pass the firstName property and the new parameter value:

// change firstName parameter to "Joe", get results
List<User> joes = query.setParameter(User_.firstName, "Joe").find();
...
// change firstName parameter to "Jake", get results
List<User> jakes = query.setParameter(User_.firstName, "Jake").find();

So you might already be wondering, what happens if you have more than one condition using firstName? For this purpose you can assign each parameter an alias by calling parameterAlias() right after specifying the condition:

// assign alias "name" to the equal() query parameter
Query<User> query = userBox.query()
.equal(User_.firstName, "").parameterAlias("name");

Then, when setting a new parameter value pass the alias instead of the property:

// change parameter with alias "name" to "Joe", get results
List<User> joes = query.setParameter("name", "Joe").find();

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, instead of getting all Users, to just get their email addresses:

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

In general there is always a find method to just return the value of the first result, like findString(), or the values of all results, like findStrings().

Note: the returned array of property values is not in any particular order, even if you did specify an order when building the query.

Handling null values

By default null values are not returned. However, you can specify a replacement value to return 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 can be combined.

Aggregating values

Property queries (JavaDoc) also offer aggregate functions to directly calculate the minimum, maximum, average, sum and count 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.

  • count(): returns the number of results. This is faster than finding and getting the length of the result array. Can be combined with distinct() to count only the number of distinct values. Since 2.0.0.

Add query conditions for related entities (links)

Since 2.0.0

After creating a relation between entities, you might want to add a query condition for a property that only exists in the related entity. In SQL this is solved using JOINs. But as ObjectBox is not a SQL database we built something very similar: links. Let's see how this works using an example.

Assume there is a Person that can be associated with multiple Address entities:

@Entity
class Person {
@Id var id: Long = 0
var name: String? = null
lateinit var addresses: ToMany<Address>
}
@Entity
class Address {
@Id var id: Long = 0
var street: String? = null
var zip: String? = null
{

To get a Person with a certain name that also lives on a specific street, we need to query the associated Address entities of a Person. To do this, use the link(RelationInfo) method of the query builder to tell that the addresses relation should be queried. Then add a condition for Address:

// get all Person objects named "Elmo"...
val builder = box.query().equal(Person_.name, "Elmo")
// ...which have an address on "Sesame Street"
builder.link(Person_.addresses).equal(Address_.street, "Sesame Street")
val elmosOnSesameStreet = builder.build().find()

What if we want to get a list of Address instead of Person? If you know ObjectBox relations well, you would probably add a @Backlink relation to Address and build your query using it with link(RelationInfo) as shown above:

@Entity
class Address {
// ...
@Backlink(to = "addresses")
lateinit var persons: ToMany<Person>
}
// get all Address objects with street "Sesame Street"...
val builder = box.query().equal(Address_.street, "Sesame Street")
// ...which are linked from a Person named "Elmo"
builder.link(Address_.persons).equal(Person_.name, "Elmo")
val sesameStreetsWithElmo = builder.build().find();

But actually you do not have to modify the Address entity (you still can if you need the @Backlink elsewhere). Instead we can use the backlink(RelationInfo) method to create a backlink to the addresses relation from Person just for that query:

// get all Address objects with street "Sesame Street"...
val builder = box.query().equal(Address_.street, "Sesame Street")
// ...which are linked from a Person named "Elmo"
builder.backlink(Person_.addresses).equal(Person_.name, "Elmo")
val sesameStreetsWithElmo = builder.build().find()

Eager Loading of Relations

By default relations are loaded lazily: when you first access a ToOne or ToMany property it will perform a database lookup to get its data. On each subsequent access it will use a cached version of that data.

val customers = customerBox.query().build().find()
// Customer has a ToMany called orders
customers[0].orders[0] // first access: this will cause a database lookup

While this initial lookup is fast, you might want to prefetch ToOne or ToMany values before the query results are returned. To do this call the QueryBuilder.eager method when building your query and pass the RelationInfo objects associated with the ToOne and ToMany properties to prefetch:

val customers = customerBox.query()
.eager(Customer_.orders) // Customer has a ToMany called orders
.build()
.find()
customers[0].orders[0] // first access: this will NOT cause a database lookup

Eager loading only works one level deep. If you have nested relations and you want to prefetch relations of all children, you can instead add a query filter as described below. Use it to simply access all relation properties, which triggers them to lookup there values as described above.

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)