Ever since Thomas Boyt created a Firebase Ember Data adapter, our community started asking about an official Firebase integration for Ember. We're excited to release EmberFire today: official Firebase bindings for Ember Data! EmberFire makes it easy for developers to add a real-time backend to their Ember application with just a few lines of code. Check out the blog example, and then dive into code to wire up your app's Ember backend - no servers required.
As the Ember community grows, many companies (including Square, Zendesk, and Groupon) are using the framework to build ambitious, asynchronous web applications. A few weeks back, Tom and Yehuda from the Ember Core Team came by the Firebase offices and worked with us to develop a first-class integration for Firebase. Tom explains the benefits this integration will bring to developers:
“By freeing developers from the constraints of the backend, Firebase unlocks a whole new category of sophisticated, client-side JavaScript applications. Now, with first-class support for Ember.js, those developers can continue pushing the boundaries of what's possible in the browser by leaning on the strong architectural features of Ember that lead your app towards clean separation of concerns instead of messy spaghetti. (Let's also not forget terrific support for URLs out of the box.) Ember's always been about building ambitious web applications, and this collaboration with Firebase only strengthens that idea.”
We know Firebase's real-time data synchronization will be a great fit with Ember's opinionated front-end philosophy, so let's take a look at how it all works.
To begin using EmberFire, simply include the necessary libraries in your application. Note that our EmberFire bindings work only with Ember Data.
<!-- Don't forget to include Ember and its dependencies --> <script src="http://builds.emberjs.com/canary/ember-data.js"></script> <script src="https://cdn.firebase.com/js/client/1.0.17/firebase.js"></script> <script src="emberfire.js"></script>
Now you're ready to automatically sync your Ember models with data stored in Firebase. To start using EmberFire, simply create an instance of DS.FirebaseAdapter and DS.FirebaseSerializer in your app, like this:
DS.FirebaseAdapter
DS.FirebaseSerializer
App.ApplicationAdapter = DS.FirebaseAdapter.extend({ firebase: new Firebase('https://<my-firebase>.firebaseio.com') }); App.ApplicationSerializer = DS.FirebaseSerializer.extend();
With the adapter and serializer set up, you can now interact with the data store as you normally would with Ember. For example, calling find() with a specific ID will retrieve that record from Firebase. It will also start watching for updates and will update the data store automatically whenever anything is added or removed.
For more documentation, check out the EmberFire README.
Yehuda, Tom, and the rest of the Ember Core Team, who gave us invaluable input on multiple iterations of the bindings. We’d also like to thank everyone who helped us test early versions of EmberFire and gave excellent feedback to help us make the bindings top-notch.
This is just the beginning of our Ember integration, so we welcome your feedback and participation! Submit a pull request, or share your thoughts on Twitter or the Firebase Google Group. We can't wait to see what you build with Ember and Firebase.
Update (November 4, 2014): While this post still contains some useful and relevant information, we have released advanced query functionality which solves a lot of the problems this post discusses. You can read more about it in our queries blog post.
No WHERE clauses? No JOIN statements? No problem!
Coming from a SQL background as I did, it can take a while to grok the freedom of NoSQL data structures and the simplicity of Firebase's dynamic, real-time query environment.
Part 1 of this double-header will will cover some of the common queries we know and love and talk about how they can be converted to Firebase queries. Part 2 goes on to cover some advanced query techniques for Firebase and a solution for full-blown content searches.
So let's jump in. Here's what we're going to cover today:
This article relies heavily on the proper use of the following API calls, all of which are introduced in the documentation for Queries and Limiting Data:
This article also leans heavily on theory from Anant's authoritative post, Denormalizing is Normal. Where Anant's post covers a wide breadth and highly foundational concepts, this post serves more as a quick reference and recipe book.
We're going to work with the examples-sql-queries Firebase for all the examples. Feel free to browse the data and get a feel for the structure.
A lot of times in our docs, you'll see something like var ref = new Firebase(URL); and then later, ref.child('user/1'). But in our examples we use new Firebase('URL/child/path'). So which should you use? They are functionally equivalent; use the one that keeps your code simple to read and maintain.
var ref = new Firebase(URL);
ref.child('user/1')
new Firebase('URL/child/path')
Using a variable will be a bit DRYer if it's going to be referenced several times, but creating multiple Firebase instances does not incur any additional overhead as this is all optimized internally by the SDK.
Select a user by ID (WHERE id = x)
We'll start off with the basics and build from here. In Firebase queries, records are stored in a "path", which is simply a URL in the data hierarchy. In our sample data, we've stored our users at /user. So to retrieve record by it's id, we just append it to the URL:
new Firebase('https://example-data-sql.firebaseio.com/user/1').once('value', function(snap) { console.log('I fetched a user!', snap.val()); });
See it work
Find a user by email address (WHERE email = x)
Selecting an ID is all good and fine. But what if I want to look up an account by something that's not already part of the URL path?
Well this is where ordered data becomes our friend. Since we know that email addresses will be a common lookup method, we can call setPriority() whenever we add a new record. Then we can use that priority to look them up later.
new Firebase("https://examples-sql-queries.firebaseio.com/user") .startAt('kato@firebase.com') .endAt('kato@firebase.com') .once('value', function(snap) { console.log('accounts matching email address', snap.val()) });
Pretty cool and useful for most cases, but what if we can't use priorities? Or we need to search on more than one field? Well then it's time to employ some indices!
See this example, which uses index/ to link email addresses to user accounts.
Get messages posted yesterday (WHERE timestamp BETWEEN x AND y)
What if we'd like to select a range of data? Ordering data with priorities is quite useful for this as well:
new Firebase("https://examples-sql-queries.firebaseio.com/messages") .startAt(startTime) .endAt(endTime) .once('value', function(snap) { console.log('messages in range', snap.val()); });
Paginate through widgets (LIMIT 10 OFFSET 10)
First of all, let's make some assertions. Unless we're talking about a static data set, pagination behavior becomes very ambiguous. For instance, how do I define page numbers in a constantly changing data set where records are deleted or added frequently? How do I define the offset? The "last" page? If those questions are difficult to answer, then pagination is probably not the right answer for your use case.
Pagination for small, static data sets (less than 1MB) can be done entirely client side. For larger static data sets, things get a bit more challenging. Assuming we're writing append-only data, we can use our ordered data examples above and assign each message a page number or a unique incremental counter and then use startAt()/endAt().
// fetch page 2 of messages new Firebase("https://examples-sql-queries.firebaseio.com/messages") .startAt(2) // assumes the priority is the page number .endAt(2) .once('value', function(snap) { console.log('messages in range', snap.val()); });
But what if we're working with something like our widgets path, which doesn't have priorities? We can simply "start at" the last record on the previous page by passing null for a priority, followed by the last record id:
// fetch page 2 of widgets new Firebase("https://examples-sql-queries.firebaseio.com/widget") .startAt(null, lastWidgetOnPrevPage) .limitToFirst(LIMIT+1) // add one to limit to account for lastWidgetOnPrevPage .once('value', function(snap) { var vals = snap.val()||{}; delete vals[lastWidgetOnPrevPage]; // delete the extraneous record console.log('widgets on this page', vals); });
See it work or see a full pagination example
Join records using an id (FROM table1 JOIN table2 USING id)
Firebasers talk a lot about denormalization, which is great advice, but how do you put things back together once you've split them apart? Well, it's a great deal simpler than it might seem.
Firebase is a real-time sync platform. It's built for speed and efficiency. You don't need to worry about creating extra references, and can listen to as many paths as you'd like to retrieve your data:
var fb = new Firebase("https://examples-sql-queries.firebaseio.com/"); fb.child('user/123').once('value', function(userSnap) { fb.child('media/123').once('value', function(mediaSnap) { // extend function: https://gist.github.com/katowulf/6598238 console.log( extend({}, userSnap.val(), mediaSnap.val()) ); }); });
If you are anything like me, your perfectionist instincts will be kicking in about now, since our merge logic synchronously waits for the user data to load before grabbing media. It also gets a bit verbose if we add several paths to be joined. So let's expand this into a utility that will merge any number of paths asynchronously, and stick a fork in it:
See three paths merged in parallel
More Tools to Come
Part 2 of this post covers some advanced techniques for performing content searching (e.g. WHERE description IS LIKE '%foo%').
WHERE description IS LIKE '%foo%'
We're hard at work optimizing Firebase's search and querying features by combining the best aspects of patterns like map reduce with the simplicity and speed of our real-time tools. Look for more news on this in the next few months! In the mean time, we'd love to hear your feedback. Let us know in the comments or email firebase-support@google.com.