1
votes

I am working with Ember.js using Ember-Data and [Kurko's Ember Data IndexedDB Adapter] (https://github.com/kurko/ember-indexeddb-adapter) with multiple hasMany relationships. I am able to successfully add hasMany relationships without any issues. My issue lies with removing the hasMany relationships.

App.Race = DS.Model.extend({
    name: DS.attr('string'),
    ...
    athletes: DS.hasMany('athlete', {async: true}),
    ...
});

App.Athlete = DS.Model.extend({
    bib: DS.attr('number'),
    name: DS.attr('string'),
    ...
    splits: DS.hasMany('split', {async: true}),
    races: DS.hasMany('race', {async: true}),
    ...
});

App.Split = DS.Model.extend({
    distance: DS.attr('string'),
    time: DS.attr('string'),
    athlete: DS.belongsTo('athlete', {async:true}),
    race: DS.belongsTo('race', {async: true}),
    ...
});

On on occasion the athlete reference is not removed from the specified race model when deleting athletes, breaking the application. The athlete model is always deleted, and it always deletes all of the necessary splits models. I know that it has something to do with the asynchronous nature of my storage, but I cannot isolate the issue in the action:

removeAthlete: function() {
    var self = this,
    athlete = this.get('model');

    // Get race from athlete
    athlete.get('races').then(function(races){

        // Remove athlete from races
        races = races.toArray();
        races.forEach(function(race){

            console.log('removing athlete from race');
            race.get('athletes').removeObject(athlete);
            race.save();
        }); 
        // destroy splits
        athlete.get('splits').then(function(splits) {
            console.log('retrieved splits');
            splits.toArray().forEach(function(split) {
                console.log('removing split');
                split.destroyRecord();
            });

            // destroy athlete
            athlete.destroyRecord();
        });
    }); 
},
...

Edit

After some attempts with notifications, I have realized that the error has to do with the removal of the athlete reference from the race. I will give more updates as I progress.

1
I'm wondering if your issue is your call to race.get('athletes').removeObject(athlete);? That will retrieve the athletes RecordArray from the given race and directly modify that object, but Ember doesn't know you modified it because you're calling get and not set. Can you try explicitly adding a call to notifyPropertyChange (emberjs.com/api/classes/…), something like race.notifyPropertyChange('athletes'); to see if that makes a difference?Josh Padnick
I just added that to fire after race.save(); so I will give a few test runs and let you know. I don't if that is where the problem is though. The DOM removes the itemview for the athlete, and the athlete is deleted, but the reference to that athlete still remains in race.get('athletes'). So it references an athlete that no longer existsDFenstermacher
Unfortunately, adding race.notifyPropertyChange('athletes'); that did not fix the problem. The problem I think is in the removal of the athlete itself, not the notification of changes. I am making changes to my post to reflect this.DFenstermacher
It'd be great if you could setup a emberjs.jsbin.com for this. Then we could play around with it real time.Josh Padnick
I am currently working on putting a JSBin together, but it seems that JSBin is down. In the meantime I am adding more information to this post.DFenstermacher

1 Answers

1
votes

While work on a JSBin for this question I was able to figure out what the problem was and how to fix it. Hopefully I am able to put it into words. The problem lied in my nesting (or lack thereof) of asynchronous calls to storage.

In the removeAthlete method I was not nesting my asynchronous calls to my storage container, which was resulting in athlete.destroyRecord() being called prematurely.

removeAthlete: function() {
    var self = this,
    athlete = this.get('model');

    athlete.get('races').then(function(races){    // Get race from athlete

        races = races.toArray();
        races.forEach(function(race){ // Remove athlete from each race
            race.get('athletes').removeObject(athlete);
            race.save();
        }); 

        // async call to splits
        athlete.get('splits').then(function(splits) {
            // destroy each split
            splits.toArray().forEach(function(split) {
                split.destroyRecord();
            });

            /* 
            call to destroy athlete is called while other async calls are 
            waiting to get responses
            */
            athlete.destroyRecord();
        });

    }); 
},

Asynchronous calls continue executing code while waiting for responses, so the code following each asynchronous call was being called before the async response was recieved. Depending on how many splits are in the splits array, athlete.destroyRecord could be called before the code in previous lines was executed. If there were fewer splits, athlete.destroyRecord was called sooner. If there were more, athlete.destroyRecord was called later, after all the previous lines were called. So my solution was this:

removeAthlete: function() {
    var self = this,
    athlete = this.get('model');

    // Get race from athlete
    athlete.get('races').then(function(races){

        // Remove athlete from races
        races = races.toArray();
        races.forEach(function(race){

            race.get('athletes').removeObject(athlete);
            race.save().then(function(){    
                if(races.indexOf(race) === races.get('length') -1) {
                    athlete.get('splits').then(function(splits) {
                    splits.toArray().forEach(function(split) {
                        split.destroyRecord();
                    });

                    // destroy athlete
                    athlete.destroyRecord();
                    }); 
                }
            }); 
        }); 
    }); 
},

In this way, each asynchronous call which depends on the results of other asynchronous calls are called in turn. splits are only deleted after all athletes have been removed from races. I am hoping to find a simpler, more efficient solution though. This does not seem like the best way to do this. So I will not mark this as the correct answer. Hopefully someone else knows of a better way to do this.