'Firebase data structure query

After reading the documentation they recommend not using nested data structure. I am thinking this is the best way for my data but I am not sure how to add the tripId to the hotels collection.

My other option is having the user enter the "BOL" for each hotel entry... My problem with that method is how do I query the date to see all the hotels that match the "trip"?

EDIT:::::::: What I am trying to accomplish is when a user selects a trip it runs a query and displays the total of all the nights in a hotel associated with that trip.

users:
    - gCFiwSLwBeg87N6GfeofFxIAZyi2
        - UID: gCFiwSLwBeg87N6GfeofFxIAZyi2
        - firstName: "James"
        - email: "[email protected]"

trips:
    - "random document id"
        - bol: "1234567"
        - startDate: "1/3/2022"
        - destination: "Sacramento, CA"
        - userID: "Users UID"
        - foodTotal: "100"
        - hotelTotal: "240"
    - "random document id"
        - bol: "256976"
        - startDate: "2/5/2022"
        - destination: "Orlando, FL"
        - userID: "Users UID"
        - foodTotal: "150"
        - hotelTotal: "400"

hotels:
    - "random document id"
        - tripId: ?
        - date: "1/4/2022"
        - destination: "Sacramento, CA"
        - userID: "Users UID"
        - name: "Hotel Inn"
        - cost: "120"
    - "random document id"
        - tripId: ?
        - date: "1/5/2022"
        - destination: "Sacramento, CA"
        - userID: "Users UID"
        - name: "Hotel Inn"
        - cost: "120"

Here is the function I am currently using to add Hotels to firebase.

class FirebaseHotelRepository: HotelRepositoryProtocol {
    private let db = Firestore.firestore()

func addHotel(hotel: Hotel, completion: @escaping (Result<Hotel?, Error>) -> Void) {
        do {
            var addedHotel = hotel
            addedHotel.userId = Auth.auth().currentUser?.uid
            let _ = try db.collection("hotels").addDocument(from: addedHotel)
        } catch {
            fatalError("Unable to encode trip: \(error.localizedDescription)")
        }
    }
}

class AddHotelViewModel: ObservableObject {
    private let repo: HotelRepositoryProtocol

    var name: String = ""
    var location: String = ""
    var date = Date()
    var cost: String = ""

    @Published var saved: Bool = false
    
    init(repo: HotelRepositoryProtocol) {
        self.repo = repo
    }
    
    func addHotel() {
        let hotel = Hotel(bol: bol,
                          name: name,
                          location: location,
                          date: date,
                          cost: cost,
                          color: UIColor(color).hexStringFromColor())
        
        repo.addHotel(hotel: hotel) { result in
            switch result {
            case .success(let savedHotel):
                DispatchQueue.main.async {
                    self.saved = savedHotel == nil ? false : true
                }
            case .failure(let error):
                print(error.localizedDescription)
            }
        }
    }
}


Solution 1:[1]

Let start with clarifying this

not using nested data structure

Everything in Firebase is nested! - that's the way it works. That's in reference to DEEPLY nesting data as it makes it hard to get to/query etc so denormalizing or flattening the data is an often used practice.

There are a number of ways to get the data you want but let me re-state the question

I have trips and each trip has a number of associated hotels. My goal is to read the hotels for each trip to calculate a total cost for those hotels.

TL;DR Skip to the bottom: One Quick Thing section

Here's one structure

trip_0
   trip_name: "Napa trip"
   hotels:
     hotel_0: true
     hotel_1: true
     hotel_2: true

with that structure, reading in trip_0 will provide the associated hotels. You then then iterate over that array (map) and read in hotel_0 and get it's cost, then hotel_1 and get it's cost. Add the costs up and you're done. No query required.

A second option is a reverse approach with a seperate collection that links hotels and trips - this requires a query to get the hotels associated with each trip

hotels_trips
   hotel_0
      trip_0: true
      trip_1: true
      trip_2: true
   hotel_1
      trip_1: true

with the above, run a query on the hotels_trips collection for hotels there trip_1 is true. Once you have the list of hotels you can read each one to get it's cost and add them up.

Another solution is a structure and a query using arrayContains:

hotels
   hotel_0
      hotel_name: "Hotel California"
      cost: 500
      trip_array
         0: trip_0
         1: trip_1

and then the code that will query the hotels collection for all hotels that contains trip_0

func queryHotels() {
    let hotelCollection = self.db.collection("hotels")
    hotelCollection.whereField("trip_array", arrayContains: "trip_0").getDocuments { querySnapshot, error in
        if let err = error {
            print(err.localizedDescription)
            return
        }

        guard let docs = querySnapshot?.documents else { return }

        for doc in docs {
            let name = doc.get("name") as! String
            print(name)
        }
    }
}

One quick thing: Arrays are typically challenging to work with in NoSQL databases like Firebase so I usually avoid them. You may want to consider using a Map instead of an array and then query on the path trip_map/trip_0 to see if it's true. The hotels structure would include this instead of the trip_array

trip_map
   trip_0: true
   trip_1: true

and the query would test the path to see if it's value is true

hotelCollection.whereField("trip_map.trip_0", isEqualTo: true).getDocuments { querySnapshot, error in

Solution 2:[2]

From this documentation:

In some cases, it can be useful to create a document reference with an auto-generated ID, then use the reference later. For this use case, you can call doc():

Here's a sample code that you can refer to:

let trips = db.collection("trips").addDocument(data: [
 // trips' data
])

// This will append tripId to the `addedHotel`
addedHotel["tripId"] = trips.documentID

let hotels = db.collection("hotels").addDocument(from: addedHotel)

The above code should get the document ID of the trips' and you can manually append or construct the data that you should pass to the hotels` document.


You can find more relevant information here.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Jay
Solution 2 Marc Anthony B