Skip to content

Instantly share code, notes, and snippets.

@sebastianseilund
Last active December 17, 2015 11:28
Show Gist options
  • Save sebastianseilund/5601899 to your computer and use it in GitHub Desktop.
Save sebastianseilund/5601899 to your computer and use it in GitHub Desktop.
Ember Data vs. Billy Data

Billy Data is not as lightweight as Ember Model or Ember Resource. It is a full data framework. I'm pretty sure it can do at least 95% of what Ember Data can do, except for custom adapters/serializers. Here are some important differences and a couple of features I find useful.

Record arrays

Filtered record array that updates live. Also works with sorting, so that records are inserted at the correct position. Say you have a filtered record array of Person records with name "Tom" ordered by name. Anytime a Person record changes its name property, it is checked against all filtered record arrays that "observe" that property. Then it will be either added or removed. And if you change your name to Atom Dale, your Person record will be moved to the top of the list.

Sparse array support. A record array can query the server and get e.g. the first 100 records and a totalCount. The length property will be set to the totalCount from the response, and every time an index is requested the array loads 100 records around that index, unless they're already loaded of course. Records can still be created locally which will just insert them at the correct spot in the sparse array, and increment the length property. This works beautifully with 10,000s of thousands of records in an Ember.ListView.

Relationships

Full relational functionality with belongsTo and hasMany. All relationships update automatically, since a hasMany is simply a filtered record array filtered by the parent attribute. It doesn't involve that much code, and it still works with rollbacks and deleted records.

Keeping certain models 100% in memory

Ability to tell the store that all records of a specific type has been loaded, e.g. App.Contact.loadAll([{…}, …]. This way future calls to App.Contact will not send a server request, but can filter the records locally and instantly.

When our app starts up it calls GET /user/bootstrap from the server. The server will check how many records exists for each of invoices, accounts, contacts etc. and send all of them if there is less than 100 of a particular type (which will be true for most users).

This feature really helps speeding up our app. Many route transitions are instant, since the necessary data is already in memory.

Committing records

Save a record with altered properties to the server without actually changing the properties in the front-end (yet). Once the server responds with a 2xx code, the properties will be set in the client. This is very useful if you fx have a filtered list of state=draft invoices. When approving the invoice, you don't want it to be removed from the draft list before we are 100% sure that it got saved on the server. Works by calling invoice.save({properties: {state: 'approved'}}).

Call save directly on a single record and get a promise back that you can get the response from when returned from the server. No need for transactions if you just want to save a specific record.

More flexibility with embedded records. Let's say we have App.Invoice and App.InvoiceLine. We need to be able to load invoices by themselves (i.e. no lines embedded). We need to be able to save the invoice by itself. We also need to be able to save the invoice with its lines embedded (needs to be db transaction on the server). So in Billy Data, when calling save on a model, you can choose which embedded relationships you'd like to include in the request. Example: invoice.save({embed: ['lines']}). In, at least a previous version of, Ember Data you either embedded the records always or never. It also wasn't possible to load an invoice without its lines embedded, and then load the lines later.

Support for anonymous records, which is really useful when you want to POST data to a "special" endpoint. An example could be a user login form. The App.User does for obvious reason not have a password field on it. Instead you can create an anonymous record, populate it with data, and call save on it - just like a normal record. Here is our login window controller for when a user's session expires:

App.LoginWindowController = Em.ObjectController.extend(App.ui.WindowController, {

    model: function() {
        return BD.AnonymousRecord.createRecord({
            email: App.session.get('user.email'),
            password: '',
            remember: false
        })
    }.property(),

    submit: function() {
        var self = this;
        this.get('model').save('/user/login').then(function() {
            var callback = self.get('callback');
            if (callback) {
                callback();
            }
            self.close();
        });
    }

});

With BD.AnonymousRecord all error handling, JSON transforms etc. is automatically handled.

Deleting records

Deleting records does also not require a transaction. You can call deleteRecord() on any record. If the server request fails, the record is rolled back and is not in an error state. This avoids conflicts when DELETE requests fail (which they will).

Record states

More simple states, which makes sure that records are never stuck in e.g. an error state. Currently the states are managed by some boolean properties (isCreated, isLoaded, isDirty, isDeleted). It might make more sense to use a state machine, but it does help to keep it very simple and efficient.

Summary

I realize that many of my points are specific to our app, and there are definetely more use cases that needs to be taken into consideration when architecting a one-size-fits-all data framework. These are just my thoughts on what makes it difficult using Ember Data. Let's talk!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment