In my Ember app, I have a nested hasMany relationships using ember-data like so workout->exercise->set.
My API has nested JSON instead of sideloaded JSON, so to fetch an existing workout I use store.find('workout', id) and override extractSingle.
My problem is when building a new workout I need to prepopulate it with exercises and sets based on a workout plan that a user is following. On the server side, I just have a /new controller action that prepopulates everything and renders a template.
Now that I'm moving to Ember I need the same functionality, but can't seem to make it work. The first thing I tried was to use Ember.$.getJSON to call a custom API endpoint in conjunction with pushPayload. This doesn't work however, because pushPayload bypasses extractSingle which means I can't convert my nested JSON into side-loaded JSON.
The prepopulation logic is very complicated so I'd prefer not to duplicated it client side and retrieve it from the API. Any other ideas on how I could accomplish this using ember-data?
For anyone trying to load embedded relationships with EmberData, check out EmberData's EmbeddedRecordsMixin.
Using Ember-CLI, you will need to create a serializer for the model with embedded relationships and add the mixin in the serializer like this:
// app/serializers/my-example-model.js
import DS from 'ember-data';
export default DS.RESTSerializer.extend(DS.EmbeddedRecordsMixin, {
Then, declare an attrs hash and include each embedded relationship with a hash for each, like this:
attrs: {
conference: { embedded: 'always' },
organizationType: { embedded: 'always' }
}
});
In my example above, conference and organizationType are both embedded in the myExampleModel model for both serializing and deserializing. That means that when I use this serializer over a RESTful API, I will get the embedded objects and I need to put the embedded objects.
The EmbeddedRecordsMixin has several options regarding each relationship, as in you have separate settings for serializing and deserializing. you can specify ids, records, or neither. embedded: always is shorthand for:
{
serialize: 'records',
deserialize: 'records'
}
It's all documented here with better examples here:
http://emberjs.com/api/data/classes/DS.EmbeddedRecordsMixin.html.
Related
I have this model in my drf backend:
class Product:
price_range = ...
I am using EmberData with the JSONApi serializer. I have just found out that JSON API requires dasherized properties. So I need to tell drf:
JSON_API_FORMAT_KEYS = 'dasherize'
And the property gets serialized in the JSON as:
price-range
Then, EmberData does its dance, and I get the Ember model property:
import DS from 'ember-data';
export default DS.Model.extend({
...
priceRange: DS.attr('number'),
...
});
(the old RESTSerializer was expecting priceRange in the JSON, if I recall properly)
So, we go from price_range -> price-range -> priceRange (which is pretty crazy if you ask me). I found all this by trial and error. For drf the corresponding settings are documented here.
Where is this documented for JSONApi, EmberData and Ember? I would like to make sure I have really understood this, and that this is also the case for relationships. The related drf config setting would be:
JSON_API_FORMAT_RELATION_KEYS = 'dasherize'
In the blog post for the Ember-Data 1.13 release the Ember data team writes:
JSONSerializer and RESTSerializer have been refactored and streamlined
to return JSON API payloads.
This means that moving forward Ember Data expects dasherized names in line with JSON API. See the JSON API recommendation for dashes in between words of member names:
Member names SHOULD contain only the characters "a-z" (U+0061 to
U+007A), "0-9" (U+0030 to U+0039), and the hyphen minus (U+002D
HYPHEN-MINUS, "-") as separator between multiple words.
And see some of the example JSON on the JSON API home page:
-"attributes": {
"first-name": "Dan",
"last-name": "Gebhardt",
"twitter": "dgeb"
},
As far as the models are concerned, the most recent documentation for Ember 2.2.0 states:
In Ember Data the convention is to camelize attribute names on a model
And the example model given with mutli-world attribute names is as expected:
export default DS.Model.extend({
firstName: DS.attr('string'),
lastName: DS.attr('string'),
isPersonOfTheYear: DS.attr('boolean')
});
While there definitely has been churn in the recommended naming conventions, I expect that at this point most of these changes are behind us. The core team has taken notice. I believe in the move to standardize around JSON API so that inter-operable and re-usable tooling can be built, but unfortunately that move came along with these changes.
In summary: Use dasherized names in your JSON and camelized names in your models.
Edit:
It seems that while the Ember Data team went with the JSON API recommendation regarding property names in JSON payloads, it is just that - a recommendation. See this discussion on GitHub regarding the dasherized naming convention for JSON API.
So let's say you have a User model which is set up to a fairly standard API. On the front end you have an Ember project which also has that User model. The normal creation call would be something like the following:
store.createRecord('user', {email: 'test#gmail.com'}).save();
This would send off a POST request to something like /api/users. However, something fairly extensive API's support is the creation of multiple models at once. So for instance, instead of the POST call just sending a single object under user: {email: 'test#gmail.com'} it would send an array of objects like users: [{email: 'test#gmail.com'}, {email: 'test2#gmail.com'}, ...].
How I have seen this handled in ember is to just do multiple creation calls at runtime. However, this is terribly inefficient and I am wondering if Ember supports saving multiple models at the same time? How would you achieve this in Ember?
You cannot save an array of models in a single POST request Ember Data as you describe it, however there is a way.
You can save a parent model which hasMany 'user' with the EmbeddedRecordsMixin, which will include either relationship ids or full records. Your serializer would look like -
import DS from 'ember-data';
export default DS.RESTSerializer.extend(DS.EmbeddedRecordsMixin, {
attrs: {
users: { embedded: 'always' },
}
});
Depending on your use case it may make sense to create a parent model only for this purpose which hasMany 'user'. If you want to use an existing model and don't always want to embed its user records there is an answer here.
If you do decide to save the models individually, you would want to do users.invoke('save'), which will trigger a POST for each model.
If you're asking specifically about Ember Data, I don't know of any way of doing that (I don't think it's possible to use any equivalent of save() on a collection/array). There could be alternative Data libraries that may work (for instance you could check Orbit.JS - which is something I haven't done yet)
The way I've done it it to have a custom endpoint on my backend that receives a certain JSON payload and creates the resources. You do it by issuing a regular ajax call, see this example (from a project of mine).
let content = //get content that you want to post
let accessToken = this.get('session.session.authenticated.token');
Ember.$.ajax({
data: JSON.stringify(content),
dataType: 'json',
method: 'POST',
url: 'path/to/my/custom/end/point',
headers: {
'Content-Type': 'application/json',
'Authorization': `Beader ${accessToken}`
}
}).then((result) => {
// Code for success
}, (jqXHR) => {
// Code for error
}).always(() => {
// Code for always/finally
});
As you can see this is all custom code, not leveraging Ember Data store or models. So far I haven't found a better answer.
EDIT: After seeing andorov's answer. I forgot to mention something. I'm using Ember Data 2.0 (JSONAPI by default) and EmbeddedRecordsMixin does not work property with JSON API
Ember : 1.13.3
Ember Data : 1.13.5
jQuery : 1.11.3
I am trying to send a JSON payload using ember-data from my EmberJS client to my server. I want to send the entire object graph to the server on saving the project, as I don't want to send multiple requests. I wouldn't mind sending multiple requests, but I am worried about what happens if one of the requests fails in the middle and the data on the server will not be correct.
I wanted to use JSONAPI (http://jsonapi.org/format/#document-compound-documents) as that is becoming the default adapter in Ember. Also, there is a few C# libraries that handle this format, so I thought it would be quite straightforward. However, after reading the spec, it seems that I cannot embed objects if they do not have an id. EmberJS also does not attach the child objects to the JSON either even though I have specified { async: false, embedded: 'always' }) on the DS.attr.
My question is: If an application is used in such a way that an object graph is created on the client side, how do you use JSONAPI format to send the entire object graph to the server? Do I have to generate ids on the client side to satisfy the JSONAPI standard? Then once they get to the server just ignore them so they get saved with an id generated by the ORM?
Here is my labelGroup model:
import DS from 'ember-data';
export default DS.Model.extend({
name: DS.attr('string'),
labels: DS.hasMany('label-model', { async: false, embedded: 'always' })
});
Here is my project model:
import DS from 'ember-data';
export default DS.Model.extend(DS.EmbeddedRecordsMixin, {
name: DS.attr('string'),
labelGroups: DS.hasMany('labelGroup', { async: false, embedded: 'always'})
});
Here is the POST that I get after doing a save() on the project:
{
"data":{
"attributes":{"name":"Project"},
"relationships":{
"label-groups":{
"data":[
{"type":"label-groups","id":null},
{"type":"label-groups","id":null},
{"type":"label-groups","id":null},
{"type":"label-groups","id":null},
{"type":"label-groups","id":null},
{"type":"label-groups","id":null},
{"type":"label-groups","id":null}
]
}
},
"type":"label-projects"
}
}
UPDATE: I tried using https://www.npmjs.com/package/ember-cli-uuid to generate client side ids which it has. However the data getting output does not include the extra objects, only a reference to their ids. I expected to see an "included" property as specified here:http://jsonapi.org/format/#document-compound-documents, but it is not there.
{
"data":{
"id":"7b4544ee-91cd-493d-8b10-52040e68c283",
"attributes":{"name":"Project"},
"relationships":{
"label-groups":{
"data":[
{"type":"label-groups","id":"08115273-e82a-4d46-93ea-232ce071fb78"},
{"type":"label-groups","id":"9ca94fe9-8077-411e-98d2-1694c6fecce4"},
{"type":"label-groups","id":"d629f1e8-7962-404d-8034-38229ab21f77"},
{"type":"label-groups","id":"c6bda655-5489-4760-847b-bf02239bb2c5"},
{"type":"label-groups","id":"f6fef249-2d1d-43f0-ba64-24b7ff8b5637"},
{"type":"label-groups","id":"a7db25bf-52c8-477b-83e4-64e7c76b072e"},
{"type":"label-groups","id":"f3b5fbb3-261a-4b3d-b481-b9352f8ce2d6"}
]
}
},
"type":"label-projects"
}
}
Ember-data has no support for what you want at the moment. So ember-data will not save your relationships data in a save payload.
But its possible to do this your own by using a custom adapter and serializer.
I strongly recommend you to checkout the API and then look into the source.
If you call .save() on your Model the createRecord method is called on your adapter.
Here serializeIntoHash on the serializer is called to serialize the Model.
serializeIntoHash calls serialize, where serializeBelongsTo and serializeHasMany is called.
Now you can just override serializeHasMany and modify the hasMany before the line:
json[payloadKey] = hasMany;
Here you have the type and the ids as they are sent by ember-data. You could just .forEach the data on the hasMany and then fetch the store for the data and build your included array.
I hope this helps you to understand the serializer and the adapter so you can modify it to do whatever you want pretty easy. Actually this is the best part about ember-data. The structure of the adapter and the serializer, which allows easy modifications.
I am using Ember Data with the RestAdapter and the following models:
Pizzas.Pizza = DS.Model.extend({
name: DS.attr('string'),
orders: DS.hasMany('order', { async: true }),
});
Pizzas.Order = DS.Model.extend({
date: DS.attr('date'),
pizzas: DS.hasMany('pizza', { async: true }),
});
I create and save a new order as follows:
var pizza1 = an existing pizza with id 1;
var pizza2 = an existing pizza with id 2;
var newOrder = this.store.createRecord('order');
newOrder.set('date', Date());
newOrder.get('pizzas').then(function(pizzas) {
pizzas.pushObject(pizza1);
pizzas.pushObject(pizza2);
newOrder.save();
});
This works well - Ember Data performs a POST on the Order model which include the ids of the pizzas in the pizzas relationship field. However, I expected that following the save, the order id would be automatically added to the orders relationship of the 2 pizza objects by Ember Data, but that appears not to be the case. This causes 2 issues:
When I ask for all the orders for a pizza, the new order does not appear (since it was never added to the relationship field of the pizza)
When a change is made to a pizza (e.g. name change) and the pizza saved, the new order relationship is lost on the server (since it was never added to the relationship field of the pizza and the PUT only includes the order ids last fetched from the server)
I solve this by amending the last line of code above as follows:
newOrder.save().then(function() {
pizza1.get('orders').then(function(orders) {
orders.pushObject(newOrder);
})
// same for pizza 2
} );
Does Ember data require that the relationship be created manually on both sides (as I am doing) or am I missing something?
I am using beta 11 plus patches from my own fork.
Persisting relationships is something you will need to manage yourself. There can't be any hard or fast rules in Ember Data about this because different servers and json apis will have different approaches to managing relationships as well as specific validation and referential integrity rules which will dictate how relationships between models should be managed. This will introduce ordering dependencies into your model persistence stategies. If you have a lax "NoSQL" type document server that has no such referential integrity requirements then things will appear easy at first, with the eventual reality of data inconsistencies, orphans, dangling references and so on.
Be careful about client side data persistence layers that claim to solve your problems, as in reality they can only work on some narrow use cases. Its really just a matter of orchestrating the save in your controllers where the knowledge and the context of what needs to be done belongs.
A strategy I have found works well in json apis is to simply manage relationships on the model that has the foreign key (ie the "belongsTo" side) and avoid returning or managing keys on the hasMany side as all those id's being passed around don't scale well when your collections grow.
It is best to look at the source for the JSONSerializer base class where models are serialized to see what it does, the RESTSerializer inherits its behaviour. You will see that the serializer just saves ids, and does not recursively cascade saves through your object graph, which would be a bad thing as you would be trying to solve the NP-Complete Hamiltonian Path Problem!. This is why I say be VERY suspicious of data layers making promises to solve your model graph persistence problem and instead just orchestrate what you know needs to be done in your controllers. This also aligns very well with dirty tracking and buffering changes (eg using a buffered proxy).
UPDATE
I forgot to add something rather obvious, but you can define your own model serializers that can side-save any related models as part of a single POST/PUT transaction (rather than using a .then approach), depending on what your server supports. Just override the relevant serialize method to include what you need. The EmbeddedRecordsMixin can also be useful if you have some very closely related models that are always created or updated together.
For example, I have Contact model, where each Contact can have one or more Names, Addresses, Emails, or Urls. And each of those are models themselves that track preference/purpose, as well as validFrom/validTo and so on.
Firstly my application serializer mixes in the embedded records support and merging of the attrs property so I can inherit serializers:
// app/serializers/application.js
import DS from 'ember-data';
export default DS.RESTSerializer.extend(DS.EmbeddedRecordsMixin,{
mergedProperties: [ 'attrs' ]
});
Then my Contact serializer specifies what models are embedded:
// app/serializers/contact.js
import AppSerializer from "./application";
export default AppSerializer.extend({
attrs: {
names: { embedded: 'always'},
addresses: { embedded: 'always' },
emails: { embedded: 'always' },
phones: { embedded: 'always' },
urls: { embedded: 'always' }
}
});
I am trying to define a model in ember from a json object that does not conform to the JSON API standard. If I define my Route like
export default Ember.Route.extend({
model: function(){
var url = "http://website.com/prequalification";
return Ember.$.getJSON(url).then(function(data) {
return data.collection.template;
});
}
});
How do I then access my data in the template. I am trying to avoid writing a custom adapter to handle the JSON. Am heading down the wrong path here?
Thanks
When you navigate to the route which is using this route (you may need to familiarize yourself with the router and routes to understand that statement, http://emberjs.com/guides/routing/defining-your-routes/) the json returned by that model hook will be available in the template using standard handlebars syntax {{property}}
Here's a simple example, note the naming convention (index template, Index route): http://emberjs.jsbin.com/