1

I am developing an application with Firebase (1.0) and Angular (1.4). The problem I'm having is to ensure the data in view are synchronised with Firebase, while fetching denormalised data coming from two tables in Firebase:

The book table looks like this:

books:
    -JyDpkQrUozE3L7flpo: {
    title: "title1",
    author: {-JyDpkQrUozE3L7fl1x: true}
    },
    -JyDpkQrUozE3L7flp1: {
    title: "title2",
    author: {-JyDptrrrezE3L7flKx: true}
    },
    -JyDpkQrUozE3L7flp2: {
    title: "title3",
    author: {-JyDpkQrUozE3L7f222: true}
    }
    

The author table looks like this:

authors:
    -JyDpkQrUozE3L7flKx: {
        firstname: "jacques",
        lastname: "smith"
    },
    -JyDptrrrezE3L7flKx: {
        firstname: "bill",
        lastname: "halley"
    },
    -JyDpkQrUozE3L7f222: {
        firstname: "john",
        lastname: "ford"
    }
    

My controller looks like this:

.controller('booksController', ['$scope', '$firebaseArray', function($scope, $firebaseArray) {
     var ref = new Firebase("https://[PATH].firebaseio.com/";
     $scope.bookList = $firebaseArray(ref.child('books'));
     $scope.authorList = $firebaseArray(ref.child('authors'));
     $scope.getAuthor = function(id) {
        return $scope.authorList.$getRecord(id);
    };
});
    

And my html looks like this:

<pre>
<p ng-repeat="book in books" ng-init="author = getAuthor(book.author)">
The author of {{book.title}} is {{author.firstName}} {{author.lastName}}.
</p>
</pre>

The desired output of the above should be: "The author of title1 is Jacques Smith. The author of title2 is Bill Halley ...". What I'm getting however is: "The author of title 1 is. The author of title2 is..." So the author in my html returns a blank.

Any idea?

Kato
  • 40,352
  • 6
  • 119
  • 149

3 Answers3

1

What I see is that your json data for books has author as an object. This is what is passed into the $getRecord method.The ID of the author is a key, not a value.

I believe if you structure your data like this:

books: { -JyDpkQrUozE3L7flpo: { title: "title1", author: "-JyDpkQrUozE3L7fl1x" } -JyDpkQrUozE3L7flp1: { title: "title2", author: "-JyDptrrrezE3L7flKx" }, -JyDpkQrUozE3L7flp2: { title: "title3", author: "-JyDpkQrUozE3L7f222" }

It should work, but it has been a long time since I have used firebase.

Brandon Brooks
  • 271
  • 1
  • 6
  • Thanks Brandon, I made the changes to the database and it works perfectly ! – Stephane B. Sep 03 '15 at 07:54
  • Quick question: is it not best practices to have author as an object in the json data for books? Going through the documentation ( [https://www.firebase.com/docs/web/guide/structuring-data.html] ), I understood that it would be the way to go. – Stephane B. Sep 03 '15 at 08:07
  • Their point is to use the key, as it will become part of the path. That path can then be tested for membership. Your original pattern is what they recommend, it is your getter that is not in sync. – Brandon Brooks Sep 03 '15 at 14:42
1

Brandon's answer is technically-correct answer to the posed question. I'm going to elaborate a bit on what would be a better way to join these records.

I've actually answered this exact question in a fiddle, and also provided a more sophisticated, elegant, and simpler solution of how to cache and merge user profiles into objects. I'll reiterate the details of how this works here.

app.factory('NormalizedPosts', function($firebaseArray, userCache) {
  var PostsWithUsers = $firebaseArray.$extend({

   // override $$added to include users
   $$added: function(snap) {
       // call the super method
       var record = $firebaseArray.prototype.$$added.call(this, snap);
       userCache.$load( record.user ).$loaded(function( userData ) {
            record.userData = userData;
       });
       // return the modified record
       return record;
   }

  });
  return PostsWithUsers;
});

Here I've decided to use a cached list of users, since they are likely to be highly redundant, and this provides an elegant way to keep everything in sync. It's not strictly necessary--we could look them up right there in $$added, but that leaves some edge cases to be handled. So a cache of synchronized users feels right here.

So here's the caching utility:

app.factory('userCache', function ($firebase) {
    return function (ref) {
        var cachedUsers = {};
        // loads one user into the local cache, you do not need to
        // wait for this to show it in your view, Angular and Firebase
        // will work out the details in the background
        cachedUsers.$load = function (id) {
            if( !cachedUsers.hasOwnProperty(id) ) {
                cachedUsers[id] = $firebaseObject(ref.child(id));
            }
            return cachedUsers[id];
        };
        // frees memory and stops listening on user objects
        // use this when you switch views in your SPA and no longer
        // need this list
        cachedUsers.$dispose = function () {
            angular.forEach(cachedUsers, function (user) {
                user.$destroy();
            });
        };
        // removes one user, note that the user does not have
        // to be cached locally for this to work
        cachedUsers.$remove = function(id) {
            delete cachedUsers[id];
            ref.child(id).remove();
        };
        return cachedUsers;
    }
});

And here's a gist putting it all together.

Note that, if we know that when our controller is destroyed, that the data will no longer be useful, we can clean up the listeners and memory by calling $destroy. This isn't strictly necessary and could be a premature optimization, but is probably worth mentioning for users with complex production apps that have tens of thousands of records to track:

app.controller('...', function(NormalizedPosts, userCache, $scope) {
   $scope.posts = new NormalizedPosts(<firebase ref>);
   $scope.$on('$destroy', function() {
      $scope.posts.$destroy();
      userCache.$dispose();
   });
});
Kato
  • 40,352
  • 6
  • 119
  • 149
  • Kato, Thanks for this and for all your help on your the various posts/ articles you posted :) Regarding your solution, do I understand correctly that you would recommend going your path only if you have tens of thousands of records to track? Otherwise, there is no downside/ caveats to use my approach? Thanks – Stephane B. Sep 03 '15 at 08:37
  • Realistically, I wouldn't recommend either approach with tens of thousands of records. I'd recommend reducing the scope, since the client can't display a thousand records in any meaningful manner anyway. The user cache example is more pragmatic, elegant, flexible, modular--whether or not you cache the lookups. The dual list approach is a bit awkward and there are timing issues and possible race conditions--plenty of edge cases that could go wrong, particularly as code grows in the future or these move into services. – Kato Sep 03 '15 at 17:37
  • Thanks for the follow-up Kato. – Stephane B. Sep 04 '15 at 13:46
0

I realize this is an old question but I thought I share my solution for those who are still googling.

Like Brandon mentions you shouldn't have the auther as an object, just and ID (reference)

Your data should look like this:

{ 
    books: {
        JyDpkQrUozE3L7flpo: {
            title: "title1",
            authorId: JyDpkQrUozE3L7fl1x
        },
        JyDpkQrUozE3L7flp1: {
            title: "title2",
            authorId: JyDptrrrezE3L7flKx
        },
        JyDpkQrUozE3L7flp2: {
            title: "title3",
            authorId: JyDpkQrUozE3L7f222
        }

    },
    authors: {
        JyDpkQrUozE3L7flKx: {
            firstname: "jacques",
            lastname: "smith"
        },
        JyDptrrrezE3L7flKx: {
            firstname: "bill",
            lastname: "halley"
        },
        JyDpkQrUozE3L7f222: {
            firstname: "john",
            lastname: "ford"
        }
    }
}

Note that I changed the author to authorId to be more explicit what is a authoer object and what is just the id. Now lets get all the books and the author.

app.factory('BooksWithAuthor', function($firebaseArray, $firebaseObject) {
      const books = firebase.database().ref('books')
      const authors = firebase.database().ref('authors');

      const bookList = $firebaseArray.$extend({
        $$added: function(snap) {
          const book = $firebaseArray.prototype.$$added.call(this, snap);
          book.author = $firebaseObject(authors.child(book.authorId));
          return record;
        }
      });

    return new bookList(books)
});

app.controller('BookCtrl, function(BooksWithAuthor) {
  $scope.BookList = BooksWithAuthor;
});

And then in your HTML just do

<div ng-repeat="book in booklist">
{{ book.title }} was written by {{ book.author.firstname }} {{ book.author.lastname }}
</div>
Bergur
  • 3,962
  • 12
  • 20