Modelling two-way symmetric relationships in Ember - ember.js

Imagine we have two entities, stored in a relational database as:
person(id, name)
friendship(id, personAID, personBID, strength)
Here we can see that two people can be friends and a "strength" (some numerical value) will be given to that friendship.
If persons 1 and 2 are friends, then we have the following entry in the friendship table:
xyz | 1 | 2 | 50
note that the following corresponding entry does not exist:
abc | 2 | 1 | 50
Only one entry is created per "friendship".
What I struggle with is how to model this in the ember app. A "person" can have many friendships, and a "friendship" relates exactly 2 people.
// models/person.js
DS.Model.extend({
name: DS.attr('string'),
friendships: DS.hasMany('friendship', {inverse: 'friends'})
});
The friendship objects are serialized as an array of
{
id: 'abc',
friends: ['1', '2'],
score: '50'
}
so:
// models/friendship.js
DS.Model.extend({
friends: DS.hasMany('person', {inverse: friendships}),
personA: Ember.computed(friends, function() {
return this.get('friends').objectAt('0');
}),
personB: Ember.computed(friends, function() {
return this.get('friends').objectAt('1');
}),
getFriend: function(id) {
if (id === this.get('personA.id')) {
return this.get('personB');
}
if (id === this.get('personB.id')) {
return this.get('personA');
}
return null;
},
score: DS.attr('number')
});
The implementation of the friendship model feels hacky for lots of reasons. First of all, a friendship does not "have many" friends. It has exactly two. However, if I change the implementation to:
DS.Model.extend({
personA: DS.belongsTo('person'),
personB: DS.belongsTo('person'),
...
});
then I'm not sure how to model the "hasMany" relationship of person => friendship. What would the 'inverse' field be set to, for instance?
The "score" field is complicating things. If that didn't exist then this would be a reflexive relation within the "person" model, but the relation has additional data and must be able to be represented as its own entity.
Also, given the situation where I want to list all of the friends of a given person, along with the strength of that friendship, the "getFriend()" function is required. This function smells a bit to me and I can't quite put my finger on why.
So my question is, what do you see as an effective way to model two-way symmetric relationships that contain additional data such as the "score" field? I can't change anything about how the data is stored in the DB, but I do have control over everything else, so I can transform the data into any structure on the way out of the DB and into the Ember client.
Ember and Ember-data 2.x

So it took me a year but I finally came back to this problem and I think I have a fairly good solution for it. I actually wrote two blog posts detailing the solution (part 1 and part 2), but let me try to summarize it here.
The solution has both a front-end and a back-end component to it.
I defined the models (in Rails, but it could be anything really) as follows:
# app/models/friendship.rb
class Friendship < ActiveRecord::Base
belongs_to :friender, class_name: Person
belongs_to :friended, class_name: Person
end
# app/models/person.rb
class Person < ActiveRecord::Base
def friendships
Friendship.where("friender_id = ? OR friended_id = ?", id, id);
end
end
I used a JSON API compliant backend and a gem called jsonapi-resources to help me implement the server response. Whatever your implementation, the important piece is to have an endpoint for each person that returns the friendships for that person (e.g /people/1/friendships).
That endpoint should return the relationship links where data could be fetched to see who the people are on both sides of the relationships:
{
"data": [
{
"id": "1",
"type": "friendships",
(...)
"attributes": {
"strength": 3
},
"relationships": {
"friender": {
"links": {
"self": "http://localhost:4200/friendships/1/relationships/friender",
"related": "http://localhost:4200/friendships/1/friender"
},
"data": {
"type": "people",
"id": "1"
}
},
"friended": {
"links": {
"self": "http://localhost:4200/friendships/1/relationships/friended",
"related": "http://localhost:4200/friendships/1/friended"
},
"data": {
"type": "people",
"id": "4"
}
}
}
},
{
"id": "2",
"type": "friendships",
(...)
}
]
}
The models on the Ember side looks as follows:
// app/models/person.js
import DS from 'ember-data';
import Model from 'ember-data/model';
export default Model.extend({
name: DS.attr(),
friendships: DS.hasMany(),
frienderFriendships: DS.hasMany('friendship', { inverse: 'friender' }),
friendedFriendships: DS.hasMany('friendship', { inverse: 'friended' }),
});
// app/models/friendship.js
import DS from 'ember-data';
import Model from 'ember-data/model';
export default Model.extend({
strength: DS.attr('number'),
friender: DS.belongsTo('person', { inverse: 'frienderFriendships' }),
friended: DS.belongsTo('person', { inverse: 'friendedFriendships' }),
});
In the route I'm fetching the person and just display their friendships in the simplest way:
<h2>Friends of {{model.name}}</h2>
<ul>
{{#each model.friendships as |friendship|}}
<li>{{friendship.friender.name}} - {{friendship.friended.name}} - {{friendship.strength}}</li>
{{/each}}
</ul>
That works, but sends out lots of xhr requests to the backend, one for each end (friender and friended) of the relationship.
You can cut down on the number of these requests by using resource linkage in the response of the friendships endpoint. If you don't use JSON API, there are surely ways to indicate which record is on the ends of a relationship (friendship.friender or friendship.friended) so that no further ajax requests need to be made to fetch them.

Related

Ember Data relationships not resolved

I'm still learning ember.js and have run into a roadblock with ember data not resolving lookup relationships in models. I have one model 'site' that will be basically a lookup table for every other model to differentiate data based on location.
At this point, I'm doing something wrong or missing a key concept - probably both... (or maybe it's the wee hour!)
Site Model (i.e. the lookup table)
import DS from 'ember-data';
export default DS.Model.extend({
code: DS.attr(),
name: DS.attr(),
});
The site model would have a hasMany relationship to all my other models (will be about 12 when complete)
Associate Model
import DS from 'ember-data';
import { belongsTo } from 'ember-data/relationships';
export default DS.Model.extend({
site: belongsTo('site'),
last: DS.attr(),
first: DS.attr(),
active: DS.attr('boolean'),
fullName: Ember.computed('first', 'last', function() {
return `${this.get('first')} ${this.get('last')}`;
}),
});
The 'associate model' will also be a lookup along with 'site' in some other models.
I'm providing data via the JSON API spec but I'm not including the relationship data because as I understand it, ember data it should be pulling down the site data using the site id attribute.
{
"links": {
"self": "/maint/associates"
},
"data": [
{
"type": "associate",
"id": "1",
"attributes": {
"site": "6",
"last": "Yoder",
"first": "Steven",
"active": "1"
},
"links": {
"self": "/associates/1"
}
}
]
}
In my template file I'm referencing associate.site which gives me an error.
<(unknown mixin):ember431>
If I use associate.code or .name to match the site model, nothing will show in the template. The code from the 'site' table is the data I really want to displayed in the template.
So the obvious questions:
Am I wrong that Ember Data should be resolving this or do I need to
include the relationship in my API response?
I realize that my belongsTo in the 'associate' model only references
site while I want site.code, so how do I make that relationship
known or access the field in my 'associate' model?
I didn't include hasMany relationship in the 'site' model because
there would be many. Do I need to do an inverse relationship in
other models? Examples I've seen don't all show the hasMany
relationships setup.
When I look at the models in ember inspector the site field is not
included in the model. Even if I wasn't getting the correct data
should it still show up?
I like ember so far, just need to understand and get over this roadblock
Update: My backend JSON library would only generate relationship links based on the current spec which would be
"related": "/streams/1/site"
but ember data does call
"related": "/sites/1"
to resolve the relationship
So #Adam Cooper answer is correct if you generate links as he answered or if you can only generate the links based on the current specification.
If you're using the JSONAPIAdapter, which is the default, you want your response to look this:
{
"links": {
"self": "/maint/associates"
},
"data": [{
"type": "associate",
"id": "1",
"attributes": {
"last": "Yoder",
"first": "Steven",
"active": "1"
},
relationships: {
"site": {
"links": {
related: "/sites/6"
}
}
}
}]
}
That will allow Ember Data to look up the site via its relationship. Right now Ember is trying to access the site model which Ember Data can't populate hence the error you're getting. As an aside you could probably do with returning an actual boolean value for active too.

Ember-data model relationships with array

I am having a hard time to understand how the model relationships works in ember-data. I understand the concept one to many or one to one or many to many, but I don't understand how to use it properly..
My API is sending me this data :
{
gameweek: [
{
commonID: '23',
content: 'blablabla',
game: [
{
commonID: '23',
gameID: 4,
title: 'first game'
},
{
commonID: '23',
gameID: 8,
title: 'second game'
}
]
},
{
commonID: '24',
content: 'blebleble'
game: [
{
commonID: '24',
gameID: 12,
title: 'another game'
}
]
}
]
}
As you can see I receive an array that contain some data and an other array.
I don't really know if how I should create my models, should I have just one model ? or multiple like this ? (correct me if its wrong) :
//gameweek.js
import DS from "ember-data";
export default DS.Model.extend({
commonID: DS.attr('string'),
title: DS.attr('string'),
games: DS.hasMany('game')
});
//game.js
import DS from "ember-data";
export default DS.Model.extend({
commonID: DS.attr('string'),
title: DS.attr('string'),
gameweek: DS.belongsTo('gameweek')
});
I would like to be able to save my arrays in the store and keep the relationships between them.
If I do a this.store.find('gameweek', {commonID: '23'} ); I would like to get also all of the game that are related the gameweek. (the commonID would be the same if they are related).
Do I have to create a custom serializer ?
So many questions, thanks for you help !
=============================
UPDATE :
I tried to extend the DS.RESTSerializer like this :
export default DS.RESTSerializer.extend(DS.EmbeddedRecordsMixin, {
extractArray: function( store, type, record ) {
var gameweek = record.predictions;
gameweek.forEach(function( entry ) {
var data = entry.games.map(function(game) {
return game.gameID;
});
entry.games = data;
});
record = { prediction: gameweek };
return this._super( store, type, record );
}
});
This is mainly replacing my game array by an array of gameID, the new array looks like this :
{
gameweek: [
{
commonID: '23',
content: 'blablabla',
game: ["4", "8"]
},
{
commonID: '24',
content: 'blebleble'
game: ["12"]
}
]
}
But I get this error :
Error: Assertion Failed: Unable to find transform for 'integer'
I am not sure what to do here.
===================================
UPDATE2:
I tried this too :
//serializers/gameweek.js
import DS from "ember-data";
export default DS.RESTSerializer.extend(DS.EmbeddedRecordsMixin, {
primaryKey: 'gameWeekID',
attrs: {
game: { embedded: 'load' }
},
extractArray: function( store, type, record ) {
// The array of object isn't at the root structure of the record
var record = record.predictions;
record = { prediction: record };
return this._super( store, type, record );
}
});
//serializers/game.js
import DS from "ember-data";
export default DS.RESTSerializer.extend({
primaryKey: 'gameID'
});
I got this error :
Error: Assertion Failed: Ember Data expected a number or string to represent the record(s) in the "games" relationship instead it found an object. If this is a polymorphic relationship please specify a "type" key. If this is an embedded relationship please include the "DS.EmbeddedRecordsMixin" and specify the "games" property in your serializer's attrs object.
In ember you dont save arrays as models you save objects as models. Ember data expects data back in a certain way so you will need to either do this server side or do it with a serializer.
You can save relationships no problem if your models are set clearly.
In the code below taken from embers site your model is a post. so when you query for a post this is what should be returned. The post has a relationship with comments. posts have many comments and comments belong to posts. In the post model there is the id for each comment that is related to that post and then the comments are included as a separate array. http://guides.emberjs.com/v1.12.0/models/the-rest-adapter/#toc_sideloaded-relationships
{
"post": {
"id": 1,
"title": "Node is not omakase",
"comments": [1, 2, 3]
},
"comments": [{
"id": 1,
"body": "But is it _lightweight_ omakase?"
},
{
"id": 2,
"body": "I for one welcome our new omakase overlords"
},
{
"id": 3,
"body": "Put me on the fast track to a delicious dinner"
}]
}
The related model files for the above structure are
app/models/post.js
export default DS.Model.extend({
title: DS.attr('string'),
comments: DS.hasMany('comment')
});
app/models/comment.js
export default DS.Model.extend({
body: DS.attr('string'),
post: DS.belongsTo('post')
});
I dont know if its just my head or its the end of the day but the above naming and structure layout is very confusing. Can you include a bit more detail or make the details less generic, it might help with trying to figure the data out.
You should have separate models for gameweek and game. Ember Data works better with more granular models, rather than trying to deal with model fields which are raw objects. This will make it easy to extend the game model in the future with additional functionality, such as computed properties and validators. So your proposed model structure is fine.
To make this work with your current JSON structure, however, you're going to have to declare that game is embedded. To do that
// serializers/gameweek.js
import ApplicationSerializer from './application';
import DS from 'ember-data';
export default ApplicationSerializer.extend(DS.EmbeddedRecordsMixin, {
attrs: {
games: { embedded: 'load' }
}
});
We inherit from ApplicationSerializer as best practice so that if it specifies things they are inherited by this model-specific serializer.
In addition, Ember Data is going to insist on a unique id for each game, so it has a way to identify it in the store. If gameId is unique, you can use that as is, but you'll have to let Ember Data know, by saying
// serializers/game.js
import ApplicationSerializer from './application';
import DS from 'ember-data';
export default ApplicationSerializer.extend({
primaryKey: 'gameId'
});
Once you do this, you will no longer refer to the id as gameId; you will refer to it as id, and you do not need to, and should not, declare it as part of your model.

Ember-data Serialize/Deserialize embedded records on 3rd level

Pardon me for coming up with this title but I really don't know how to ask this so I'll just explain.
Model: Group (has) User (has) Post
Defined as:
// models/group.js
name: DS.attr('string'),
// models/user.js
name: DS.attr('string'),
group: DS.belongsTo('group')
// models/post.js
name: DS.attr('string'),
user: DS.belongsTo('user'),
When I request /posts, my server returns this embedded record:
{
"posts": [
{
"id": 1,
"name": "Whitey",
"user": {
"id": 1,
"name": "User 1",
"group": 2
}
}
]
}
Notice that the group didn't have the group record but an id instead.
With my serializers:
// serializers/user.js
export default DS.RESTSerializer.extend(DS.EmbeddedRecordsMixin, {
attrs: {
group: {embedded: 'always'}
}
});
// serializers/post.js
export default DS.RESTSerializer.extend(DS.EmbeddedRecordsMixin, {
attrs: {
user: {embedded: 'always'}
}
});
This expects that the User and Post model have embedded records in them. However, it entails a problem since in the json response doesn't have an embedded group record.
The question is, is there a way I can disable embedding records in the 3rd level?
Please help.
Here is a good article about serialization:
http://www.toptal.com/emberjs/a-thorough-guide-to-ember-data#embeddedRecordsMixin
Ember docs:
http://emberjs.com/api/data/classes/DS.EmbeddedRecordsMixin.html
Basically, what it says is that you have 2 options 1) serialize and 2) deserialize. Those two have 3 options:
'no' - don't include any data,
'id' or 'ids' - include id(s),
'records' - include data.
When you write {embedded: 'always'} this is shorthand for: {serialize: 'records', deserialize: 'records'}.
If you don't want the relationship sent at all write: {serialize: false}.
The Ember defaults for EmbeddedRecordsMixin are as follows:
BelongsTo: {serialize:'id', deserialize:'id'}
HasMany: {serialize:false, deserialize:'ids'}

How to load hasMany in Ember?

I have two models:
App.Offer = DS.Model.extend({
name: DS.attr('string'),
description: DS.attr('string'),
products: DS.hasMany('product')
});
App.Product = DS.Model.extend({
name: DS.attr('string'),
description: DS.attr('string'),
offer: DS.belongsTo('offer')
});
And the server is answering with record and array of ids in this way (for example if the rest adapter asks for /offers/1):
{ "offer": [ { "id": 1, "name": "aaaaaaaaa", "description": "aaaa", "product_ids": [ 1, 2 ] } ] }
but now how can I get the products? I have a route like this:
App.OffersRoute = Ember.Route.extend({
model: function() {
var offer = this.get('store').find('offer', 1);
return offer
}
});
In Ember guide is written that if you want the products you should do:
offer.get('products');
Ok, but where should I put this? in the model hook? in a Controller property?
I've tried many things but I can see no network request to products?id[]=1&id[]=2 as I expected (the server is responding correctly to this request);
Can someone please give an example showing how I can find an offer, its products and use this data in my template?
If you're using the RESTAdapter your data needs to be in this format (if you don't want to return it in this format you can create a custom serializer and fix up the json).
2 differences:
the item under offer shouldn't be an array, since you were looking for a single item it should be an object
the key product_ids should be products, product_ids is the format that the ActiveModelAdapter/ActiveModelSerializer use.
JSON
{
"offer":
{
"id":1,
"name":"aaaaaaaaa",
"description":"aaaa",
"products":[
1,
2
]
}
}
The hasMany relationship should be marked as async if you're expecting it to be returned in a separate payload.
App.Offer = DS.Model.extend({
name: DS.attr('string'),
description: DS.attr('string'),
products: DS.hasMany('product', {async:true})
});
I hooked it up in jsbin below, but I didn't hook up a result from products?ids[]=1&ids[]=2 (note ids[]=, not id[]=), if you check the network tab you'll see the request being issued (but it'll crash since there is no result).
http://emberjs.jsbin.com/OxIDiVU/345/edit

How to submit a form with related model data in ember-data 1.0?

I'm creating a record from both form data and another promise (for the related record).
Here is the basic JSON I'm centered around
//appointment
{
"id": 6,
"details": "test",
"customer": 1
}
//customer
{
"id": 1,
"name": "Sle eep",
"appointments": [6]
}
My ember-data models look like this
App.Appointment = DS.Model.extend({
details: attr('string'),
customer: belongsTo('customer', { async: true})
});
App.Customer = DS.Model.extend({
name: attr('string'),
appointments: hasMany()
});
When I create an appointment it currently looks something like this
this.store.find('customer', 1).then(function(customer) {
var appointment = {
details: 'foo',
customer: customer.get('id')
}
this.store.createRecord('appointment', appointment).save();
});
The problem with the above is that my serializer doesn't do well when the form data is a promise. Is this how I should be creating records? If not, what should this create look like?
Thank you in advance
Update
After a little more looking around, it seems the async: true on belongsTo might be the issue. Just before the "createRecord" I can see the following
Object {details: "asd", customer: 1}
But when I jump into the "createRecord" method of ember-data (in the RESTAdapter) I notice that now customer is represented as a promise again (not the integer value or string value I saw just before that method was invoked)
Why don't you wait for the find to be resolved before creating the record and sending?
var self = this,
promise = this.store.find('customer', 1); //just to show it's a promise
promise.then(function(customer){
var appointment = {
details: 'foo',
customer: customer
}
self.store.createRecord('appointment', appointment).save();
},{
alert("uh oh end of the world, couldn't find customer");
});
Async isn't extremely well documented and seems to have some willy-nilly side-effects (It is all still in beta, so no major judgement from me yet). That being said, your appointments hasMany isn't defined, here's a playground for this.
http://emberjs.jsbin.com/OlaRagux/5/edit