Currently there is a problem saving new embedded records in a hasMany relationship. Ember has a hard time reconciling the returned embedded records with the ones already in memory and creates duplicates.
The problem:
You have a hasMany relationship:
User = DS.Model.extend({
name: DS.attr(),
posts: DS.hasMany('post')
});
-
You create a new record within that array.
this.get('posts').pushObject(newRecord)
-
You call
this.save()
on the parent object and it embeds the new object, which has no server assigned id, in the JSON payload. -
Your server sees the embedded object and persists it, giving it an id.
-
Server responds with the embedded object which now has an id.
-
You normalize your payload so the embedded object is side-loaded (and now has an id)
{ "user": { "name": "Tim", "posts": ["1"] }, "posts": [ { "id": "1" ... } ] }
-
Ember sees this as a new record and pushes it into the store.
-
The hasMany relationship looks up the reference to this id and appends the record, so it now references the original (no id) and the new record (with an id).
At this point you have duplicate records until you call reload()
on the record. Ember has no way of knowing that these two records were supposed to be the same thing.
I have a working branch that solves this issue by sending a client specific identifier to the server (defaults to _clientId
but is configurable) in any unsaved embedded records.
The server needs to be aware of this special key and pass it back unchanged with any embedded objects.
e.g. Request:
{
"user": {
"id": "1",
"name": "Tim",
"posts": [
{
"_clientId": "ember212",
"body": "test"
}
]
}
}
Response:
{
"user": {
"id": "1",
"name": "Tim",
"posts": [
{
"id": "1",
"_clientId": "ember212",
"body": "test"
}
]
}
}
Using the fix I have implemented, Ember is able to reconcile these records and prevent duplicates in the resulting hasMany array.
I was hoping to get some feedback as to the actual implementation.
Ember Data pre 1.0 had an internal client id and a method for looking up a record by client id. It would have been perfect to use in this situation. 1.0 removed this.
Instead, I am using Ember.guidFor(record)
as an alternative, but there is no global guid lookup to find an object by guid. Because of this, I have made my own clientId to record lookup in the EmbeddedRecordsMixin called clientIdMap
. Once it reconciles a client side record it removes the entry from the map. This works - but has a small problem - Rails ActiveModelSerializer likes to send back a HTTP 204 with no content when you update a record. If this happens, the lookup entryis not removed and over time the clientIdMap
will grow and grow.
I don’t really like the idea of the clientIdMap
.
Is anyone aware of a better (and performant) method for looking up a record that has no id yet?
There are other less attractive options I’ve already pondered:
- Store an expiration time on the
clientIdMap
entry and clean out old records when they expire (yuck) - Clear the entire map every time we finish
extractSingle
– this will probably fail to work if two responses arrive out of order. - Loop through the record’s hasMany relationship, looking for the guid (wont scale and the original record is not readily available inside the extractSingle method)
Any thoughts would be welcome.
Keep in mind, I am trying to keep this solution decoupled from the ActiveModelSerializer.