I have post and comment models. A post has many comments, and a comment belongs to a post. My server sideloads a post’s comments when returning a post, and similarly a post is sideloaded with comments.
I call store.createRecord('comment', {post: post}) to create a new comment for post post. The returned object is <App.Comment:ember1083:null>. The server responds to the POST with the new comment, which also sideloads the post, which in turn sideloads the new comment. It looks like another object <App.Comment:ember2000:1> is created in memory representing the new comment, in addition to the original <App.Comment:ember1083:null>. I end up with two objects in memory representing the same comment. Is that expected, or should Ember Data know that it already has a representation of the newly created comment in memory, and not create another one?
This in and of itself isn’t a huge deal, but I allow comments to be edited. After the comment is created, post.comments contains the 2nd version of the new comment <App.Comment:ember2000:1>. After editing (via a PUT to the server) that object, post.comments now contains the 1st version <App.Comment:ember:1083:null>! The 2nd version has the updates, but the 1st version doesn’t, so it appears to the user that the update didn’t persist when it actually did. Why are the objects in post.comments being switched like that?
Are you using Ember-Data, what version, and are you doing so with the RESTAdapter/serializer or did you / your team roll your own?
From experience, using a ED > 1.0 and the RESTAdapter sideloading can occur before the primary response is merged back into the initial object, which means that this would be what was happening:
create comment (id : null)
push sideloaded comment (id : 1)
merge response (id : 1) with initial comment (id : null)
Should fail because by then the ID won’t be unique. Make sure debugging is on because I believe I remember an assertion for this case that would be observable.
Hi @jthoburn, we’re using Ember Data 1.0.0-beta.4 with DS.RESTAdapter.
Are you saying that if the primary response is first merged into the initial object, the sideloaded comment won’t cause another object to be created? I have debugging on, but do not see any exceptions. I am seeing that the sideloaded comment is consistently processed before the primary response.
Solution 1: I generally would think that loading a comment puts it in a “single comment” view vs. say a list so in that situation removing the side loaded comments from the side loaded post is a good way to go about it.
Solution 2: You setup the side loading to exclude the primary comment (this should be your best solution, since the comment data is already the primary object, and since the side-loaded post should already have a comments array with a reference to the newly created comment as well, filtering it from the side-sideloaded comments would solve your problem. If you are using an out-of-the-box-into-the-fire API builder though, this (again) may not be an achievable solution without some effort.
Solution 3: Warning: this solution is not very future thinking in the sense that you will use your adapter to modify behavior best left to Ember Data. If you want to update your ED version, you will need to run tests to ensure non-breaking changes. You may also have to prompt me for more info on this, but…
In your adapter / normalizer.
step 1: get the primary record, and the primary record only. Here you can merge it with the original record (which you should still have access to), or you push it to the store. This process may require a few calls to change the record state and ensure it’s properly merged with the original that had “null”, such as didUpdateRecord().
step 2: call .sideload() with the remaining data
step 3: return a promise (a cached copy of the original promise returned by your ajax function works marvels here.)
The best way to go about solution 3 is to carefully read through and understand what ED does along each step of the way when you call .save().
.destroy() could work, but shouldn’t your actions be in the route
More importantly, your save function will only work if:
(1) her API setup uses PUT to update embedded records as well without deleting missing ones (a lot of API builders do NOT do this, and this would result in any newer comments being deleted during the save).
(2) her API returns response statuses for each of the changes it made, else there’s no way to know if the comment was persisted or not since you are no longer saving the comment directly.
A better approach might be something like
comment.save().then(onSuccess, function(error) {
if (error === 'some error message about can't push record with existing id') {
comment.destroy(); //might need to do something about it's state due to the error before this will work
onSuccess();
} else { ;//do error stuff }
});
this is only for embedded parent child relationships, specially to use with the accepts_nested_attributes in rails. so if there is an error some where in the payload it should return a 422 response during post.
since the temporary child objects (comments) are created outside the store they won’t be needed once they are pushed into the relationship. so hence the destroy function, ember will remove the obj at the end of the run loop, doesn’t matter if the save is successful or not. as far as the store is concerned there are no child objects when creating the parent object(post).
the whole purpose of creating the the child object outside the store is to prevent ember-data from creating duplicate objects. there’s a detailed post at this link describing why duplicate objs are created: https://github.com/emberjs/data/pull/999
so once the save is successful depending on the configuration of the server it should return either a list of ids or side-load the records with the parent
here’s what a create function looks in rails that could be used with the above method:
class Post< ActiveRecord::Base
has_many :comments
accepts_nested_attributes_for :comments
end
class Comment< ActiveRecord::Base
belongs_to :post
end
class PostSerializer < ActiveModel::Serializer
attributes :id, :name, :content
has_many :comments
embed :ids, include: true
end
class CommentSerializer < ActiveModel::Serializer
attributes :id, :user_name, :content, :post_id
end
class PostsController < ApplicationController
respond_to :json
def create
post= Post.new(post_params)
if params[:post][:comments]
params[:post][:comments].each do | comment|
post.comments<< Comment.new(:user_name => comment[:user_name], :content=> comment[:content])
end
end
post.save
if post.id
render json: post, :status => 200
else
render json: { :errors => post.errors }, :status => 422
end
end
private
def post_params
params.require(:post).permit(:name, :content)
end
end
side-loading comments
class CommentsController < ApplicationController
respond_to :json
def index
response = []
if params[:ids]
params[:ids].each do | id |
# 1-> find the comment belonging to the id
# 2-> if comment is present append it to the response array
end
# 3-> respond with the response array
end
end
end
Unfortunately, my app also needs to show a list of comments from different posts, along with some information about the posts. They’re not actually posts and comments in my use-case, but I figured that would be the simplest parallel to draw
I like this solution, but you’re right that it will take a little bit of mucking around in the backend, as I’m currently simply using the ActiveModelSerializer gem.
I think that making the hasMany comments relationship async solves my issue, but it means more network roundtrips.
At the end of the day, I’m curious why Ember Data doesn’t correctly merge the two records. Is it working as expected, or is it broken? Interestingly, I noticed that only one object is created if the comment is the first comment on a post!
Thanks for the various suggestions! I’ll continue to play around.
Thanks @amajdee for the sample code. I’m not terribly familiar with embedding records as I’m purely sideloading at the moment. How do the two methods compare from an Ember Data perspective? Does processing the two types of responses follow the same codepath?
actually in the sample i provided only the create part has embedded object (new comments are embedded in the post when creating a new post), after that the comments will be side-loaded with the post,
here’s what the post payload looks like. (embedded comments won’t be added to the store)
{
post: {
title: test,
content: test content,
comments: [
{
user_name: john,
content: this is a comment,
post_id: null,
id: null
}, {
user_name: jane,
content: this is a comment also,
post_id: null,
id: null
}
]
}
}
and the response (side-loaded comments in the response will be pushed to the store)
{
post: {
id: 1,
title: test,
content: test content,
comment_ids: [1, 2]
}
comments: [
{
id:1,
user_name: john,
content: this is a comment,
post_id: 1
}, {
id:2,
user_name: jane,
content: this is a comment also,
post_id: 1
},
]
}