Queries
The ObjectBox Swift database supports building Queries using a query builder. Learn all about Query syntax and how to get to entities and their property values by filtering for specific criteria.
Beyond simple "fetch all" commands, like
personBox.all()
, you may want to filter entities by their properties. ObjectBox's query syntax should be familiar to every Swift developer, because queries are written in Swift:let query: Query<Person> = try personBox.query {
Person.name.startsWith("Andrea")
}.build()
let allAndreas: [Person] = try query.find()
You simply write your class name, the property name, and then an operator, or a method name (like
startsWith()
here). While most of these calls look like the regular Swift calls you're used to, they are actually provided by ObjectBox's code generator, so not all methods you would e.g. find on a String
are available for queries. See below for available operations.Store query objects for re-use 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.Query conditions can be combined by the use of the logical operators
&&
and ||
.let query: Query<Person> = try personBox.query {
Person.firstName.startsWith("Ste")
&& Person.lastName.startsWith("Jo")
}.build()
let maybeSteveJobs = try 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> = try personBox.query {
(Person.firstName.contains("Santa") || Person.age > 100)
&& Person.lastName.isEqual(to: "Claus", caseSensitive: true)
}.build()
let oldClauses = try query.find()
ObjectBox also provides 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 inPerson.firstName == "Steve"
- Comparison:
<
and>
, as inCocoaPod.rating > 4.5
Those operators are available for optional types, which currently lack the long form.
There are no custom operators for conditions like
Property<T, String>.startsWith()
, as their names are already much more familiar to Swift developers.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.find()
will return all results that matchQuery.count()
will return the count of all results that matchQuery.findUnique()
will return an unique match from the result set; returns nil if no result was found and throws if the result is not unique
In addition to specifying conditions you can order the returned results using the
ordered()
method:let query = try userBox.query { User.firstName == "Joe" }
.ordered(by: User.lastName) // in ascending order, ignoring case
.build()
You can also pass flags to
ordered(by:,flags:)
to sort in descending order, to sort case sensitively or to treat null values specially . For example, to sort the above results in descending order and case sensitively instead:let query = try userBox.query { User.firstName == "Joe" }
.ordered(by: User.lastName, flags: [.descending, .caseSensitive])
.build()
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 every time.To do that, you use the query's
setParameter()
methods. Refer to the Query API docs for a comprenhesive list of permitted setParameter()
variants.There are many 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 usingpersonBox.query { Person.age == 21 }.build()
will effectively change the condition toPerson.age == 18
query.setParameters(Person.age, to: 10, and: 18)
for a query you built usingpersonBox.query { Person.age.isBetween(0, and: 99) }.build()
will effectively change the condition toPerson.age.isBetween(10, and: 18)
You will get fatal errors if you try to call the plural-s-Version when the original condition only has one comparison value, and vice versa. Also note that this is an usage error and thus does not throw.
One problem of the property-based approach above is that you have no control what happens when there are two or more conditions on the same property:
let query = try personBox.query {
Person.age > 10
&& Person.age < 50
}.build()
query.setParameter(Person.age, to: 25) // did this affect "<" or ">" ?
To resolve this ambiguity, 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 = try personBox.query {
"MinAge" .= Person.age > 10
&& "MaxAge" .= Person.age < 50
}.build()
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.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> = try personBox.query { Person.firstName.startsWith("S") }.build()
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.Currently, property queries do not honor the
ordered(by:,flags:)
setting. If you need a fixed order for your proerty query results, you must sort them manually after retrieving them.A simple aggregate function is to request the maximum, minimum, or average value of a given property.
let query: Query<Person> = try personBox.query().build()
let agePropertyQuery: PropertyQuery<Person, Int> = query.property(Person.age)
let maxAge: Int = try agePropertyQuery.max()
let minAge: Int = try agePropertyQuery.min()
let averageAge: Double = try agePropertyQuery.average()
Number-based operations are only available for number-based properties and not for strings, for example. Also note that the average of a property is always a floating point number, even if the property is an
Int
(what would you do with a non-fractional average anyway?).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> = try personBox.query { Person.firstName == "Steve" }.build()
let steveLastNamePQ: PropertyQuery<Person, String> = query.property(Person.lastName)
let allOfStevesLastNames: [String] = try 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.
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] = try personBox.query().build()
.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:
do {
let singleNameResult: String = try personBox.query { Person.age > 999 }.build()
.property(Person.firstName)
.findUniqueString()
} catch ObjectBoxError.uniqueViolation(let message) {
print("Found more than one item! \(message)")
}
guard let singleNameResult = singleNameResult else {
fatalError("No result found at all.")
}
You can combine
distinct()
with findUnique()
.If your entity contains a relation to other entities, you can apply additional criteria to the related entities by using the
link(_ property:, conditions:)
method on your query.E.g. if you had an entity
Order
that has a property ToOne<Order, Customer>
pointing to the customer that placed the order, you could select all orders belonging to a particular customer from today's orders usinglet sallysOrders = try orderBox.query{
Order.date.isAfter(thisMorning)
}.link(Order.customer) {
Customer.name == "Sally Sparrow"
}.build().find()
This is equivalent to what other databases call a
join
.Let's assume we have this interesting Entity:
class Person: Entity {
var id: Id = 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, Void> { /* ... */ }
static var firstName: Property<Person, String, Void> { /* ... */ }
static var lastName: Property<Person, String, Void> { /* ... */ }
static var age: Property<Person, Int, Void> { /* ... */ }
}
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. This is what you use for queries.
The associated types of the generic
Property<Entity, Value, ReferencedType>
itself encode so much interesting information already to make the query interface very intuitive, for example: Property<E, String, R>
exposes methods that make sense for strings, likecontains
Property<E, Int, R>
exposes methods that make sense for numbers, including comparisonsProperty<E, Date, R>
exposes methods for dates, likeisBetween(_: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.Last modified 3yr ago