6

I'm building an app with Backbone.js. Some of my server-side APIs will return me all the new or changed models since a particular time. Some of those objects may be new to my collection so I can just add them. Others may already exist, in which case I'd like to update the existing model. So basically I'm looking for upsert (update-or-insert) functionality.

This is similar to the {add:true} option that was added in 0.9.0, except that I also want updating.

Is there an easy/known way to do this? It doesn't seem hard to update the code, but I don't want to reinvent a wheel.

Brian Reischl
  • 7,216
  • 2
  • 35
  • 46

2 Answers2

5

I have solved this situation in a generic method:

App.Utils = {
  refreshCollection: function( collection, collectionJSON ){
    // update/add
    _( collectionJSON ).each( function( modelJSON ) {
      var model = collection.get( modelJSON.id );
      if( model ) {
        model.set( modelJSON );
      } else {
        collection.add( modelJSON );
      }
    });

    // remove
    var model_ids_to_keep     = _( collectionJSON ).pluck( "id" );
    var model_ids             = collection.pluck( "id" );
    var model_ids_to_remove   = _( model_ids ).difference( model_ids_to_keep )

    _( model_ids_to_remove ).each( function( model_id_to_remove ){
      collection.remove( model_id_to_remove );
    });
  },
}

Params

  • collection: is a Backbone.Collection
  • collectionJSON: is an Array with the models data in Hash style.. typical JSON response.

I'm sure it can be optimized, especially the remove part. But I keep it like this for readability due I'm still making tests.

Example of use

// code simplified and no tested
var VolatileCollection = Backbone.Collection.extend({
  // url:
  // model:
  // so on ... 
})

var PersistentCollection = Backbone.Collection.extend({
  // url: not needed due this Collection is never synchronized or changed by its own
  //   change the VolatileCollection instead, all changes will be mirrored to this Collection
  //   through events
  // model: the same

  initialize: function( opts ){
    this.volatileCollection = opts.volatileCollection;
    this.volatileCollection.on( "reset", this.update, this );
    this.volatileCollection.on( "change", this.update, this );
  }

  update: function(){
    App.Utils.refreshCollection( this, this.volatileCollection.toJSON() );
  }
})

var volatileCollection = new VolatileCollection();
var persistentCollection = new PersistentCollection({ volatileCollection: volatileCollection });

volatileCollection.fetch();
fguillen
  • 36,125
  • 23
  • 149
  • 210
  • This is not quite what I was asking for, since I don't want to remove missing models, but it would be quite easy to make it do that. What you've done seems similar to a fetch/reset, except that it doesn't destroy models that will be in the final collection. Seems like a nice optimization. – Brian Reischl Mar 12 '12 at 15:38
  • Nice. This *does* assume, however, that the *identity* column is always going to be "id", but that's not always going to be the case. As a more generic function, you might want to check the collection's model's "idAttribute" and a reference to that instead of defaulting to all the "'id'" and ".id"s around – JayC Mar 12 '12 at 19:21
  • I just implemented something similar, here is my optimization to the "remove" part: https://gist.github.com/3289267 – Jonathan Julian Aug 07 '12 at 21:00
3

NOTE: This answer was written for Backbone v0.9. Since then Backbone v1.0 has released, which contains the collection.set() method described here. That will likely be a better solution.

I threw together my own version of this over the weekend. I'll put it up here in case anybody else finds this thread, but I'd still be happy to see any other solutions that might be out there.

I did this by modifying the Backbone.js source, which is not necessarily a great idea, but it was easy. There were two changes, first add this function to the Backbone.Collection prototype:

//**upsert** takes models and does an update-or-insert operation on them
//So models that already exist are updated, and new models are added
upsert: function (models, options) {
    var self = this;
    options || (options = {});
    models = _.isArray(models) ? models.slice() : [models];


   var addModels = [];
    _.each(models, function (newModel) {
        var n = self._prepareModel(newModel, options);
        var existingModel = self.get(n.id);
        if (existingModel) {
            existingModel.set(n, options);
        } else {
            addModels.push(n);
        }
    });

    if (!_.isEmpty(addModels)) {
    self.add(addModels, options);
    }
}

Then modify one line in the Backbone.Collection.fetch() function to read:

collection[options.add ? 'add' : (options.upsert ? "upsert" : 'reset')](collection.parse(resp, xhr), options);

This allows you to call fetch({upsert:true}) to get the behavior I was looking for.

Brian Reischl
  • 7,216
  • 2
  • 35
  • 46
  • I think my issue about defaulting to "id" with fguillen's code also applies here. (Models won't always have "id" as the identity.) – JayC Mar 12 '12 at 19:22
  • 1
    The default Backbone.Model implementation will set the .id property for you, if you have set the idAttribute. Here's the relevant code: if (this.idAttribute in attrs) this.id = attrs[this.idAttribute]; – Brian Reischl Mar 12 '12 at 22:08