Search code examples
nestedfirebasesubqueryfirebasesimpleloginrest-firebase

Firebase Nested Query


I am new to firebase and have read through the docs, but can't seem to find a away to get a list of all Ladders with populated User data. I don't really want to duplicate the user data in every ladder they are a member of.

Here is my data structure:

{
"ladders" : [ {
    "description" : "Real Real Tennis",
    "name" : "Ping Pong",
    "players" : {
      "simplelogin:5" : true
    }
  }, {
    "description" : "Real Tennis",
    "name" : "Mario Tennis",
    "players" : {
      "simplelogin:5" : true
    }
  } ],
  "users" : {
    "simplelogin:5" : {
      "email" : "[email protected]",
      "md5hash" : "1e737a7b6e1f0afb1d6fef521097400b",
      "name" : "Bob",
      "username" : "bob1"
    }
  }
}

Here is my best attempt, but it looks like the ladders lookup finishes and returns before the users lookup is done so it returns with players being empty.

laddersRef.on('value', function(laddersSnapShot){
        var ladders = []
        laddersSnapShot.forEach(function(ladderData) {
            var ladder = ladderData.val();
            ladder.players = [];
            ladder.id = ladderData.key();

            laddersRef.child(ladder.id).child('players').on('value', function(snap){
                snap.forEach(function(player){
                    usersRef.child(player.key()).on('value', function(snap1){
                        ladder.players.push(snap1.val())
                    })
                })
            })
            ladders.push(ladder)
        });
    });

Solution

  • Most everything about this example is broken. A read through the guide is essential before this question can be answered in a meaningful way. As written, your code establishes a new listener on each ladder's list of players any time any field anywhere in ladders/ is modified.

    This occurs because laddersRef.on('value') will be updated any time there is a change anywhere in that path. Each time this occurs, you then establish laddersRef.child(ladder.id).child('players').on('value'), which creates multiple listeners on each ladder's players/ path. The fundamentals of this are covered in retrieving data

    The correct answer here is to utilize the tools as intended, rather than trying to make a slow, tedious, error-prone and synchronous process out of asynchronous API methods.

    To directly answer your question, this is what you asked for:

    function done(ladders) {
       console.log(ladders);
    }
    
    laddersRef.on('value', function(laddersSnapShot){
      function doneWithLadder() {
         if( ++laddersDone === laddersNeeded ) {
           done(ladders);
         }
      }
    
      var ladders = [];
      var laddersDone = 0;
      var laddersNeeded = laddersSnapShot.numChildren();
    
      laddersSnapShot.forEach(function(ladderData) {
        var ladder = ladderData.val();
        var playersFound = 0;
        var playersNeeded = ladderData.numChildren();
        ladder.players = [];
        ladder.id = ladderData.key();
        laddersSnapshot.child('players').forEach(function(ps) {
           usersRef.child(ps.key()).once('value', function(userSnap) {
               ladder.players.push(usersSnap.val());
               if( ++playersFound === playersNeeded ) {
                  doneWithLadder();
               }
           });
        });
      });
    });
    

    But this is more elegant (and much more elegant would be to use a framework and avoid this entirely):

    function addLadder(snap) {
       putLadderInDom(snap.key(), snap.val());
       snap.ref().child('players').on('value', changePlayers);
       // Use whatever framework you have available to put this into
       // the DOM. Be sure to keep the record key around so you can
       // add the user in later
    }
    
    function changePlayers(playersSnap) {
       var ladderKey = playersSnap.parent().key();
       var numFetched = 0;
       var numPlayersToFetch = playersSnap.numChildren();
       var profiles = {};
       playersSnap.forEach(function(ss) {
          loadProfile(ss.key(), function(userSnap) {
             profiles[userSnap.key()] = userSnap.val();
             if( ++numFetched === numPlayersToFetch ) {
                updatePlayers(ladderKey, profiles);
             }
          });
       });
    }
    
    function updatePlayers(ladderKey, profiles) {
       // use whatever framework you have available to insert profiles
       // into the DOM
    }
    
    function loadProfile(key, callback) {
       usersRef.child(key).once('value', callback);
    }
    
    laddersRef.on('child_added', addLadder);