Where most people have problems loading embedded Ember models, I have a problem with the exact opposite.
Ember throws errors when I try to parse a record that contains an embedded relationship, where the content for that hasMany relation is an empty array.
How do you make an embedded Ember Data hasMany relation optional or nullable?
I have a model..
App.Beat = DS.Model.extend({
notes: DS.hasMany('note', {
embedded: 'always',
defaultValue: []
}),
...
})
This model is an association in a Bar model, which is an association in a Track model. Those don't really matter here.
These embedded hasMany relationships get serialized with the following serializer..
// http://bl.ocks.org/slindberg/6817234
App.ApplicationSerializer = DS.RESTSerializer.extend({
// Extract embedded relations from the payload and load them into the store
normalizeRelationships: function(type, hash) {
var store = this.store;
this._super(type, hash);
type.eachRelationship(function(attr, relationship) {
var relatedTypeKey = relationship.type.typeKey;
if (relationship.options.embedded) {
if (relationship.kind === 'hasMany') {
hash[attr] = hash[attr].map(function(embeddedHash) {
// Normalize the record with the correct serializer for the type
var normalized = store.serializerFor(relatedTypeKey).normalize(relationship.type, embeddedHash, attr);
// If the record has no id, give it a GUID so relationship management works
if (!normalized.id) {
normalized.id = Ember.generateGuid(null, relatedTypeKey);
}
// Push the record into the store
store.push(relatedTypeKey, normalized);
// Return just the id, and the relation manager will take care of the rest
return normalized.id;
});
}
}
});
}
});
After successfully deserializing and loading the records in the store, somewhere in the application the bars property on a Track gets accessed. If that Bar has beats of which one of those beats do not have any notes (because it is a rest beat where no notes get played) then the following error is thrown:
"You looked up the 'bars' relationship on '' but some of the associated records were not loaded. Either make sure they are all loaded together with the parent record, or specify that the relationship is async (DS.hasMany({ async: true }))"
This error comes from the following assertion in ember-data.js:hasRelationship:
Ember.assert("...", Ember.A(records).everyProperty('isEmpty', false));
where records is the array of bars that contain beats that optionally contains notes.
So, how do I make an embedded hasMany relation optional so that it accepts an empty array of records?
I recommend using the EmbeddedRecordsMixin in a recent Ember Data beta (10 or 11) and then see if you still have an issue with embedded records.
Your application serializer:
App.ApplicationSerializer = DS.RESTSerializer.extend(DS.EmbeddedRecordsMixin,{
// probably nothing required here yet
});
And then in your Beat model serializer:
App.BeatSerializer = App.ApplicationSerializer.extend({
attrs: {
notes: { embedded: 'always' }
}
});
It turned our that it was the isEmpty on my bar model that conflicted with the property that was being tested in the Ember assertation. Renaming this property made everything work.
Related
I have a model with a Fixtures data set, so no backend involved in here. For the model, I have 22 data records. When I query it for the first time in my IndexRoute, all 22 data records are returned. No problem here.
When I leave the route, and come back later, the model hook of my IndexRoute is called again, but this time the same query does not return data.
My Model hook looks like:
model: function () {
var placeId = 0;
console.log('Index Route: Model Hook');
console.log('Getting hints for place ' + placeId);
this.get('store').find('hint', { place: placeId })
.then(
function (hints) {
console.log('Found hints', hints.get('content'));
}
);
return this.get('store').find('hint', { place: placeId });
}
As you can see, for demo purposes I always query hints with place id equal to zero. As already said, the first time it returns the data (and I can see the data in the Chrome Ember Inspector), but the second time I enter this route does not return the data (which I know is out there).
Edit:
My hint model looks basically like
App.Hint = DS.Model.extend({
title: DS.attr('string'),
// some basic boring attributes
place: DS.belongsTo('place', { async: true }) // Association with my Place Model
});
App.Place = DS.Model.extend({
title: DS.attr('string'),
// some more attributes
hints: DS.hasMany('hint', { async: true })
});
So the query {place: placeId} simply gets all those hints who have an association with a specific place. The problem is not that the query does not work - it works the first time the Index Route is triggered (and it works as I expect it to work). The problem is that all subsequent calls of index route, and all other locations that are trying to access hints, do not work anymore and always return an empty set.
Finally I found the answer. The problem seems to be related to finding the hint records through the belongsTo association with my places.
Anyway, I found this post Find record from belongsTo association in Ember.js and this is how the actual solution looks like:
model: function () {
var placeId = 0;
return this.store.find('place', placeId)
.then(function (place) {
return place.get('hints');
})
.then(function (hints) {
return hints;
});
},
I am a little confused about what { place: placeId } is supposed to be doing, since i am not sure if fixtureAdapter can mimic server queries (never tried).
That said however, if you want your route to always return all the 'hints' in your fixture data, all you should need to do is this:
return this.store.find('hint');
note: you only need to call once.
if that still does not work. try posting what your fixture data and adapter looks like.
I am attempting to save an Ember Data DS.Model after it's been updated, but when I call myModel.save(), I'm finding that Ember Data is sending the original, non-updated model instead of the updated one. I'm trying to understand why this is happening and what I need to do differently.
Here are some details. First, I have two models:
/models/OrgUser.js:
DS.Model.extend({
...
orgPerson: DS.belongsTo('org-person', { inverse: 'org-user', async: true, embedded: 'always' }),
});
Note that I am using a customized RESTSerializer (see below), so the only use of embedded: 'always' is how my custom RESTSerializer handles it.
/models/OrgPerson.js:
DS.Model.extend({
...
orgUser: DS.belongsTo('org-user'),
})
To persist these models, I'm using the RESTAdapter. In an attempt to generate a single JSON request to my API that contains both models above, I've made a single customization to the adapter. I don't think this is affecting anything, but just in case I'm missing something, here it is:
/serializers/application.js:
DS.RESTSerializer.extend({
serializeBelongsTo: function(record, json, relationship) {
var key = relationship.key;
key = this.keyForRelationship ? this.keyForRelationship(key, 'belongsTo') : key;
var data = record.get('data');
if (relationship.options.embedded && relationship.options.embedded === 'always') {
json[key] = data[relationship.key] ? data[relationship.key].get('data') : null;
}
else {
json[key] = data[relationship.key] ? data[relationship.key].get('id') : null;
}
if (relationship.options.polymorphic) {
this.serializePolymorphicType(record, json, relationship);
}
}
})
With that setup, I have a template where I update the orgPerson properties. I can confirm these are bound properties because updating their input updates their display on another part of the template in real-time. I then call an action on my controller, and within that action do the following:
/controllers/my-page.js:
export default Ember.ObjectController.extend( FormMixin, {
actions: {
submitForm: function() {
...
this.get('model') // Chrome console shows that _data.orgPerson._data.firstName has the (incorrect) old property
this.get('model').serialize() // returns (incorrect) old firstName
this.get('orgPerson.firstName') // returns (correct) updated firstName
this.get('orgPerson').get('firstName') // returns (correct) updated firstName
...
}
}
});
Any idea why I am getting two different versions of the same model? How can I serialize the correctly updated model? Thanks for any input!
SOLUTION:
Thanks (again!) to #kingpin2k, I have resolved this issue. Here are the steps I took:
My serializer was in fact the problem, and using Ember's old preserved data. I replaced the line data[relationship.key].get('data') with the line data[relationship.key].serialize() and this was fixed.
I then ran into another issue, which was that if I edited my record, did NOT save it, and then went back to my list of records, the list still showed the edit. My first thought was that I needed to update my list page's array model to show only the latest content, but there didn't appear to be any Ember facilities for this.
So I ultimately solved this by using the following code in my route. Note that because orgPerson is async: true I had to wrap my model in a promise. Note also that I had to directly call model.orgPerson versus just model.
Updated route:
actions: {
willTransition: function( transition ) {
this.controller.get('model.orgPerson').then( function( value ) {
if ( value.get('isDirty') ) {
value.rollback();
}
});
}
}
Going forward, I just want to call this.controller.get('model').rollback(), so I'm going to write a util function that traverses eachRelationship and then individually calls rollback() on any of the objects. Whew, a lot of subtlety to get this working right.
Ember Data stores the original values in the data obj. It stores modified values in _attributes obj. During a save it moves _attributes obj to inFlightAttributes obj, then after the save is complete it merges them from inFlightAttributes to data. All of this is so you can rollback your record.
When you define a property as attr it hooks up the magical get where it first checks _attributes, then inFlightAttributes, then data and returns that property's result.
function getValue(record, key) {
if (record._attributes.hasOwnProperty(key)) {
return record._attributes[key];
} else if (record._inFlightAttributes.hasOwnProperty(key)) {
return record._inFlightAttributes[key];
} else {
return record._data[key];
}
}
https://github.com/emberjs/data/blob/v1.0.0-beta.8/packages/ember-data/lib/system/model/attributes.js#L267
In your case, Ember Data doesn't know you are saving that record, and you are manually grabbing the old properties from the data obj. You'd either need to manually merge _attributes to data or trick Ember Data into thinking you'd saved it.
Does anyone know of a way to specify for an Ember model an attribute which is not persisted?
Basically, we're loading some metadata related to each model and sending that data to Ember via the RESTAdapter within the model. This metadata can be changed in our app, but is done via using an AJAX call. Once the call succeeds, I want to be able to update this value within the model without Ember sticking its nose in this business by changing the model to the uncommitted and doing whatever it does with transactions behind the scenes.
I also have the problem that this metadata, which is not data from the model's database record, is passed by the RESTAdapter back to the server, which doesn't expect these values. I am using a RoR backend, so the server errors out trying to mass-assign protected attributes which aren't meant to be attributes at all. I know I can scrub the data received on the server, but I would prefer the client to be able to distinguish between persistent data and auxiliary data.
So, to the original question: is there any alternative to Ember-Data's DS.attr('...') which will specify a non-persistent attribute?
The other answers to this question work with Ember data versions up to 0.13, and no longer work.
For Ember data 1.0 beta 3 you can do:
App.ApplicationSerializer = DS.RESTSerializer.extend({
serializeAttribute: function(record, json, key, attribute) {
if (attribute.options.transient) {
return;
}
return this._super(record, json, key, attribute);
}
});
Now you can use transient attributes:
App.User = DS.Model.extend({
name: DS.attr('string', {transient: true})
});
These attributes won't be sent to the server when saving records.
When this PR get's merged it will be possible to flag properties as readOnly. But till then there are some workarounds to this, e.g. overriding your addAttributes method in the Adapter and deal with your special properties, here an example how this could look like:
Define your Model by adding the new option readOnly:
App.MyModel = DS.Model.extend({
myMetaProperty: DS.attr('metaProperty', {readOnly: true})
});
and then on the Adapter:
App.Serializer = DS.RESTSerializer.extend({
addAttributes: function(data, record) {
record.eachAttribute(function(name, attribute) {
if (!attribute.options.readOnly) {
this._addAttribute(data, record, name, attribute.type);
}
}, this);
}
});
what this does is to loop over the attributes of your model and when it find's an attribute with the readOnly flag set it skips the property.
I hope this mechanism works for your use case.
Following this answer, to prevent a field from being serialized, override the default serializer for your model:
In app/serializers/person.js:
export default DS.JSONSerializer.extend({
attrs: {
admin: { serialize: false }
}
});
See here for the source PR. This solution works in Ember Data 2, and should work in older versions as well.
Update
This answer is most likely out of date with the current releases of Ember Data. I wouldn't use anything in my answer.
I'm answering this question for reference, and because your comment indicated that the record remains isDirty, but here is my solution for read-only, non-persistent, non-dirty attributes.
Overriding the addAtributes method in your Serializer prevents readOnly attributes from being sent to the server, which is probably exactly what you want, but you need to extend (or reopen) your adapter to override the dirtyRecordsForAttributeChange to prevent the record from becoming dirty:
App.CustomAdapter = DS.RESTAdapter.extend({
dirtyRecordsForAttributeChange: function(dirtySet, record, attrName, newValue, oldValue) {
meta = record.constructor.metaForProperty(attrName);
if (meta && meta.options.readOnly) { return; }
this._super.apply(this, arguments);
};
});
Then you can use readOnly attributes like so:
App.User = DS.Model.extend({
name: DS.attr('string', {readOnly: true})
});
user = App.User.find(1); # => {id: 1, name: 'John Doe'}
user.set('name', 'Jane Doe'); #
user.get('isDirty') # => false
This setup is working for me.
I'm working with a set of data that can potentially have duplicate values. When I initially add the data I'm using what little information I have available on the client (static info stored on the model in memory).
But because I need to fetch the latest each time the handlebars template is shown I also fire off a "findAll" in the computed property to get any new data that might have hit server side since the initial ember app was launched.
During this process I use the "addObjects" method on the ember-data model but when the server side is returned I see duplicate records in the array (assuming it's because they don't have the same clientId)
App.Day = DS.Model.extend({
appointments: function() {
//this will hit a backend server so it's slow
return App.Appointment.find();
}.property(),
slots: function() {
//no need to hit a backend server here so it's fast
return App.Slot.all();
}.property(),
combined: function() {
var apts = this.get('apppointments'),
slots = this.get('slots');
for(var i = 0; i < slots.get('length'); i++) {
var slot = slots.objectAt(i);
var tempApt = App.Appointment.createRecord({start: slot.get('start'), end: slot.get('end')});
apts.addObjects(tempApt);
}
return apts;
}.property()
});
Is it possible to tell an ember-data model what makes it unique so that when the promise is resolved it will know "this already exists in the AdapterPopulatedRecordArray so I'll just update it's value instead of showing it twice"
You can use
DS.RESTAdapter.map('App.Slot', {
primaryKey: 'name-of-attribute'
});
DS.RESTAdapter.map('App.Appointment', {
primaryKey: 'name-of-attribute'
});
But I think it is still impossible because App.Slot and App.Appointment are different model classes, so if they have same ids it won't help. You need to use the same model for both slots and appointments for this to work.
Edit
After examinig the source of ember-data, i think that you can define the primaryKey when you define your classes, like:
App.Slot = DS.Model.extend({
primaryKey: 'myId',
otherField: DS.attr('number')
});
I didn't tested it though..
Edit 2
After further reading seems that the previous edit is no longer supported. You need to use map as i wrote earlier.
I have an application logic that requires two models to have reciprocal hasMany relationships. As an example, imagine a set of GitHub issues that can be tagged with several labels.
I am trying to use an adapter that extends the default RESTAdapter. All the application works fine but the double hasMany relationship throws an exception. Digging into the code, a method inverseBelongsToForHasMany throws an exception.
So, I guess that Ember.Data does not support the association of two models with hasMany relationships in both sides and every hasMany requires an associated belongsTo. My questions are:
Is this supported and the issue is just I am doing something wrong?
If it is not supported, is it a feature planned to appear?
Is this a association type to be avoided in this kind of applications? If so, which is the best approach or workaround?
Thanks in advance
We use a similar method of creating the association object. However, instead of overriding the methods in store, we just added the join objects to the api.
so in the models we create:
App.Hashtag = DS.Model.extend({
hashtagUsers: DS.hasMany('App.HashtagUser', {key: 'hashtag_user_ids'})
});
App.User = DS.Model.extend({
hashtagUsers: DS.hasMany('App.HashtagUser', {key: 'hashtag_user_ids'})
});
App.HashtagUser = DS.Model.extend({
user: DS.belongsTo('App.User'),
hashtag: DS.belongsTo('App.Hashtag')
});
Then for the transactions we simply alter and commit the join object.
App.UserController = Ember.ObjectController.extend({
followHashtag: function(tag) {
var hashtagUser;
hashtagUser = this.get('hashtagUsers').createRecord({
hashtag: tag
});
tag.get('hashtagUsers').pushObject(hashtagUser);
App.store.commit();
}
unfollowHashtag: function(tag) {
var itemToRemove;
itemToRemove = this.get('hashtagUsers').find(function(hashtagUser) {
if (hashtagUser.get('hashtag') === this) {
return true;
}
}, tag);
this.get('hashtagUser').removeObject(itemToRemove);
tag.get('hashtagUser').removeObject(itemToRemove);
itemToRemove.deleteRecord();
App.store.commit();
});
The API creates a HashtagUser object and the follow method just adds that user to both the associated pieces.
For removal, it pops the associated objects and destroys the association object.
Although it's not as elegant as it could be, our big motivation was that when Ember Data gets updated then we should be able to transition it to a simple stock Ember Data supported version more easily than if we've messed with the Store itself.
Many to Many relationships are not yet supported in ember-data. For the moment, one possible workaround is to manually manage the join table.
A = DS.Model.extend({
abs: DS.hasMany('Ab'),
bs: function () {
return this.get('abs').getEach('b');
}
});
Ab = DS.Model.extend({
a: DS.belongsTo('A'),
b: DS.belongsTo('b')
});
B = DS.Model.extend({
abs: DS.hasMany('Ab'),
bs: function () {
return this.get('abs').getEach('a');
}
});
This is just the starting point. You need then to customize your models and adapter in order to send/receive/persist records in a working manner
For example, in our app, we introduce an { includedJoin: true } option inside the hasMany relations, and declare the join table as a JoinModel
A = DS.Model.extend({
abs: DS.hasMany('Ab', {includeJoin: true}),
...
});
DS.JoinModel = DS.Model.extend();
Ab = DS.JoinModel.extend({
... belongsTo relationships ...
});
Then in the Adapter, we override the create/update/delete methods in order to ignore the joins table lifecycle in the store
createRecords: function (store, type, records) {
if (!DS.JoinModel.detect(type)) {
this._super(store, type, records);
}
}
Finally, in the serializer, we override the addHasMany function in order to send the join data to the server as embedded ids in the parent models.
addHasMany: function (hash, record, key, relationship) {
var
options = relationship.options,
children = [];
//we only add join models, use of `includeJoin`
if (options.includedJoin) {
record.get(relationship.key).forEach(function (child) {
children.pushObject(child.toJSON({
includeId: true
}));
});
hash[key] = children;
}
}
Server-side we are using Rails with ActiveModelSerializer, so the only little-tricky-customization is when when we update the parent models, we manually manage the joins relation, and create/delete entries in the join table.