Queries

OT
Last updated 13 days ago

Learn how to get to entities and their property values by filtering for specific criteria.

Inspecting Entities

Let's assume we have this interesting Entity:

class Person: Entity {
var id: Id<Person> = 0
var firstName: String
var lastName: String
var age: Int
init(firstName: String, lastName: String, age: Int) {
self.firstName = firstName
self.lastName = lastName
self.age = age
}
// ...
}

For this entity, the code generator will create something like this:

extension Person {
static var id: Property<Person, Id<Person>> { /* ... */ }
static var firstName: Property<Person, String> { /* ... */ }
static var lastName: Property<Person, String> { /* ... */ }
static var age: Property<Person, Int> { /* ... */ }
}

That's your entity property metadata. The code generator will create these static properties for you with the same name as a stored object's properties. You'll be using this for queries.

The associated types of the generic Property<Entity, Value> itself encode so much interesting information already to make the query interface very intuitive, for example:

  • Property<E, String> exposes methods that make sense for strings, like contains

  • Property<E, Int> exposes methods that make sense for numbers, including comparisons

  • Property<E, Date> exposes methods for dates, like isBetween(_:and:)

All of these factory methods create a QueryCondition, the type used for queries, and a lot of them have intuitive operator variants. Please refer to the API docs for a full list of operators and methods.

Building Queries that Return Entities

Beyond simple "fetch all" commands, like personBox.all(), you may want to filter entities by their properties. ObjectBox offers a very Swifty query API for you to use:

let query: Query<Person> = personBox.query {
Person.firstName.startsWith("Steve")
}
let allSteves: [Person] = query.find()

The block you pass to Box.query(_:) is of the type () -> QueryCondition<Person>. That's where all the methods of the Property type from above come into play.

If you omit the block, the query will be configured to return results similar to Box.all():

let allQuery = personBox.query()
let allPersons = allQuery.find()

Store query objects for re-use, if you can, to increase performance.

You can call Query.find(), Query.count, and all the other operations that execute a query multiple times. In performance-critical situations, building a new query object thousands of times can easily become costly.

Combine Conditions with Boolean Operators and Parentheses

Query conditions can be combined by the use of the boolean operators && and ||.

let query: Query<Person> = personBox.query {
Person.firstName.startsWith("Ste")
&& Person.lastName.startsWith("Jo")
}
let maybeSteveJobs = query.find()

Since these are overloads of native Swift operators, their standard operator precedence rules apply (&& > ||). You can use parentheses to group conditions and affect the truth condition of the whole expression.

That's fancy talk for: group condition parts by wrapping them in parens.

let query: Query<Person> = personBox.query {
(Person.firstName.contains("Santa") || Person.age > 100)
&& Person.lastName.isEqual(to: "Claus", caseSensitive: true)
}
let oldClauses = query.find()

QueryCondition Operators

Thanks to Swift's amazing superpowers, we also provide operators for the most common operations. Please note that the property comes first:

  • Collection containment: and ; for example, disallowing teens entry to your disco: Person.age ∉ (10..<18)

  • Equality: == and !=, as in Person.firstName == "Steve"

  • Comparison: < and >, as in CocoaPod.rating > 4.5

There are no custom operators for conditions like Property<T, String>.startsWith, because we don't want to be ridiculous, so you should still know the regular condition factiory methods for maximum effect.

Query Operations

Once you have a query set up properly, you can execute it as often as you want to get the latest results of its evaluation. These are the most common operations:

  • Query.all and Query.find() will return all results that match

  • Query.count will return the count of all results that match

  • Query.first will return an optional first match from the result set

Please refer to the Query API docs for a comprenhesive list of permitted operations.

Paginating Results

Pass an offset and limit to Query.find to paginate results:

let page = 3
let resultsPerPage = 10
let results = personBox.find(offset: page * resultsPerPage, limit: resultsPerPage)
display(persons: results)

Sorting

Sorting results is not yet implemented in ObjectBox Swift Alpha 1.

Modifying Conditions Later

You can modify conditions after creation of the query. That can be useful if your base query stays the same but one small part changes between find() executions and you don't want to create a new Query object everytime.

Please refer to the Query API docs for a comprenhesive list of permitted setParameter variants.

setParameter Changes the Condition Value

For this purpose, there's a lot of useful variants of the Query.setParameter and Query.setParameters method. (Note the plural s.) The first version will set the comparison value of a condition that matches to a new value. The second version does the same for comparisons with two values, hence the plural s.

For example:

  • Query.setParameter(Person.age, to: 18) for a query you built using personBox.query { Person.age == 21 } will effectively change the condition to Person.age == 18

  • Query.setParameters(Person.age: to: 10, and: 18) for a query you built using personBox.query { Person.age.isBetween(0, and: 99) } will effectively change the condition to Person.age.isBetween(10, and: 18)

You will get runtime exceptions if you try to call the plural-s-Version when the original condition only has one comparison value, and vice versa.

Use PropertyAlias to Disambiguate Conditions

One problem of the Property type-based approach above is that you have no control what happens when there are two or more conditions on the same property:

let query = personBox.query {
Person.age > 10
&& Person.age < 50
}
query.setParameter(Person.age, to: 25) // did this affect "<" or ">" ?

To not get stuck in a situation like this, we introduced aliases: you can register a short name (called an alias) for a query condition. That's a simple string to give the condition a name.

We use a variant of the definition operator (.=) for this:

let query = personBox.query {
"MinAge" .= Person.age > 10
&& "MaxAge" .= Person.age < 50
}
query.setParameter("MinAge", to: 25)

There is, of course, also a plural s-variant for conditions with two values. The same warning as above applies: make sure not to mix up the plural and singular versions.

A downside of the string-based alias approach is that you lose type information: you have to make sure not to pass a Double to a setter for a condition that operates on String, for example.

Building Queries that Operate on Properties

In addition to filtering entities, you can also build queries for their properties. These are aggregate functions like max, min, average, count, but also various find methods. These are available on the PropertyQuery type, that you can get from a regular query like this:

let query: Query<Person> = personBox.query { Person.firstName.startsWith("P") }
let agePropertyQuery: PropertyQuery<Person, Int> = query.property(Person.age)

The PropertyQuery will respect all conditions of its original Query. So in the example above, only entities with a firstName that starts with "S" will be regarded. Use the empty block variant personBox.query() if you want to operate on all entities.

Please refer to the PropertyQuery API docs for an exhaustive list of operations.

Aggregate Functions

A simple aggregate function is to request the maximum, minimum, or average value of a given property.

let query: Query<Person> = personBox.query()
let agePropertyQuery: PropertyQuery<Person, Int> = query.property(Person.age)
let maxAge: Int = agePropertyQuery.max
let minAge: Int = agePropertyQuery.min
let averageAge: Int = agePropertyQuery.average

Number-based operations are only available for number-based properties and not for strings, for example.

Fetching all Property Values

In addition to aggregate functions, you can also use "find" methods. These will return any or all of the values of entities matching the query:

let query: Query<Person> = personBox.query { Person.firstName == "Steve" }
let steveLastNamePQ: PropertyQuery<Person, String> = query.property(Person.lastName)
let allOfStevesLastNames: [String] = steveLastNamePQ.findStrings()
// if allOfStevesLastNames.contains("Jobs") { ... }

While the possibilities are not literally endless, they are plentiful if you combine query conditions with property-based requests.

Distinct and Unique Results

The property query can also only return distinct values, not producing duplicates. That means if you have 5 people in your database with ages [25, 30, 30, 40, 40], you will get [25, 30, 40] as a distinct result set.

For example:

let names: [String] = personBox.query()
.property(Person.firstName)
.distinct(caseSensitiveCompare: false)
.findStrings()

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

let singleNameResults: [String] = personBox.query { Person.age > 999 }
.property(Person.firstName)
.unique()
.findStrings()
assert(names.count == 1)

You can combine unique with distinct.