Ember Data - Endless frustration


#1

I’ve raised this problem on GitHub, StackOverflow and on IRC and I’ve not got anywhere at all with this issue and have wasted hours trying to figure it out. All I’m trying to do it something really simple and it a fundamental feature of other similar libraires such as backbone-relational. At this point I’m seriously considering just giving up on ember all together.

Take the following two models:

App.Basket = DS.Model.extend({
  fruits: DS.hasMany('App.Fruit')
});

App.Fruit = DS.Model.extend({
  basket: DS.belongsTo('App.Basket')
});

Now, I load a basket but don’t specify any fruit_ids. Then I fetch some fruits that have basket_id. fruit.get(‘basket’) returns the basket but the hasMany on the basket does not update so basket.get(‘fruits’) is just empty. There are many situations where it is impractical to include every single id for a hasMany relationship in the original JSON.

I’ve tried adding:

didLoad: function() {
  this.get('basket').get('fruits').pushObject(this);
  this.get('stateManager').send('becameClean');
  this.get('stateManager').send('finishedMaterializing');
}

To the fruit model. This works fine the first time the data is fetched but when ember decides to fetch the data again didLoad is never called ever again, I can’t find any hook that is actually called on subsequent attempts to fetch the (fresh?) data. I’ve also attempted to “silently” add models to hasMany relationships when ember-data materialises the belongsTo attribute via playing around with ember-data internals but I just end up breaking the rendering of templates.

All of what I’ve attempted it from looking at the source of ember-data, I can’t find any documentation so I’ve no idea if what I’m doing is on the right path or not.

Sorry if this seems like a bit of a rant, I’m just finding the whole thing very frustrating for something that should be so simple. Hopefully someone can point me in the right direction or shed some light on this, the forum seems like my last hope here.

Thanks.


#2

As far as I can tell, this is one of several major reasons why Ember Data is marked as not “production ready” on the website:

Is It “Production Ready™”?

No. The API should not be considered stable until 1.0.

I’ve actually used Ember Data successfully in a real production project despite this warning, and it’s actually a great library in many ways. But it’s far from finished—there are no validations, HTTP error-handling is shaky at best, reloading models is tricky, and I’ve hit one or two really nasty bugs.

There are several ways to use Ember.js without having Ember Data wreck your life:

  1. Use $.ajax calls from within the model method on your router, and roll your own very simplistic models using Ember.Object. I think I heard that Discourse does quite a bit of this.
  2. Use ember-rest or ember-resource instead of Ember Data. I haven’t tried either, but they look like they’re much simpler (and less featureful) than Ember Data.
  3. Feed Ember Data exactly the JSON it wants, and stop trying to fight it. This is what I ended up doing, with excellent results. In your case, this means that, yes, you need to always send the association IDs with the models, at least until this part of Ember Data gets implemented.

The root of your problems is that you’re treating Ember Data like production-ready code, when it’s really a very nice prototype that can be made to work, but only if you don’t ask it to do anything too strange. Once I was forced to make this attitude adjustment, I actually got pretty good results with it.


#3

Hello @emk, could you show us your solution for point 3?

I very like to use Ember Data right now!

Thank you


#4

See the guide to the REST adapter for a good starting point. Try to produce the same JSON format you see there. And if there’s a mismatch between your server and Ember Data, fix the server. If you use Rails, see active_model_serializers.


#5

@Tomdarkness, first of all, I share your pain in terms of getting ember-data to work in a handful of what appears to be basic use cases.

While I love the library on a overall basis, it’s far from finished - and has been the cause of a myriad of problems for me, too. I’ve seriously considered ditching it at least two or three times now, but the work being put into this from the entire community is worth a million - and I’m sure things will get better soon.

TL;DR: You need to handle associations yourself in order to make things work properly in all cases. There’s just no other way at this point.

Below, I’ll share my own experiences in the hope that you may find my ramblings somewhat useful. It is my intention to first describe what works (creating/finding records), then explain at least one use case that doesn’t work as expected (loading records), and finally attempt to come up with a solution (using Em.ArrayController).

Here goes:

Pain point #1: ember-data not updating associated records automatically in all cases

At this point, you cannot rely on ember-data updating all associated records automatically whenever you’re dealing with records, at least not in all cases. Consider this scenario:

Models:

App.Basket = DS.Model.extend({
  name: DS.attr('string'),
  fruits: DS.hasMany('App.Fruit')
});

App.Fruit = DS.Model.extend({
  name: DS.attr('string'),
  basket: DS.belongsTo('App.Basket')
});

Then, you make the following calls in your code:

Calls:

var basket = App.Basket.createRecord({ name: 'My fruit basket', id: '60' });

var banana = App.Fruit.createRecord({ name: 'Banana', basket: basket, id: '61' });

Results:

Playing around in the console in Chrome, getting the fruits from the basket results in this:

basket.get('fruits') -> Class {type: function, store: Class, _changesToSync: Ember.OrderedSet, owner: Class, name: "fruits"…}

Or, accessing the fruits.content directly:

basket.get('fruits.content') -> [ Object ]

So far, this seems to work the way it should. However, say you retrieve some data from a pub/sub setup. If you do, you might want to App.store.load those data into the store. This is one of the scenarios where the automagic gets medieval - or straight up just doesn’t work.

Loading data into the store:

Loading data into the store is as simple as calling App.store.load(App.Fruit, { basket_id: '60', id: '80', name: 'Pear' });. Assuming everything works as it should, we should be able to create (or find) a basket, create (or find) a banana, load a pear, and have both fruits show up when we get them from the basket.

Attempt this in the Chrome console:

Calls (creating):

var basket = App.Basket.createRecord({ name: 'My fruit basket', id: '60' });

var banana = App.Fruit.createRecord({ name: 'Banana', basket: basket, id: '61' });

Results:

basket.get('fruits.content'); // -> returns [ Object ].

Calls (loading):

App.store.load(App.Fruit, { basket_id: '60', id: '80', name: 'Pear' });

Results:

basket.get('fruits.content'); // -> still only returns [ Object ]!

This suggests that the automagic surrounding the handling of associations does not entirely work at this point. Put differently, even though you have your JSON behave exactly as required out-of-the-box, you’ll run into trouble. Therefore, you need to do something else if you wish to continue to use ember-data.

(So far, this is exactly what you’ve come up with yourself. I’m just writing this in an explanatory style for other people to potentially benefit from our common experiences.)

Pain point #2: handling the associations manually:

Months ago, I came to the same conclusion as you: to handle the associations on my own. Yet this results in some other problems - or one, at least, which is quite significant.

Whenever a record is loaded, didLoad is triggered. And hooray! We can get the associations of a record by iterating through this.eachRelationship(...). That way, we can fetch all parents via checking for kind === 'belongsTo' iterating through the assocations.

However, the problem is that adding a child record to a parent’s hasMany relation will result in the parent being dirtied, just as you’ve discovered. Although this makes sense from a strict client-side perspective, it’s not the truth since the parent is not dirty in reality; it might just be that another user has added an additional instance of App.Fruit that belongs to our basket. This instance gets pushed to our client and we want to update the association accordingly, accidentally dirtying the parent as a result. Ouch!

So far, my own experiments have all stranded here and my ninja coding skills have taken over. What I have done in my own project is to introduce a common model that every other model extends.

Attempting to make things work using an Em.ArrayController:

My App.Model has didInit implemented. When the event is triggered, I run through each association in order to create an Em.ArrayController for each hasMany relationship. Here’s a snippet from my model.js file:

didInit: function() {
	var self = this;
	
	this.eachRelationship(function(name, meta) {
		if (meta.kind === 'hasMany') {
			self.set('loaded' + Em.String.classify(name), Em.ArrayController.extend().create());
		}
	});
},

In the case of our App.Basket model, creating a new instance would result in basket.loadedFruits being available. And inside this ArrayController is where I store the associated records.

A trimmed-down version of model.js source is included here:

App.Model = DS.Model.extend({

	// Primary key:
	primaryKey: 'id',

	// Toggles:
	hasBeenDeleted: false,
	hasBeenLoaded: false,

	// Events:
	didCreate: function() {
		this.didLoad();
	},

	didDelete: function() {
		if (!this.get('hasBeenDeleted')) {			
			this.trigger('deleteContexts');
			this.toggleProperty('hasBeenDeleted');
		}
	},

	didInit: function() {
		var self = this;

		this.eachRelationship(function(name, meta) {
			if (meta.kind === 'hasMany') {
				self.set('loaded' + Em.String.classify(name), Em.ArrayController.extend().create());
			}
		});
	},

	didLoad: function() {
		if (!this.get('hasBeenLoaded')) {
			// Trigger init and load contexts:
			this.trigger('didInit');
			this.loadContexts();

			// Toggle the hasBeenLoaded property:
			this.toggleProperty('hasBeenLoaded');
		}
	},

	// Relations:
	addRelation: function(record, relation) {
		this.handleRelation(record, relation, 'addObject');
	},

	deleteRelation: function(record, relation) {
		this.handleRelation(record, relation, 'removeObject');
	},

  handleRelation: function(record, relation, type) {
		relation = 'loaded' + Em.String.classify(relation) + '.content';

    if (typeof record !== "undefined" && record !== null) {
			var self = this;

			if (record.get('isLoaded')) {					
				record.get(relation)[type](self);
			} else {
				record.one('didLoad', function() { record.get(relation)[type](self); });
			}
		}
	},

	// Functions:
  deleteContexts: function() {
		var self = this;

		this.eachRelationship(function(name, meta) {
			if (meta.kind === 'belongsTo') {
				self.deleteRelation(self.get(name), Em.String.decamelize(meta.parentType.toString().split('.')[1]));
			}
		});
  },

	loadContexts: function() {
		var self = this;

		this.eachRelationship(function(name, meta) {
			if (meta.kind === 'belongsTo') {
				self.addRelation(self.get(name), Em.String.decamelize(meta.parentType.toString().split('.')[1]));
			}
		});
  }

});

Notice that in the didLoad implementation, loadContexts() is called, making sure to add the record to each of the parents that the record belongs to.

Intermezzo: this is a terrible workaround and I really don’t fancy doing it this way, but I’ve run into the same limitations as you, forcing me to follow down this path.

Worth noting #1: attempting to manually change the state of records by send('becameClean') etc. will break the internal bookkeeping of changes to a record - it cannot be done that way.

Worth noting #2: having records stored in separate controllers allows for you to keep track of things such as offset, limit, and whether or not additional records are available on the server. This is great for pagination/infinite scrolling.

Closing comments:

I have great faith in what will happen to ember-data in the coming months in terms of stabilizing the library and weeding out these bugs so we don’t need to create layers upon layers on top of ember-data that rely on the triggering of events to make associations work as expected in all cases. It seems like a fragile approach at best - but hey, it works. <insert ninja coding GIF here>

Please do share any additional findings. I promise to do so myself. And hang in there! I’m sure things will improve pretty soon.