1

I am working on my first Firebase project using AngularFire2. Below is the overall design of my learning project.

  • Users uploads photos and it's stored in the Firebase storage as images.
  • The uploaded photos are listed in the homepage sorted based on timestamp.

Below is the structure that I have now when I started. But I feel difficulty when doing joins. I should be able to get user details for each uploads and able to sort uploads by timestamp.

User:
- Name
- Email
- Avatar

Uploads:
  - ImageURL
  - User ID
  - Time

I read few blogs de-normalising the data structure. For my given scenario, how best can i re-model my database structure?

Any example for creating some sample data in the new proposed solution will be great for my understanding.

Once the image upload is done, I am calling the below code to create an entry in the database.

addUpload(image: any): firebase.Promise<any> {
  return firebase.database().ref('/userUploads').push({
    user: firebase.auth().currentUser.uid,
    image: image,
    time: new Date().getTime()
  });
}

I am trying to join 2 entities as below. i am not sure how can I do it efficiently and correctly.

 getUploads(): any {
    const rootDef = this.db.database.ref();
    const uploads = rootDef.child('userUploads').orderByChild('time');

    uploads.on('child_added',snap => {
      let userRef =rootDef.child('userProfile/' + snap.child('user').val());
      userRef.once('value').then(userSnap => {
        ???? HOW TO HANDLE HERE
      });
    });

return ?????;
}

I would like to get a final list having all upload details and its corresponding user data for each upload.

Purus
  • 5,701
  • 9
  • 50
  • 89
  • The structure looks fine for a basic image sharing app. What specific problem do you have? If it's a problem in the implementation, share the [minimal code that reproduces where you are stuck](http://stackoverflow.com/help/mcve). – Frank van Puffelen May 27 '17 at 19:23
  • @FrankvanPuffelen : I have added the code that I am using. I am not sure how can I structure and use it in code to add and list.. – Purus May 28 '17 at 05:15

2 Answers2

2

This type of join will always be tricky if you write it from scratch. But I'll try to walk you through it. I'm using this JSON for my answer:

{
  uploads: {
    Upload1: {
      uid: "uid1",
      url: "https://firebase.com"
    },
    Upload2: {
      uid: "uid2",
      url: "https://google.com"
    }
  },
  users: {
    uid1: {
      name: "Purus"
    },
    uid2: {
      name: "Frank"
    }
  }
}

We're taking a three-stepped approach here:

  1. Load the data from uploads
  2. Load the users for that data from users
  3. Join the user data to the upload data

1. Load the data uploads

Your code is trying to return a value. Since the data is loaded from Firebase asynchronously, it won't be available yet when your return statement executes. That gives you two options:

  1. Pass in a callback to getUploads() that you then call when the data has loaded.
  2. Return a promise from getUploads() that resolves when the data has loaded.

I'm going to use promises here, since the code is already difficult enough.

function getUploads() {
  return ref.child("uploads").once("value").then((snap) => {
    return snap.val();
  });
}

This should be fairly readable: we load all uploads and, once they are loaded, we return the value.

getUploads().then((uploads) => console.log(uploads));

Will print:

{
  Upload1 {
    uid: "uid1",
    url: "https://firebase.com"
  },
  Upload2 {
    uid: "uid2",
    url: "https://google.com"
  }
}

2. Load the users for that data from users

Now in the next step, we're going to be loading the user for each upload. For this step we're not returning the uploads anymore, just the user node for each upload:

function getUploads() {
  return ref.child("uploads").once("value").then((snap) => {
    var promises = [];
    snap.forEach((uploadSnap) => {
      promises.push(
         ref.child("users").child(uploadSnap.val().uid).once("value")
      );
    });
    return Promise.all(promises).then((userSnaps) => {
      return userSnaps.map((userSnap) => userSnap.val());
    });
  });
}

You can see that we loop over the uploads and create a promise for loading the user for that upload. Then we return Promise.all(), which ensures its then() only gets called once all users are loaded.

Now calling

getUploads().then((uploads) => console.log(uploads));

Prints:

[{
  name: "Purus"
}, {
  name: "Frank"
}]

So we get an array of users, one for each upload. Note that if the same user had posted multiple uploads, you'd get that user multiple times in this array. In a real production app you'd want to de-duplicate the users. But this answer is already getting long enough, so I'm leaving that as an exercise for the reader...

3. Join the user data to the upload data

The final step is to take the data from the two previous steps and joining it together.

function getUploads() {
  return ref.child("uploads").once("value").then((snap) => {
    var promises = [];
    snap.forEach((uploadSnap) => {
      promises.push(
         ref.child("users").child(uploadSnap.val().uid).once("value")
      );
    });
    return Promise.all(promises).then((userSnaps) => {
      var uploads = [];
      var i=0;
      snap.forEach((uploadSnap) => {
        var upload = uploadSnap.val();
        upload.username = userSnaps[i++].val().name;
        uploads.push(upload);
      });
      return uploads;
    });
  });
}

You'll see we added a then() to the Promise.all() call, which gets invoked after all users have loaded. At that point we have both the users and their uploads, so we can join them together. And since we loaded the users in the same order as the uploads, we can just join them by their index (i). Once you de-duplicate the users this will be a bit trickier.

Now if you call the code with:

getUploads().then((uploads) => console.log(uploads));

It prints:

[{
  uid: "uid1",
  url: "https://firebase.com",
  username: "Purus"
}, {
  uid: "uid2",
  url: "https://google.com",
  username: "Frank"
}]

The array of uploads with the name of the user who created that upload.

The working code for each step is in https://jsbin.com/noyemof/edit?js,console

Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
  • It works and I understand the logic. But as you mentioned, I am getting 2 console logs every time. I am not sure how to remove the duplicates in user table as you have asked me to try that. I tried removing using normal array filtering. but it didnot work. – Purus May 28 '17 at 15:58
  • Any tips would be helpful on removing duplicates. – Purus May 28 '17 at 16:00
  • I get two set of outputs in the console.. 7 image uploads and user details are printed twice. I am not sure what i am missing.. Is this due to duplicate users? – Purus May 28 '17 at 16:26
  • When we restrict the promise to have only unique user, how can we match based on index? It will fail as the index numbers and size does not match... – Purus May 28 '17 at 16:35
  • So in that case you will have to match the users by their UID. So keep a list of the users you've already loaded and only load users that are not in that list. – Frank van Puffelen May 28 '17 at 17:34
  • Thanks for your response. This is too complicated to accomplish a very basic thing. – Purus May 29 '17 at 13:08
  • I have posted the code which works for me. Can you please review if that will be efficient? – Purus May 29 '17 at 13:59
0

I did the following based on Franks answer and it works. I am not sure if this is the best way for dealing with large number of data.

getUploads() {

    return new Promise((resolve, reject) => {
      const rootDef = this.db.database.ref();
      const uploadsRef = rootDef.child('userUploads').orderByChild('time');
      const userRef = rootDef.child("userProfile");
      var uploads = [];

      uploadsRef.once("value").then((uploadSnaps) => {

        uploadSnaps.forEach((uploadSnap) => {

          var upload = uploadSnap.val();

          userRef.child(uploadSnap.val().user).once("value").then((userSnap) => {
            upload.displayName = userSnap.val().displayName;
            upload.avatar = userSnap.val().avatar;
            uploads.push(upload);
          });
        });

      });

      resolve(uploads);
    });

}
Purus
  • 5,701
  • 9
  • 50
  • 89
  • This doesn't deal with asynchronicity yet. By the time you return `uploads`, it will still be empty. To verify this, put `console.log(uploads.length)` right before the return statement. My code uses promises to handle this situation: you call `then()` on the promise and in that write the code that needs the uploads. – Frank van Puffelen May 29 '17 at 14:12
  • I just added promises to my solution. In terms of performance or dealing with large data, will this be efficient? I would like an expert view from you :) – Purus May 29 '17 at 14:13