My application backend has several resources. A model is exposed for each resource.
The entry point to all other resources is through the User model. What I mean is, given a User we can find BlogPost. Given a BlogPost we can find Comments etc.
In Ember terminology, we could say:
User hasMany BlogPost
BlogPost hasMany Comment
Comment belongsTo BlogPost
By backend exposes REST APIs of the form:
GET /api/v1/users/1
GET /api/v1/users/1/blog_posts/1
GET /api/v1/users/1/blog_posts/1/comments/1
I'm trying to figure out how to use Ember Data to fetch Comment belonging to a certain BlogPost belonging to a certain User.
From what I see, if I define a typical Ember model for Comment:
App.Comment = DS.Model.extend({
...
blogPost: DS.belongsTo('App.BlogPost')
});
and in the CommentRoute I have the following:
var CommentRoute = MessageRoute.extend({
model: function(params) {
this.store.find('comment')
},
The request is sent to:
/api/v1/comments
I don't even know where to begin in order for Ember Data to use urls of the form:
GET /api/v1/users/1/blog_posts/1/comments/1
I've seen several similar questions asked (see links below), but haven't seen a definitive answer to any of them. Most of them are almost a year old when ember-data, perhaps, did not have such functionality (or so it is claimed in some of these threads).
I ask again to confirm whether ember-data has such functionality or not.
Similar Questions:
Ember Data nested Models
Canonical way to load nested resources
Deep nested routes
The best way to handle it is with links. If you don't want to do it like that, it is far from supported, and difficult to hack in (the pipeline just doesn't easily pass the information around). Personally I'd recommend rolling your own adapter in that case (Ember without Ember Data).
App.Foo = DS.Model.extend({
name: DS.attr('string'),
bars : DS.hasMany('bar', {async:true})
});
App.Bar = DS.Model.extend({
foo: DS.belongsTo('foo'),
});
json:
{
id: 1,
name: "bill",
links: {
bars: '/foo/1/bars'
}
}
Example: http://emberjs.jsbin.com/OxIDiVU/971/edit
Related
Imagine that I have two models, Author and Book.
// models/author.js
DS.Model.extend({
name: DS.attr(),
books: DS.hasMany('book')
});
// models/book.js
DS.Model.extend({
title: DS.attr(),
author: DS.belongsTo('author')
})
It would be nice to have an endpoint at
/api/authors/{authorID}/books to be able to get all of the authors books in one batch request, instead of making multiple calls to /api/books/{bookID}, but it doesn't seem that ember supports this. It is possible to do /api/books?authorID={authorID}, but that would lose some of the benefits of the store.
Is there an Ember idiomatic way of doing /api/authors/{authorID}/books? Again, the goal is to be able to make one batch request to get all books for an author instead of making one call for every book in the author's hasMany list.
For a little more context, I have the following routes structure:
// router.js
...
this.route('authors', function() {
this.route('author', { path: ':id' }, function() {
this.route('books');
});
});
...
In the 'authors' route I will load all of the authors. I do not want to synchronously load their related books, yet, as the list is potentially massive and not used on this route.
In the author route I'm using data of the author already retrieved (the name, in this case).
In the books route I would like to finally load all of the author's related books without needing to send a single request per book.
If you use modelFor() in the books route, you can get() the books for a single author.
authors author route
model(params) {
return this.store.find('author', params.id);
}
authors author books route
model() {
var author = this.modelFor('authors.show');
return author.get('books');
}
Here's an ember-twiddle: https://ember-twiddle.com/924dd7d31e0984b708f9
There is for RestAdapter but it's quite ugly. The query is then /api/books?ids[]=1&ids[]=2. But if you know you will need all authors books you can sideload them and when you call author.get('books') it won't trigger fetch anymore.
I have a notes model that I want to attach to one of two other models, customers and suppliers.
In my database I have a foreignType and foreignId field that holds the type and the corresponding ID for the customer or supplier, something like
notes: { {id: 1, body:'bar',foreignType:'customer',foreignId:100},
{id: 2, body:'foo',foreignType:'supplier',foreignId:100}
}
That is, a note can be attached to a customer or a supplier.
The convention seems to be that the field be called noteType?
I have seen a tutorial where the related type was nested in the JSON, rather then being at the root.
My ember models look like this:
//pods/note/model.js
export default DS.Model.extend({
//...
body: DS.attr('string'),
foreign: DS.belongsTo('noteable',{polymorphic:true})
});
//pods/noteable/model.js (is there a better/conventional place to put this file?)
export default DS.Model.extend({
notes: DS.hasMany('note')
});
//pods/customer/model.js
import Noteable from '../noteable/model';
export default Noteable.extend({ //derived from Noteable class
name: DS.attr('string'),
//...
});
//pods/supplier/model.js
// similar to customer
// sample incoming JSON
//
{"customer":{"id":2,"name":"Foobar INC",...},
"contacts":
[{"id":1757,"foreignType": "customer","foreignId":2,...},
{"id":1753,"foreignType": "customer","foreignId":2,...},
...],
...
"todos":
[{"id":1,"foreignType":"customer","foreignId":2,"description":"test todo"}],
"notes":
[{"id":1,"foreignType":"customer","foreignId":2,"body":"Some customer note "}]
}
How to set this up correctly, i.e. what does Ember expect?
My notes aren't attaching correctly to the customer model. They show up in the Data tab of the Ember Inspector, but the notes list of any customer is empty.
I can see several possibilities:
extend customer/supplier from DS.Model and have a property notes: belongsTo('noteable'), that would mean the belongsTo in notes isn't polymorphic, as there wouldn't be any derived classes, only Noteable itself. Not sure if ember (data) can deal with this nesting correctly.
extend from Noteable. what if I want to have other things like addresses or contacts, that can be related to customer or supplier?
create duplicate models like customernote/suppliernote, customercontact/ suppliercontact, customer/supplier/employee address. And have the backend return the filtered table/model name depending on the endpoint. I don't like to repeat myself though ....
Ember : 2.2.0
Ember Data : 2.2.1
I love how Ember doc has explained polymorphism here - https://guides.emberjs.com/v2.13.0/models/relationships/#toc_polymorphism
So, first you need to have a 'type' which will define the model to be used (your data calls it foreignType)
Next, your note model will be the polymorphic model (similar to paymentMethod model in the example above). Let me know in the comment if you need more clarification, but I think if you follow the given example, it'll be very clear.
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.
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 have these models:
Gmcontrolpanel.Offer = DS.Model.extend({
name: DS.attr('string'),
date: DS.attr('string'),
duration: DS.attr('number'),
products: DS.hasMany('product', {async: true}),
});
Gmcontrolpanel.Product = DS.Model.extend({
name: DS.attr('string'),
description: DS.attr('string'),
offer: DS.belongsTo('offer'),
variations: DS.hasMany('variation', {async: true})
});
Gmcontrolpanel.Variation = DS.Model.extend({
description: DS.attr('string'),
quantity: DS.attr('number'),
price: DS.attr('string'),
product: DS.belongsTo('product')
});
I'm trying to build a reusable interface for creating/editing an offer; i've made two separate views for inserting a product and inserting a variation;
the product view has a + and a - button to add or remove products, and the same for variation view;
the model for the route is:
model: function() {
return this.get('store').createRecord('offer');
}
What I want is that, when clicking on save button, all (offer, products and variations) are saved;
First of all: which one is the best way of implementing this? containerViews? collectionViews or {{#each}} loops?
And then, how can I create the child records and bind them to the input fields on the child views? I mean: I can create a new product record every time a productView is inserted and the same for variations, but when saving how can I get all these records and set properly all the relationships fields?
Here is a skeleton example of how to set up the relationship:
var newOffer= store.createRecord('offer', {name:....});
//or you can do newOffer.set('name',...);
var newVariation = store.createRecord('variation', {description:.....});
var newProduct = store.createRecord('product', {name:..., description:.....});
newProduct.get('variations').pushObject(newVariation);
newOffer.get('products').pushObject(newProduct);
But for saving the model and persisting it in db, there is one slight problem. Saves are per model, so even when you have the relationship set up properly when we do save on offer model, it doesnot embed the data associated with hasMany relationed models. So we could do something like this :
Note: I have read about bulk save but haven't tried it yet - you might want to give it a shot but if that didnt work then i would do save on each model from bottom up like
newVariation.save().then(function(){
newProduct.get('variations').pushObject(newVariation);
//since the variation model is already saved, it has id associated with the model
//so ember data now knows that it should set variations as variations:[id of the variation model we just saved] when sending post request for product
newProduct.save().then{ //same pattern as above }
}, function(){//handle failure}
Here the case was simple, we had just one variation and one product but you may have multiple of them. We can do rsvp.all to sync up the promises for saves but it is bit sluggish becuse you have to make separate api calls for each save and since you may have multiple variations and products, the no of ajax calls can be bit insane. One way of getting around this would be that you create your own json structure by looping through the models, and combine the models into single json and make a single post request with jQuery ajax api call, save the content in your db and then make use of pushPayload(
http://emberjs.com/api/data/classes/DS.Store.html#method_pushPayload) to load up all the data back to the store.
This is what i have done in similar situation but there might be more elgant solutions out there, so i would wait on more opinions on this.
As for the view thing, i would think you would need a view for product only, this is what im thinking:
//offer.hbs
Bunch of form elemnts to update name description
+ - buttons to add product
{{#each product}}
{{#view productView product=this}}//this is just a normal view
{{/each}}
// your template associated with productView will be like
+ - buttons to add variations to product
{{#each product.variations}}
Show form elments to update name and description
{{/each}}
How about if we give this a try in setupController
setupController:function(controller, model){
model.reload().then(function(data){controller.set('model', data);});
}
Or, what if you create an action to transition to edit mode and in that action you reload the model first and within its then hook, you do the transitionToRoute with reloaded model data. Something like:
goToEdit: function(model){
model.reload().then(function(data){transitionToRoute('offer.edit', data});
}