Two Ember objects created for single backend object?


#1

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?

I hope this makes sense!


#2

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:

  1. create comment (id : null)

  2. push sideloaded comment (id : 1)

  3. merge response (id : 1) with initial comment (id : null)

  4. 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.


#3

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.


#4

Yes, that is what I’m saying.

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().


#5

creating parent child relationships in one payload, that doesn’t result in duplicate child objects.

var attr = DS.attr,
    hasMany = DS.hasMany,
    belongsTo = DS.belongsTo;

Blogger.Post= DS.Model.extend({
  name: attr('string'),
  content: attr('string'),
  comments: hasMany('comment')
});

Blogger.Comment= DS.Model.extend({
  userName: attr('string'),
  content: attr('string'),
  post: belongsTo('post')
});

Blogger.PostSerializer = DS.ActiveModelSerializer.extend(DS.EmbeddedRecordsMixin, {
  attrs: {
    comments: { embedded: 'always' }
  }
});

Blogger.PostsNewController = Ember.ObjectController.extend({
  actions: {
    save: function() {
      var store = this.get('store'),
          post = store.createRecord('post', {name: "This is a post", content: "the quick brown fox..."});

      var comment = Blogger.Comment._create({ store: store });
      comment.set('_data', {userName: "john", content: "this is a comment"});

      post.get('comments').pushObject(comment);

      comment.destroy();

      var onSuccess = function(post) {
        console.log(post);
      };

      var onFail = function(post) {
        console.log(post);
      };

      post.save().then(onSuccess, onFail);
    }
  }
});

#6

.destroy() could work, but shouldn’t your actions be in the route :wink:

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 }
});

#7

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


#8

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

#9

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 :smile:

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.


#10

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?


#11

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
    },
  ]
}

as for the 2 methods, i’m not sure i follow,