In the previous guide we detailed how we can perform miscellaneous queries on a Back4App Database. In this guide we focus on a specific type of query that involves objects with relations.
Query relational data stored on a Back4App Database using the ParseSwift SDK.
1 - Quick review about the Query<U>class
Any query performed on a Back4App Database is done via the generic class Query<U>. The generic parameter U (conforming to the ParseObject protocol) is the data type of the objects we are trying to retrieve from the database.
Given a data type like MyObject, we retrieve these objects from a Back4App Database in the following way
Swift
1 import ParseSwift
23 struct MyObject: ParseObject {4...5}67 let query = MyObject.query()89// Executes the query asynchronously10 query.find { result in11// Handle the result (of type Result<[MyObject], ParseError>)12}
Before we begin to execute queries, it is necessary to set up some data on a Back4App Database. We will store five types of objects:
Author
Book
ISBD
Publisher
BookStore
1 struct Author: ParseObject {2...3 var name: String?4}
Additionally, in order to construct queries for relational data, we will implement the following relations
1:1 relation between Book and ISBD.
1:N relation between Book and Publisher.
M:N relation between Book and Author.
M:N relation between BookStore and Book.
We now proceed to store some data on the Back4App Database. This step can be implemented using Swift or directly from your app’s console on the Back4App platform.
Swift
Back4App's console
//A quick way to insert elements on your Back4App Database is via the console located in your App’s API section. Once you are there, you can start running Javascript code to save the sample data1// Authors2 const aaronWriter =newParse.Object('Author');3 aaronWriter.set('name','Aaron Writer');4 await aaronWriter.save();56 const beatriceNovelist =newParse.Object('Author');7 beatriceNovelist.set('name','Beatrice Novelist');8 await beatriceNovelist.save();910 const caseyColumnist =newParse.Object('Author');11 caseyColumnist.set('name','Casey Columnist');12 await caseyColumnist.save();1314// Publishers15 const acaciaPublishings =newParse.Object('Publisher');16 acaciaPublishings.set('name','Acacia Publishings');17 await acaciaPublishings.save();1819 const birchDistributions =newParse.Object('Publisher');20 birchDistributions.set('name','Birch Distributions');21 await birchDistributions.save();2223// Books with their corresponding ISBD24 const aLoveStoryISBD =newParse.Object('ISBD');25 aLoveStoryISBD.set('isbn','9781401211868');26 await aLoveStoryISBD.save();2728 const aLoveStoryBook =newParse.Object('Book');29 aLoveStoryBook.set('title','A Love Story');30 aLoveStoryBook.set('publisher', acaciaPublishings);31 aLoveStoryBook.set('publishingDate',newDate('05/07/1998'));32 aLoveStoryBook.set('isbd', aLoveStoryISBD);33 const bookARelation = aLoveStoryBook.relation("authors");34 bookARelation.add(aaronWriter);35 await aLoveStoryBook.save();36 aLoveStoryISBD.set('book', aLoveStoryBook.toPointer());37 await aLoveStoryISBD.save();3839 const benevolentElvesISBD =newParse.Object('ISBD');40 benevolentElvesISBD.set('isbn','9781401211868');41 await benevolentElvesISBD.save();4243 const benevolentElvesBook =newParse.Object('Book');44 benevolentElvesBook.set('title','Benevolent Elves');45 benevolentElvesBook.set('publisher', birchDistributions);46 benevolentElvesBook.set('publishingDate',newDate('11/31/2008'));47 benevolentElvesBook.set('isbd', benevolentElvesISBD);48 const bookBRelation = benevolentElvesBook.relation("authors");49 bookBRelation.add(beatriceNovelist);50 await benevolentElvesBook.save();51 benevolentElvesISBD.set('book', benevolentElvesBook.toPointer());52 await benevolentElvesISBD.save();5354 const canYouBelieveItISBD =newParse.Object('ISBD');55 canYouBelieveItISBD.set('isbn','9781401211868');56 await canYouBelieveItISBD.save();5758 const canYouBelieveItBook =newParse.Object('Book');59 canYouBelieveItBook.set('title','Can You Believe It?');60 canYouBelieveItBook.set('publisher', birchDistributions);61 canYouBelieveItBook.set('publishingDate',newDate('08/21/2018'));62 canYouBelieveItBook.set('isbd', canYouBelieveItISBD);63 const bookCRelation = canYouBelieveItBook.relation("authors");64 bookCRelation.add(aaronWriter);65 bookCRelation.add(caseyColumnist);66 await canYouBelieveItBook.save();67 canYouBelieveItISBD.set('book', canYouBelieveItBook.toPointer());68 await canYouBelieveItISBD.save();6970// Book store71 const booksOfLoveStore =newParse.Object('BookStore');72 booksOfLoveStore.set('name','Books of Love');73 const bookStoreARelation = booksOfLoveStore.relation("books");74 bookStoreARelation.add(aLoveStoryBook);75 await booksOfLoveStore.save();7677 const fantasyBooksStore =newParse.Object('BookStore');78 fantasyBooksStore.set('name','Fantasy Books');79 const bookStoreBRelation = fantasyBooksStore.relation("books");80 bookStoreBRelation.add(benevolentElvesBook);81 await fantasyBooksStore.save();8283 const generalBooksStore =newParse.Object('BookStore');84 generalBooksStore.set('name','General Books');85 const bookStoreCRelation = generalBooksStore.relation("books");86 bookStoreCRelation.add(aLoveStoryBook);87 bookStoreCRelation.add(canYouBelieveItBook);88 await generalBooksStore.save();
3 - Query the data
Once the database has some sample data to work with, we start executing the different kinds of queries associated with the relations detailed earlier.
Queries involving 1:1 relations
Given two data types sharing a 1:1 relation (Book and ISBD in this case), we can retrieve one from the other as follows. The way we implemented the relation in Book allows us to retrieve its related ISBD object simply by calling the include(_:) method on the query. Let us retrieve the ISBD from the book A Love Story:
Swift
1 let aLoveStoryBookQuery = Book.query("title"=="A Love Story").include("isbd")// Note how we include the ISBD with the include(_:) method23 let book =try? aLoveStoryBookQuery.first()// Retrieves synchronously the book including its ISBD45 aLoveStoryBookQuery.first { result in// Retrieves asynchronously the book including its ISBD6// Handle the result (of type Result<Book, ParseError>)7}
On the other hand, a query to retrieve a Book object related to a given ISBD is implemented in the following way. By looking at the implementation of ISBD, we note that the relation is represented by the book property (of type Pointer<book>). This pointer provides a set of methods and properties to retrieve information about the object it points to. In particular, we call the fetch(...) method on the book property to fetch the associated Book
Swift
1 let someISBD: ISBD
23 let book: Book?=try? someISBD.book?.fetch()// Retrieves synchronously the book asscociated to someISBD45 someISBD.book?.fetch { result in// Retrieves asynchronously the book asscociated to someISBD6// Handle the result (of type Result<Book, ParseError>)7}
We should remark that this implemetation for a 1:1 relation is not unique. Depending on your use case, you can implement 1:1 relations in different ways.
Queries involving 1:N relations
In a scenario where we need to query all the books published by a given publisher, we first need to retrieve the publisher. For instance, we first retrieve the data object associated with the publisher Acacia Publishings. Depending on the situation, this procces may vary
Swift
1do{2// Using the object's objectId3 let acaciaPublishings =tryPublisher(objectId:"SOME_OBJECT_ID").fetch()45// Or6// Using a Query7 let acaciaPublishings =try Publisher.query("name"=="Acacia Publishings").first()// Returns (synchronously) the first Publisher with name 'Acacia Publishings'. The constraint is constructed using the == operator provided by the ParseSwift SDK89...// To be completed below10}catch{11// Hanlde the error (of type ParseError)12}
Now that we have access to acaciaPublishings, we can construct the query to retrieve its related books. We proceed to create the query by instantiating a Query<Book> class. In this case, this class is instantiated using the static method query(...) provided by the Book object. The (variadic) arguments for this method are the standard QueryConstraint objects. Therefore, the books we are looking for are retrieved with the following snippet
Swift
1do{2 let acaciaPublishings =try Publisher.query("name"=="Acacia Publishings").first()// Returns the first Publisher with name 'Acacia Publishings'34 let constraint: QueryConstraint =try"publisher"== publisher
5 let query = Book.query(constraint)// Creates the query to retrieve all Book objects where its publisher field equalt to 'acaciaPublishings'67 let books:[Book]=try query.find()// Executes the query synchronously89// books should contain only one element: the book 'A Love Story'10}catch{11// Hanlde the error (of type ParseError)12}
An asynchronous implentation for the above snippet may be written in the following way
Swift
1// We retrieve the Publisher with name 'Acacia Publishings'2 Publisher.query("name"=="Acacia Publishings").first { result in3 switch result {4 case .success(let publisher):5 guard let constraint: QueryConstraint =try?"publisher"== publisher else{fatalError()}67// Then, we retrieve the books with the corresponding constraint 8 Book.query(constraint).find { result in9 switch result {10 case .success(let books):11// books should contain only one element: the book 'A Love Story'12break13 case .failure(let error):14// handle the error (of type ParseError)15break16}17}1819 case .failure(let error):20// handle the error (of type ParseError)21break22}23}
Queries involving M:N relations (Case 1)
To illustrate this case, we consider the following scenario; we want to list all the stores containing books published after a given date (e.g., 01/01/2010). Firstly we require an intermediate query to select the books. Next, we construct the main query to list the stores.
Therefore, we prepare the first query for the books
Swift
1 let booksQuery = Book.query("publishingDate">Date(string:"01/01/2010"))// We construct the date constraint using the > operator provided by the ParseSwift SDK23do{4 let books =try booksQuery.find()5...// To be completed below6}catch let error as ParseError {7// Handle any potential error8}catch{9// Handle any potential error10}
We then construct the stores’ query using booksQuery’s results. The method containedIn(_:array:) returns the constraint we need for this case
Swift
1 let booksQuery = Book.query("publishingDate">Date(string:"01/01/2010"))// We construct the date constraint using the > operator provided by the ParseSwift SDK23do{4 let books =try booksQuery.find()56// Here is where we construct the stores' query with the corresponding constraint7 let storesQuery = BookStore.query(trycontainedIn(key:"books", array: books))89 let stores =try storesQuery.find()1011// stores should containt only one element: the 'General Books' BookStore 12}catch let error as ParseError {13// Handle any potential error14}catch{15// Handle any potential error16}
Similarly, we can implement this process asynchronously
Swift
1 let booksQuery = Book.query("publishingDate">Date(string:"01/01/2010"))// We construct the date constraint using the > operator provided by the ParseSwift SDK23 booksQuery.find { result in4 switch result {5 case .success(let books):6 guard let constraint =try?containedIn(key:"books", array: books)else{fatalError()}7 let storesQuery = BookStore.query(constraint)89 storesQuery.find { result in10 switch result {11 case .success(let stores):1213 case .failure(let error):14// Handle the error (of type ParseError)15}16}17 case .failure(let error):18// Handle the error (of type ParseError)19}20}
Queries involving M:N relations (Case 2)
Suppose we need to select all the stores that have books written by a given author, say, Aaron Writer. In order to achieve this, we require two additional queries:
A query (Query<Author>) to obtain the object associated with the author Aaron Writer.
A query (Query<Book>)to select all the books written by Aaron Writer.
The main query (Query<BookStore>) to select the stores we are looking for.
The procedure to implement these queries are very similar to the previous ones:
Swift
1 let authorQuery = Author.query("name"=="Aaron Writer")// The first query to retrieve the data object associated to 'Aaron Writer'23do{4 let aaronWriter =try authorQuery.first()56 let booksQuery = Book.query(trycontainedIn(key:"authors", array:[aaronWriter]))// The second query to retrieve the books written by 'Aaron Writer'78 let books =try booksQuery.find()910 let storesQuery = BookStore.query(trycontainedIn(key:"books", array: books))// The main query to select the stores where the author ('Aaron Writer') has his books available1112 let stores =try storesQuery.find()1314// stores should contain two items: 'Books of Love' and 'General Books'15}catch let error as ParseError {16// Handle the error17}catch{18// Handle the error19}
Conclusion
With the ParseSwift SDK, we were able to construct relational queries that allowed us to select items based on the type of relations they have with other data types.