3

Okay the obligatory information:

DEBUG: ------------------------------- 
DEBUG: Ember      : 1.3.1 
DEBUG: Ember Data : 1.0.0-beta.7.f87cba88 
DEBUG: Handlebars : 1.0.0 
DEBUG: jQuery     : 1.10.2 
DEBUG: ------------------------------- 

Here are the relevant models:

App.Destination = DS.Model.extend({
    label: DS.attr('string'),
    destinationMatchRules: DS.hasMany('destinationMatchRule', { async: true }),
    chargeRules: DS.hasMany('chargeRule', { async: true }),
});

App.ChargeRule = DS.Model.extend({
    startDate: DS.attr('date'),
    endDate: DS.attr('date'),
    costPerMinute: DS.attr('number'),
    countryCode: DS.attr('string'),
    label: DS.attr('string'),
    connectionCost: DS.attr('number'),
    destination: DS.belongsTo('destination', { async: true }),
});

App.DestinationMatchRule = DS.Model.extend({
    pattern: DS.attr('string'),
    priority: DS.attr('number'),
    destination: DS.belongsTo('destination', { async: true }),
});

I have a destination.edit route/controller/template that lets me create or edit these all together. If I create a new set from scratch and hook them all up, I use this code to save everything:

save: function() {
    var destination = this.get('content');
    destination.save().then(function(){
        var promises = Ember.A();
        destination.get('destinationMatchRules').forEach(function(item){
            promises.push(item.save());
        });
        destination.get('chargeRules').forEach(function(item){
            promises.push(item.save());
        });
        Ember.RSVP.Promise.all(promises).then(function(resolvedPs){
            alert('All saved!');
        });
    });
}

And it works! And there was much rejoicing.

However. If I use the same route and code to edit an existing graph--say I edit the destination's label and/or the destinationMatchRule's priority--I have big problems with both belongsTo('destination', { async: true }) relationships.

I've put breakpoints in my serializer's serializeBelongsTo method and all over my save method. It seems that the following is happening:

  1. If the DestinationMatchRule and ChargeRule objects in the hasMany are not newly created records, calling .save() on the Destination record automatically tries to save the records in the hasManys, so the explicit child record item.save() calls in my save method above are pre-empted in this case. (I think…this is a part I'm slightly less sure about)
  2. When the serializeBelongsTo method of the serializer is called for the DestinationMatchRule and ChargeRule records, the destination property of each is a DS.PromiseObject, and the promise is unsettled. This results in serializeBelongsTo adding destination: undefined to the JSON object, and ultimately in the destination property being dropped completely from the serialized JSON sent to the backend.

Naturally, this causes my backend to think that the destination object for the child records has been removed (which in my case causes a cardinality violation).

When I put logging statements near my explicit item.save() calls, I see instead that the DS.PromiseObject is settled, which is why I conclude what I described in #1 above. I first tried wrapping those calls in all manner of Ember.run.next callbacks to try to give the belongsTo time to settle, but if I'm correct about #1 that won't help anyway. But that also means I have no place to try and work around the problem!

Can somebody please help me figure this out--I'm so close and I think this may be the last battle in getting Ember Data to actually work and be used in an "ambitious" web application.

Edit

Correction, my conclusion about the order of operations here was incorrect. In fact I can make it work for both newly created graphs and editing existing ones by making my save method look like this:

save: function() {
    var destination = this.get('content');
    destination.save().then(function(){
        //var promises = Ember.A();
        destination.get('destinationMatchRules').forEach(function(item){
            item.get('destination').then(function(){
                item.save();
            });
                //promises.push(item.save());
        });
        destination.get('chargeRules').forEach(function(item){
            item.get('destination').then(function(){
                item.save();
            });
        });
        //Ember.RSVP.Promise.all(promises).then(function(resolvedPs){
        //    alert('All saved!');
        //});
    });
}

BUT, this doesn't scale well: what if I had more belongsTo relationships? Might they all have to settle? Also it's now much harder to tell when the whole operation is complete--I can't add the item.save() returned promise to my promises array because it isn't created immediately (hints on this welcome in comments). And, should I be thening some other promise that I don't see, to make sure that the destination.save() is really really complete, and all the relationships have been propagated?

S'pht'Kr
  • 2,809
  • 1
  • 24
  • 43
  • Hi i am trying to do a similar thing but when I call '.then' on a '.get' request from my controller I receive an "Object [object Object] has no method 'then'" – oneiota Mar 07 '14 at 12:25
  • @oneiota Are you using Ember Data or straight jQuery ajax/get calls? If you're using plain old `$.get(...)` that's likely a very different ballgame. And the comments don't likely have enough room to discuss it :-). Open a new question and link it here and I'll have a look. – S'pht'Kr Mar 07 '14 at 20:06
  • 1
    @oneiota Ah, I "get" what you're saying now--ha. So I presume that the `.get('propname').then(...)` you are trying is on a `hasMany` or `belongsTo` property of your model? If so, this syntax only makes sense if the relationship property is marked `async`, as mine are above, and it sounds like yours aren't. That is, you only use (really, only _need_) the `.then(…)` part if there's a possibility that the related models aren't already loaded--i.e., the property really contains a `DS.PromiseObject` or `DS.PromiseArray`, i.e. it's an `async` relationship. – S'pht'Kr Mar 07 '14 at 20:53
  • Hahaha thanks heaps! you were exactly right i did not have async marked on the model properties!!! Hence the promise was invalid as the models were already loaded. Now works fine - what an ember moment that was... – oneiota Mar 08 '14 at 05:31

1 Answers1

1

Posting as an answer as this now seems a good general solution. But if this is necessary I'm really wondering if there is a way that's already in the code to handle it more succinctly and I'm duplicating something…

Nevertheless, here is what I'm doing to get around this, I created a new method on DS.Model called saveWhenSettled that returns a Promise:

DS.Model.reopen({
  saveWhenSettled: function() {
    var record = this;
    return new Ember.RSVP.Promise(function(resolve, reject){
      var promises = Ember.A();
      record.eachRelationship(function(rel){ 
        if(record.get(rel).then) promises.pushObject(record.get(rel));
      });
      Ember.RSVP.Promise.all(promises).then(function(){
        record.save().then(function(result){
          resolve(result);
        }, function(reason){
          reject(reason);
        });
      }, function(reason){
        reject(reason);
      });
    });
  }
});

Now I can just:

save: function() {
    var destination = this.get('content');
    destination.save().then(function(){
        var promises = Ember.A();
        destination.get('destinationMatchRules').forEach(function(item){
            promises.push(item.saveWhenSettled());
        });
        destination.get('chargeRules').forEach(function(item){
            promises.push(item.saveWhenSettled());
        });
        Ember.RSVP.Promise.all(promises).then(function(resolvedPs){
            alert('All saved!');
        });
    });
}

So I get my completion notification back, and it automatically takes care of multiple relationships on the record needing to settle (which I've already run into since posting the question).

Again, I'd love for someone to tell me I've duplicated baseline code and I should do it another way.

A similar approach could probably be used to generalize my save method to auto-save all child relationships first, but I'll leave that as an exercise for the reader.

S'pht'Kr
  • 2,809
  • 1
  • 24
  • 43