I have an Ember App where some Models use Ember Data and some don't. My question relates to creating relationships between these Models and also the best way to structure the Model relationships.
Models
Currently I have the following Models:
Foods
not using Ember Data
makes $.ajax request to external API
extends a Ember.Object (see here and here for examples of the methodology)
Meals
uses Ember Data
has many Portions
Portions
uses Ember Data
hasOne Meal
hasOne Food
In my app I need a Portion to be a unique record which has a weight field. Each Portion should derive it's other values from a associated Food. A Meal should contain many Portions.
Questions
Should Portions be a Model in it's own right our should it be stored in some kind of array-like structure as a field on the Meal (eg: portions)? Consider that a Portion is not reusable and is only able to be associated with a single Meal.
If "Yes" to #1 then what could my Meal Model def look like?
As Food does not use Ember Data what's the best technique for defining a relationship between a Portion and a Food?
Ultimately the User experience should allow someone to
View a Food
Create a Portion of that Food
Associate the Portion with a Meal
View all Portions associated with a Meal
Your help is much appreciated.
Q1: Should Portions be a Model in it's own right our should it be stored in some kind of array-like structure as a field on the Meal (eg: portions)?
I'm not sure you are asking if Portions should be a model or Portion should be a model. But whatever I think the solution is to build Portion as a model and build portions relationship for Meal model. Because you have functionality to create a portion with a food. In my understanding the portion should be created without a meal (although it can link to a meal later).
Q2: If "Yes" to #1 then what could my Meal Model def look like?
The model definition is like this:
App.Portion = DS.Model.extend({
weight: DS.attr(),
meal: DS.belongsTo('meal', {async: true})
});
App.Meal = DS.Model.extend({
portions: DS.hasMany('portion', {async: true})
});
Q3: As Food does not use Ember Data what's the best technique for defining a relationship between a Portion and a Food?
It's better to still use Ember Data to define Food model, just define your custom adapter and serializer, Ember Data handles the rest. The DS.Adapter and DS.Serializer documentations are good place to start. Below is a simple example.
// Just name it "FoodAdapter" and Ember Data knows to use it for "Food".
App.FoodAdapter = DS.Adapter.extend({
find: function(store, type, id) {
// The returned value is passed to "serializer.extract" then "store.push"
return this._ajax({url: '/external/food', type: 'GET'});
},
createRecord: function() {},
updateRecord: function() {},
deleteRecord: function() {},
findAll: function() {},
findQuery: function() {},
_ajax: function(options) {
// Transform jQuery promise to standard promise
return Em.RSVP.cast($.ajax(options));
}
});
App.FoodSerializer = DS.Serializer.extend({
// Assume the json is:
// {
// "food_data": {
// "name": "XXX",
// "price": 100
// }
// }
extract: function(store, type, payload, id, requestType) {
return payload.food_data;
},
serialize: function() {}
});
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 am relatively new to Ember.js, so I am giving myself a project to figure things out.
I believe I understand the very basics. Controllers contain state-logic, while models contain model attribute-logic.
In my example, I have a collection of models. These models contain an attribute that represents an id of another model:
App.Pokeball = DS.Model.extend({
name: DS.attr('string'),
rate: DS.attr('number'),
pokemon: DS.belongsTo('pokemon')
});
I have a Controller that contains selectedPokemonId and selectedPokemon attributes. When selectedPokemonId changes, I want to automatically update all the Pokeball models.
I know its awful, but here is the function I am using to update the Models:
selectedPokemon: function(selectedPokemonId) {
var pokemonId = this.get('selectedPokemonId'),
store = this.store,
id = 1,
max = App.Pokeball.FIXTURES.length;
for (id,max; id<= max;id++) {
store.update('pokeball', {
id: id,
pokemon: pokemonId
});
}
return store.find('pokemon', this.get('selectedPokemonId'));
}.property('selectedPokemonId'),
Technically, this does what I need it to... but I am certain I am not doing this the "ember way", there has to be a cleaner way to bind the relationship between controller state and models.
Github Example Code here
Working example
I like to work directly with models as objects instead of managing record ids. Doing this greatly simplifies your code. Here's how I would accomplish this.
First, your route should return all the models you want to work with using the model hook.
The route's model hook should look something like:
model: function()
{
return Ember.RSVP.hash ({
pokeballs: this.store.find('pokeball'),
pokemon: this.store.find('pokemon')
});
}
In general you want to do store.find calls in the route model hook because they can be asynchronous (return a Promise) and the model hooks waits for promises to resolve before proceeding. This ensures your data will always be ready for your controller to work with it. More here: http://emberjs.com/guides/models/finding-records/. Note that the model we'll be working with is an object with two properties, pokeballs and pokemon, which are both collections representing all the respective objects in the store.
In your controller, instead of a selectedPokemonId, you can reference a selectedPokemon model object directly. You can then observe the change to the selectedPokemon using 'observes' and then simply set the selectedPokemon on each pokeball and save each pokeball model to persist it back to the store. If you're just using fixtures you could get away without even saving each pokeball because 'set'-ing a property on the model object is enough to change it in the store.
selectedPokemonObserver: function()
{
var thePokemonToSet = this.get('selectedPokemon');
this.get('pokeballs').forEach( function( aPokeball ) { // note you can also do this.get('model.pokeballs') since the model is an object with two properties, pokeballs and pokemon
aPokeball.set('pokemon', thePokemonToSet); //note that instead of an id, i'm setting the pokemon model object here to satisfy the belongsTo relationship
aPokeball.save(); // you might not need this if using only fixtures and not persisting to db.
});
}.observes('selectedPokemon')
Anything referencing these model objects in your templates will automatically be updated.
I think the "Ember way" to do what you want to accomplish is to use an observer instead of a property:
...
selectedPokemonObserver: function() {
var pokemonId = this.get('selectedPokemonId'),
store = this.store,
id = 1,
max = App.Pokeball.FIXTURES.length;
for (id, max; id <= max; id++) {
store.update('pokeball', {
id: id,
pokemon: pokemonId
});
}
}.observes('selectedPokemonId'),
selectedPokemon: function() {
return this.store.find('pokemon', selectedPokemonId);
}.property('selectedPokemonId'),
...
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'm new to Ember and Ember-data and am deciding whether to use Ember-Data or one of the other persistence libraries. In order to evaluate, I'm experimenting with writing a small Rails-backed app.
One of my routes can be considered similar to the Todo MVC app that is frequently used in examples.
In my template, I have a number of input fields that represent attributes within the model. Furthermore, I also have one element in the model that represents a hasMany relationship.
Models:
App.CompanyModel = DS.Model.extend
company: DS.attr()
desc: DS.attr()
contacts: DS.hasMany('company_contact')
App.CompanyContactModel = DS.Model.extend
firstname: DS.attr()
lastname: DS.attr()
...
Within my controller, I want to be able to create a new CompanyModel record (and by virtue, add one or more contacts models to it), but not have it appear within the controller's instance of the CompanyModel until I'm ready to do so.
Currently, when a user wants to add a new record, I have a component that calls an action in my controller as follows:
#set('new_company',
#store.createRecord('company')
)
This actually works fine, except for one thing. My view has to populate the individual attributes within "new_company", which it does, however, the record is immediately added to the controller's model instance and appears in the list of records; I only want the newly created record to be visible in the table once a particular action has taken place.
Instead of instantiating new_company with createRecord, I could do something like this:
#set('new_company',
Ember.Object.create
companyname: ''
desc: ''
contacts: [
firstname: ''
lastname: ''
]
)
And then do a #store.createRecord('company', #get('new_company')), however, given I've already defined my attributes in the model, it doesn't feel very DRY to me.
I'm using Ember 1.5.0 and Ember-Data 1.0.0-beta.7.
It appears I'm not the first person to have this issue (create temporarty non persistent object in Ember-Data), but it appears that Ember-Data has sufficiently changed to make all of these solutions inoperable.
Thanks for your help!
You're real issue is you're using what's considered a live collection. I'm going to assume in your route you've done something like this:
App.FooRoute = Em.Route.extend({
model: function(){
return this.store.find('company');
}
});
find with no parameters says, hey Ember Data, find me all the records that are company. Well Ember Data shoots off a request to your back-end, then returns store.all('company'). all is a live collection that will always have all the records of that type currently in the store. In your case, you are saying I want to avoid any record that is new. There are a couple of ways to handle this.
Create a static list. (You'll need to manually add/remove objects to/from this list).
App.FooRoute = Em.Route.extend({
model: function(){
return this.store.find('company').then(function(companies){
return companies.toArray();
});
}
});
Example: http://emberjs.jsbin.com/OxIDiVU/641/edit
Create a computed property that only shows records that aren't new
App.FooRoute = Em.Route.extend({
model: function(){
return this.store.find('company');
}
});
App.FooController = Em.ArrayController.extend({
savedRecords: function(){
return this.get('model').filterBy('isNew', false);
}.property('model.#each.isNew')
// shorthand this could be written like this
// savedRecords: Ember.computed.filterBy('model', 'isNew', false)
});
Then in your template you would iterate over the computed property
{{#each item in savedRecords}}
{{/each}}
Example: http://emberjs.jsbin.com/OxIDiVU/640/edit
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});
}