Firebase nested queries

Hello together,

i do have the following database structure in firebase:


In general: each country has various clubs and each club has various teams with it’s own information.

I want to get for all clubs in a country all it’s teams with the individual team information. All my attempts failed. I’m driving crazy soon with the firebase database. What’s wrong with my coding or do I understand something fundamentally wrong.

Because I can’t query all these informations at once, i first query all clubs in a country. Within this query - once I have the club informations - I query for the teams in each club. And this query doesn’t work. I never get any documents back. The debugger jumps directly to the return line. Why. Is this a problem of synchronous/async task?

enclosed my code: I skipped all the definitions of the various structs. it’s not helpful anyway …

func getClubsInCountry(country: String) {
        
        // Get a referene to the countries collection
        let db = Firestore.firestore()
        
        db.collection("countries").document("\(country)").collection("clubs")
            .getDocuments() {[self] (querySnapshot, error) in
                if let error = error {
                    print("Error getting documents: \(error)")
                } else {
                    // Declare temporary lists for clubs
                    var clubs = [Club]()
                    for doc in querySnapshot!.documents {
                        var m = Club(id: doc.documentID,
                                     name: doc["name"] as? String ?? "",
                                     FireStorePath: doc["logoUrl"] as? String ?? "",
                                     city: doc["city"] as? String ?? nil,
                                     street: doc["street"] as? String ?? nil,
                                     nr: doc["nr"] as? Int ?? nil,
                                     zip: doc["zip code"] as? Int ?? nil,
                                     web: doc["web"] as? String ?? nil)
                        
                        // load teams which belong to the club
                        Task {
                            let test = try await getTeamsInClub(country: "\(country)", teamId: doc.documentID)
                          //  m.teams = test
                        }
                        
                        clubs.append(m)
                        print("m: \(m)")
                    }
            }
            DispatchQueue.main.async {
                self.clubs = clubs
            }
        }
    }

and the getTeamsInClub function …

  func getTeamsInClub(country: String, teamId: String) async -> [Team] {
        // Get a referene to the countries collection
        let db = Firestore.firestore()
        var teams = [Team]()
        
        let dbPath = db.collection("countries").document("\(country)").collection("clubs").document("\(teamId)").collection("teams")
        print("step 1")
        dbPath.getDocuments() { (queryTeams, error) in
            print("step 2")
            if let error = error {
                print("Error getting documents: \(error)")
            } else {
            
                // Declare temporary lists for teams belonging to the club
                for doc in queryTeams!.documents {
                    var n = Team(id: doc.documentID,
                                 name: doc["name"] as? String ?? "",
                                 short: doc["short"] as? String ?? nil,
                                 sport: doc["sport"] as? String ?? "")
                    teams.append(n)
                }
                
            }
        }
        return teams
    }

when I try to find the error with debugging, I recognized, that the debugger never jumps to the line "print(“step 2”).

Appreciate your help very much!

Thanks, Peter

Hi Peter!

Sorry for the trivial question but, did you first make some prints to make sure you got all the club documents correctly? In your getTeamsInClub did you make a print to look at the teamId argument you’re feeding it? (which is a typo and should instead be a clubId if I understand correctly?)

Also, is there a rationale for having your data nested this way? If your data structure was flattened, for example with a “teams” root collection where each team document has a country code embed as a field, you could easily query for all teams belonging to a country without having to go through the extra step of querying for all clubs in a country, and you’d also avoid yourself a bunch of document reads in the process.

But maybe you need your data structure to be nested this way for some reason I’m not aware of in which case never mind :sweat_smile:

Hy Cal,

you are right, it’s just a type. Of Course it is a clubID. The reason why I went for a sub collection was, that I come from SQL-Databse structure – no double informations stored. If I flatten the data structure, I need a country and club information for each team. Could be possible and dangerous in data management.

Mainly I’m interested how I can solve this coding issue - i don’t understand why firebase doesn’t read the documents in the sub collection. I really want to know why or what I do wrong.

Thanks, Peter

True, but how NoSQL works, they do duplicate data.

This is an older series, but still applicable for how NoSQL and SQL work, and how with NoSQL, you do duplicate data

First, I would not write it this way, with a Task inside of a completion handler. I haven’t personally tried this, so I’m not sure if it’s a “bad practice” to do this, but I wouldn’t.

Also how you have the getTeamsInClub written is wrong. You have a firebase function that uses a completion handler, NOT using async / await syntax. You need to use withCheckedThrowingContinuation at some point, otherwise this function isn’t actually asynchronous with async / await

See this post, for using async / await syntax within older functions.

You should watch this series, for how Firestore works, specifically video 5, about how to structure data.

Because like Cal mentioned you may have more unnecessary read/writes due to how you structure your data, which can end up costing you, literally.

Hey @peter_luger,

I was watching a YouTube playlist about concurrency and had to stop by the end of episode 6 because I think it made me understand what’s wrong with your code and what could be improved.

I think the root of the problem is how you have a for loop (which is synchronous) inside of which you’re trying to call an asynchronous function. Your print(“step 1”) runs but not print(“step 2”) so it looks like Swift runs the synchronous part of your code, but doesn’t want to run the async portion of your code.

I’m guessing you’re aware of this problem, because you tried to put your async function inside a Task. But it seems it’s not the way you should do it. It’s like Swift decides to skip your async call so it can move on to the next iteration of your for loop.

I recommend watching the playlist I mentioned earlier. It features a way to iterate asynchronous calls over an array. (episode 6: TaskGroup)

To convince you of this potential, I tried to put some code together myself based on what I learnt today (as a little challenge to solidify knowledge, also I’m productively procrastinating on working on my own things haha). I think it should work! (I hope)

I can’t test it but it compiles and theoretically it should return an array of tuples of (clubID, [Team]). If it doesn’t well… my bad, but still this should give you ideas as to what you should be doing. I’ll leave it up to you for the error handling and updating each club’s property of teams.

Alright, back to binging the playlist. Cheers!

func getClubs(country: String) async -> [Club] {
        let clubsRef = db.collection("countries").document("\(country)").collection("clubs")

        let snapshot = try? await clubsRef.getDocuments()
        
        if snapshot != nil {
            return snapshot!.documents.compactMap { try? $0.data(as: Club.self) }
        } else {
            return []
        }
       
    }
    
    func getTeamsInClub(country: String, clubID: String) async -> (String, [Team])? {
        let teamsRef = db.collection("countries").document(country).collection("clubs").document(clubID).collection("teams")
        
        let snapshot = try? await teamsRef.getDocuments()
        
        if snapshot != nil {
            return (clubID, snapshot!.documents.compactMap { try? $0.data(as: Team.self) })
        } else {
            return nil
        }
    }
    
    func getAllTeamsForAllClubs(country: String, clubs: [Club]) async throws -> ([(String, [Team])]) {
        
        return try await withThrowingTaskGroup(of: (String, [Team])?.self, body: { group in
            
            var data = [(String, [Team])]()
            
            for club in clubs {
                group.addTask {
                    await self.getTeamsInClub(country: country, clubID: club.id!)
                }
            }
            
            for try await tuple in group {
                if let tuple = tuple {
                    data.append(tuple)
                }
            }
            
            return data
        })
    }