Query Cookbook
Query Cookbook for ParseSwift
Introduction
In the basic queries guide, we introduced some of the basic methods to construct queries and retrieve data from a Back4App Database. The ParseSwift SDK offers practical ways to construct more complex queries for real-world applications.
In this guide, you will dive deep into the Query<U> (generic) class and see all the methods you can use to build your queries. You will use a simple database class (named Profile) with some mocked data to perform queries using swift.
Before getting started, we should establish a couple of concepts about objects and data types. In this guide, we refer as objects to any data type (like structs) that can be stored in a database. In some situations, they are also referred as items or elements. Properties are the members of a data type (mostly structs). However, in here, we usually call them fields. ParseSwift uses the term ‘key’, to indicate the properties’ name. Thus, any parameter labeled as ‘key’ in any method from the ParseSwift SDK indicates that its value has to match a field’s name.
Prerequisites
To complete this tutorial, you will need:
- A basic iOS App to test queries
Goal
To explore the different methods theQuery<U> class provide to execute queries.
The Query<U> class
For this guide, we will query information about a person’s contact. These data is stored under the class Profile on the Back4App Database. In order to retrieve profiles via an iOS App, we need to create the Profile struct that will contain such information
1 import ParseSwift
2 ...
3
4 struct Profile: ParseObject {
5 ...
6
7 // Custom properties to organize the person's information
8 var name: String?
9 var birthday: Date?
10 var numberOfFriends: Int?
11 var favoriteFoods: [String]?
12 var luckyNumbers: [Int]?
13 var lastLoginLocation: ParseGeoPoint? // A data type to store coordinates for geolocation
14 var isActive: Bool?
15 var membership: Pointer<Membership>? // More details about Membership below
16
17 ...
18 }Additionally, we use the object Membership to show how queries between Profile and Membership interact with each other. The Membership struct is implemented in the following way
1 import ParseSwift
2 ...
3
4 struct Membership: ParseObject {
5 ...
6
7 // Custom properties to organize the membership's information
8 var typeDescription: String?
9 var expirationDate: Date?
10
11 ...
12 }Any query operation on a Back4App Database is performed by the generic class Query<Profile> where the generic type Profile (conforming to the ParseSwift protocol) is the object we are trying to retrieve. After instantiating a Query<Profile> object, we must call one of its find(_) methods (see the basic queries guide) to handle the result returned from the query.
You can read more about the Query<U> class here at the official documentation.
Saving Data Objects
With the following snippet we create and save some data to test miscellaneous queries
1 let adamSandler = Profile(
2 name: "Adam Sandler",
3 birthday: Date(string: "09/09/1996"),
4 numberOfFriends: 2,
5 favoriteFoods: ["Lobster", "Bread"],
6 luckyNumbers: [2, 7],
7 lastLoginLocation: try? ParseGeoPoint(latitude: 37.38412167489413, longitude: -122.01268034622319),
8 isActive: false,
9 membership: nil
10 )
11
12 let britneySpears = Profile(
13 name: "Britney Spears",
14 birthday: Date(string: "12/02/1981"),
15 numberOfFriends: 52,
16 favoriteFoods: ["Cake", "Bread"],
17 luckyNumbers: [22, 7],
18 lastLoginLocation: try? ParseGeoPoint(latitude: 37.38412167489413, longitude: -122.01268034622319),
19 isActive: true,
20 membership: nil
21 )
22
23 let carsonKressley = Profile(
24 name: "Carson Kressley",
25 birthday: Date(string: "03/27/1975"),
26 numberOfFriends: 12,
27 favoriteFoods: ["Fish", "Cookies"],
28 luckyNumbers: [8, 7],
29 lastLoginLocation: try? ParseGeoPoint(latitude: 37.38412167489413, longitude: -122.01268034622319),
30 isActive: true,
31 membership: nil
32 )
33
34 // We create and save a Membership object
35 let premiumMembership = Membership(typeDescription: "Premium", expirationDate: Date(string: "10/10/2030"))
36 let savedPremiumMembership = try! premiumMembership.save() // Careful with the forced unwrap
37
38 let danAykroyd = Profile(
39 name: "Dan Aykroyd",
40 birthday: Date(string: "07/01/1952"),
41 numberOfFriends: 66,
42 favoriteFoods: ["Jam", "Peanut Butter"],
43 luckyNumbers: [22, 77],
44 lastLoginLocation: try? ParseGeoPoint(latitude: 37.38412167489413, longitude: -122.01268034622319),
45 isActive: true,
46 membership: try? .init(savedPremiumMembership)
47 )
48
49 let eddyMurphy = Profile(
50 name: "Eddie Murphy",
51 birthday: Date(string: "04/03/1961"),
52 numberOfFriends: 49,
53 favoriteFoods: ["Lettuce", "Pepper"],
54 luckyNumbers: [6, 5],
55 lastLoginLocation: try? ParseGeoPoint(latitude: -27.104919974838154, longitude: -52.61428045237739),
56 isActive: false,
57 membership: nil
58 )
59
60 let fergie = Profile(
61 name: "Fergie",
62 birthday: Date(string: "03/27/1975"),
63 numberOfFriends: 55,
64 favoriteFoods: ["Lobster", "Shrimp"],
65 luckyNumbers: [12, 7],
66 lastLoginLocation: try? ParseGeoPoint(latitude: -27.104919974838154, longitude: -52.61428045237739),
67 isActive: false,
68 membership: nil
69 )
70
71 do {
72 _ = try? adamSandler.save()
73 _ = try? britneySpears.save()
74 _ = try? carsonKressley.save()
75 _ = try? danAykroyd.save()
76 _ = try? eddyMurphy.save()
77 } catch {
78 // Handle the errors here
79 }Once the above code is executed, we should have a small database to work with.
See more about how to save data with ParseSwift at iOS Data Objects.
Query retrievers
These methods are responsible for running the query and retrieving its results, being always present in your query implementation.
//This is the basic method for retrieving your query results
1 let query = Profile.query() // Instantiate a Query<Profile> class with no additional constraints
2
3 let profiles = try? query.find() // Executes the query synchronously returns and array of Profiles. It throws an error if something happened
4
5 query.find { result in // Executes the query asynchronously
6 // Handle the result (of type Result<[Profile], ParseError>)
7 }Queries with constraints on string type fields
We now start applying specific constraints to queries. Firstly, we will impose constraints that select objects only when a String type field satisfies a given condition. This and any other type of constraint imposed on a Query<Profile> object is done via the QueryConstraint object. Below and in the next sections we detail how to construct such constraints.
//Used when we need to select all the objects which have a field equal to a given String. We use the == operator to construct the corresponding constraint. On the left-hand side goes the field’s name and on the right hand side goes the String to compare against
1 let constraint: QueryConstraint = "name" == "Fergie" // Selects all Profiles with name equals to Fergie
2 let query = Profile.query(constraint)
3
4 let profiles = try? query.find() // Executes the query synchronously
5
6 query.find { result in // Executes the query asynchronously
7 // Handle the result (of type Result<[Profile], ParseError>)
8 }Queries with constraints on comparable-like fields
It is very common to try to select objects where a certain number type field has to be equal, distinct, larger or smaller than a given value. This is accomplished by using the following comparable operations
//Use this constraint when you need to select objects where a certain number type field is equal to a given value.
1 let constraint: QueryConstraint = "numberOfFriends" == 2 // Selects all Profiles with number of friends = 2
2 let query = Profile.query(constraint)
3
4 let profiles = try? query.find() // Executes the query synchronously
5
6 query.find { result in // Executes the query asynchronously
7 // Handle the result (of type Result<[Profile], ParseError>)
8 }These constraints also work on Date type fields. For instance, we can have
1 let constraint: QueryConstraint = "birthday" > Date(string: "08/19/1980") // Selects all Profiles who were born after 08/19/1980
2 let query = Profile.query(constraint)
3
4 let profiles = try? query.find() // Executes the query synchronously
5
6 query.find { result in // Executes the query asynchronously
7 // Handle the result (of type Result<[Profile], ParseError>)
8 }For Bool type fields, we have the Equal and Not equal options. For instance
1 let constraint: QueryConstraint = "isActive" == true
2 let query = Profile.query(constraint) // Selects all active Profiles
3
4 let profiles = try? query.find() // Executes the query synchronously
5
6 query.find { result in // Executes the query asynchronously
7 // Handle the result (of type Result<[Profile], ParseError>)
8 }Queries with constraints involving geolocation
For fields containing location data (i.e., of type ParseGeoPoint) we have the following query options.
//In order to select objects where one of their ParseGeoPoint type fields is near to another given geopoint, we use
1 guard let geopoint = try? ParseGeoPoint(latitude: 37.38412167489413, longitude: -122.01268034622319) else {
2 return
3 }
4
5 let query = Profile.query(near(key: "lastLoginLocation", geoPoint: geopoint))
6 // near(key:geoPoint:) is a method provided by the ParseSwift SDK
7
8 let profiles = try? query.find() // Executes the query synchronously
9
10 query.find { result in // Executes the query asynchronously
11 // Handle the result (of type Result<[Profile], ParseError>)
12 }Queries with constraints involving array type fields
When the objects we want to retrieve need to satisfy a given condition on any of their array type fields, ParseSwift SDK provides the following alternatives to accomplish that
//Add a constraint to the query that requires a particular field to be contained in the provided array.
1 let constraint: QueryConstraint = containedIn(key: "luckyNumbers", array: [2, 7])
2 // containedBy(key:array:) is a method provided by the ParseSwift SDK
3
4 let query = Profile.query(constraint)
5
6 let profiles = try? query.find() // Executes the query synchronously
7
8 query.find { result in // Executes the query asynchronously
9 // Handle the result (of type Result<[Profile], ParseError>)
10 }Advanced queries
Until now we introduced the main conditions we can apply on a given field to select objects from a Back4App Database using ParseSwift SDK. In this section we continue with the composition of queries and advanced queries.
//It is used to select objects depending on the value of a given field not being nil.
1 let constraint: QueryConstraint = exists(key: "membership")
2 // exists(key:) is a method provided by the ParseSwift SDK
3
4 let query = Profile.query(constraint) // Will retrieve all Profiles which have a membership
5
6 let profiles = try? query.find() // Executes the query synchronously
7
8 query.find { result in // Executes the query asynchronously
9 // Handle the result (of type Result<[Profile], ParseError>)
10 }Query ordering
Sorting the results from a query is key before starting to display or manipulate those results. The ParseSwift SDK provides the following options to accomplish that.
//This kind of sorting procedure relies on a score metric. In order to use this option, the Profile object must conform to the ParseQueryScorable protocol. Once the protocol is implemented, we call the sortByTextScore() method on the Query<Profile> object to obtain a new one that implements the required sorting method.
1 extension Profile: ParseQueryScorable {
2 var score: Double? {
3 // Add the corresponding implementation
4 }
5 }
6
7 let query = Profile.query().sortByTextScore()
8
9 let profiles = try? query.find() // Executes the query synchronously
10
11 query.find { result in // Executes the query asynchronously
12 // Handle the result (of type Result<[Profile], ParseError>)
13 }Field selecting
Depending on what data is required during a query, this can take more or less time. In some scenarios it may be enough to retrieve certain fields from an object and ignore the unnecessary fields. Furthermore, by selecting only the fields we need from a query, we avoid over-fetching and improve performance on the fetching process.
//Calling the exclude(_:) method on a query returns a new query that will exclude the fields passed (in variadic format) as the argument.
1 let query = Profile.query().exclude("name", "numberOfFriends")
2
3 let profiles = try? query.find() // Executes the query synchronously
4
5 query.find { result in // Executes the query asynchronously
6 // Handle the result (of type Result<[Profile], ParseError>)
7 }Pagination
In large Databases pagination, is a fundamental feature for querying and handling a large amount of results. the ParseSwift SDK provides the following methods to manage those situations.
//Determines the maximum number of results a query is able to return, the default value is 100. We call the limit(_:) method on the query in question to change this value.
1 let query = Profile.query().limit(2)
2
3 let profiles = try? query.find() // Executes the query synchronously
4
5 query.find { result in // Executes the query asynchronously
6 // Handle the result (of type Result<[Profile], ParseError>)
7 }Compound queries
These methods will create compound queries, which can combine more than one Query<Profile> instance to achieve more complex results.
//Compose a compound query that is the AND of the passed queries. This is accomplished by first instantiating the queries to be used for the AND operation. The and(queries:) method provided by the ParseSwift SDK allows us to perform the AND operation and embed the result in a QueryConstraint object. This constraint is then used to instantiate the final query to be used for retrieveing the results.
1 let query1 = Profile.query("numberOfFriends" > 10)
2 let query2 = Profile.query("numberOfFriends" < 50)
3
4 let query = Profile.query(and(queries: query1, query2))
5 // and(queries:) is a method provided by the ParseSwift SDK. It takes an array of queries and applies the AND op. on them.
6
7 let profiles = try? query.find() // Executes the query synchronously
8
9 query.find { result in // Executes the query asynchronously
10 // Handle the result (of type Result<[Profile], ParseError>)
11 }Database related
These methods are related to the database preferences and operations.
//Executes an aggregate query, retrieving objects over a set of input values. To use this feature, instead of executing the query with find(...), we call the aggregate(_:completion:). The first argument of this function is a dictionary containing the information about the pipeline. For more details, refer to MongoDB documentation on aggregate.
1 let query = Profile.query()
2
3 query.aggregate([["pipeline": 5]]) { result in
4 // Handle the result (of type Result<[Profile], ParseError>)
5 }Conclusion
Using the different methods, objects and operators provided by the ParseSwift SDK, we were able to understand how to construct and retrieve objects from a Back4App Database. With this cookbook you should be able to perform queries with very flexible constraints and manage the results according to your use case. For more details about any of the above topics, you can refer to the ParseSwift repository.