1

My data structure is something like the following:

restaurant_owners 
    |
    |owner_id  (a unique ID)
      |
      |restaurant_name
      |email

restaurant_menus 
    |
    |restaurant_name
         |
         |dish_type  (drinks, appetizer, etc...)
             |
             |dish_id  (a unique ID)
                 |
                 |name
                 |
                 |price

The idea of the app is basically to allow "restaurant_owners" to login and manage the menu of their respective restaurant. However I am having problems with the following code: (note that the fetchDish function is called in viewDidLoad)

func fetchDish() {

    var restaurantName: String?

    let uid = FIRAuth.auth()?.currentUser?.uid

    //first time referencing database
    FIRDatabase.database().reference().child("owners").child(uid!).observeSingleEvent(of: .value, with: { (snapshot) in

        if let dictionary = snapshot.value as? [String: AnyObject] {

            DispatchQueue.main.async{
                restaurantName = dictionary["name"] as? String
                print(restaurantName!)
            }
        }
    })

    //second time referencing database
    FIRDatabase.database().reference().child("restaurants").child(restaurantName!).child("appetizer").observe(.childAdded, with: { (snapshot) in

        if let dictionary = snapshot.value as? [String: AnyObject] {
            let dish = Dish()
            dish.setValuesForKeys(dictionary)
            self.dishes.append(dish)

            DispatchQueue.main.async {

                self.tableview.reloadData()
            }
        }

    }, withCancel: nil)
}

What I am trying to do is to retrieve the the name of the restaurant for the current logged in user and store it in the variable "restaurantName". Then when I am referencing the database for the second time I can use this variable inside of .child (e.g.: .child(restaurantName)).

However, when I run this, I get an error saying that the restaurantName (in the database reference) is of value nil. I tried putting in some breakpoints and it seems like the first line of the second database reference is operated before whatever is "within" the first database reference, so basically restaurantName is called before any value is stored in it.

Why is this occurring? How do I work around this problem? Also, what are the best practices to achieve this if I'm doing it completely wrong?

NoSQL is very new to me and I have completely no idea how I should design my data structure. Thanks for the help in advance and please let me know if you need any other information.

UPDATE:

The problem was solved by changing my data structure to what Jay has suggested. The following code is what worked for me: (modified Jay's code a bit)

func fetchOwner() {

    let uid = FIRAuth.auth()?.currentUser?.uid
    let ownersRef = FIRDatabase.database().reference().child("owners")
    ownersRef.child(uid!).observeSingleEvent(of: .value, with: { snapshot in

        if let dict = snapshot.value as? [String: AnyObject] {
            let restaurantID = dict["restaurantID"] as! String
            self.fetchRestaurant(restaurantID: restaurantID)
        }

    }, withCancel: nil)
}

func fetchRestaurant(restaurantID: String) {

    let restaurantsRef = FIRDatabase.database().reference().child("restaurants")
    restaurantsRef.child(restaurantID).child("menu").observe(.childAdded, with: { snapshot in

        if let dictionary = snapshot.value as? [String: AnyObject] {
            let dish = Dish()
            dish.setValuesForKeys(dictionary)
            self.dishes.append(dish)

            DispatchQueue.main.async {
                self.tableView.reloadData()
            }
        }

    }, withCancel: nil)
}
Community
  • 1
  • 1
JustTro11
  • 113
  • 1
  • 9
  • Please remove DispatchQueue.main.async. It's not needed. – Jay Feb 28 '17 at 19:32
  • Are there any specific negative effects of using DispatchQueue.main.async? It noticeably speeds up the time for the table to load information when I put self.tableview.reloadData() in it. – JustTro11 Mar 01 '17 at 09:40
  • In this application it shouldn't make any noticeable difference in performance as the tableView won't be refreshed until the Firebase data is received from the server and the code in the closure is processed. With this case, you're throwing a asynchronous task on the main serial queue which puts the task in order behind whatever is in front of it. i.e. it's not needed as the Firebase function is already asynchronous. – Jay Mar 01 '17 at 16:23

1 Answers1

0

A couple of things:

Firebase is Asynchronous and you have to account for that in your code. As it is in the post, the second Firebase function may execute before the first Firebase function has successfully returned data i.e. restaurantName may be nil when the second call happens.

You should nest your calls (in this use case) to ensure data is valid before working with it. Like this.. and keep reading

let ownersRef = rootRef.child("owners")
let restaurantRef = rootRef.child("restaurants")

func viewDidLoad() {
   fetchOwner("owner uid")
}

func fetchOwner(ownerUid: String) {

    var restaurantName: String?

    let uid = FIRAuth.auth()?.currentUser?.uid
    ownserRef.child(ownerUid).observeSingleEvent(of: .value, with: { snapshot in

        if let dict = snapshot.value as? [String: AnyObject] {
                restaurantId = dict["restaurant_id"] as? String
                fetchRestaurant(restaurantId)
            }
        }
    })
 }

func fetchRestaurant(restaurantId: String) {
    restaurantRef.child(restaurantId).observeSingleEvent(of: .value, with: { snapshot in

        if let dict = snapshot.value as? [String: AnyObject] {
            let restaurantName = dict["name"] as! String
            let menuDict = dict["menu"] as! [String:Any]
            self.dataSourceArray.append(menuDict)
            menuTableView.reloadData()
        }
    }
}

Most importantly, it's almost always best practice to disassociate your key names from the data it contains. In this case, you're using the restaurant name as the key. What if the restaurant name changes or is updated? You can't change a key! The only option is to delete it and re-write it.... and... every node in the database that refers to it.

A better options it to leverage childByAutoId and let Firebase name the nodes for you and keep a child that has the relevant data.

restaurants
    -Yii9sjs9s9k9ksd
       name: "Bobs Big Burger Barn"
       owner: -Y88jsjjdooijisad
       menu:
         -y8u8jh8jajsd
             name: "Belly Buster Burger"
             type: "burger"
             price: "$1M"
         -j8u89joskoko
             name: "Black and Blue Burger"
             type: "burger"
             price: "$9.95"

As you can see, I leveraged childByAutoId to create the key for this restaurant, as well as the items on the menu. I also referenced the owner's uid in the owner node.

In this case, If the Belly Buster Burger changes to the Waist Slimming Burger, we can make one change and it's done and anything that references it is also updated. Same thing with the owner, if the owner changes, then just change the owner uid.

If the restaurant name changes to Tony's Taco Tavern, just change the child node and it's done.

Hope that helps!

edit: Answer to a comment:

To get the string (i.e. the 'key' of a key:value pair) immediately created by .childByAutoId()

    let testRef = ref.child("test").childByAutoId()
    let key = testRef.key
    print(key)
Jay
  • 34,438
  • 18
  • 52
  • 81
  • Thanks a lot for the help Jay. Really appreciate it. – JustTro11 Mar 01 '17 at 09:41
  • follow up question: is there a way to immediately store the string generated by .childByAutoId so I can use it in a following reference? Or would I have to do this nested functions again? I know a quick work around is to use NSUUID().uuidString, but it just doesn't seem elegant enough. – JustTro11 Mar 01 '17 at 09:45
  • @JustTro11 sure! Super simple! I added some code to the tail end of my answer. – Jay Mar 01 '17 at 16:31
  • @Jay Could you please help me out with an answer. I need to access another firebase database from within a project which is already linked to a firebase project and I don't know where to go from here or if my idea of having to databases for 2 ios apps is even a good one. https://stackoverflow.com/questions/44866220 . – bibscy Jul 02 '17 at 13:06