Reusing Ids For Soft Deleted Records

I am running up against this, understandable, behavior from the store.

"Assertion Failed: 'some-record' was saved to the server, but 
the response returned the new id '32', which has already 
been used with another record.'"

My API supports soft deletes. For complicated business logic reasons, if we recreate a record and it’s the exact same (for our purposes) as the old record that was deleted, we reactivate the old/deleted record rather than create a new one.

I am seeing the error above when I delete a record (soft delete), and then recreate the record. The record is being reactivated with the same id.

As far as my users and API are concerned, there is no issue with reusing the same id and reviving a previously deleted record.

Unfortunately, Ember does not seem to like that behavior. From what I understand, the store is complaining because it thinks I am creating a record and reusing an old id, but really I am reviving the old, deleted, record.

Is there any way to totally purge a record from the store to free up that id? Or is there a better way to handle soft deleted records in Ember?

I tried unloadRecord, but that does not seem to do the job. I still see that issue so it would seem the id is still being referenced somehow by the store.

So — I don’t know what’s going on here, but I was curious, so I dug in a little bit and came across a few things that might be relevant:

  • When deleting a record, it’s purge from the store is async so there is no guarantee when that record will no longer be loaded
  • There is a method (undocumented and likely private? Maybe not even accessible from the model/store?) called destroySync that makes sure the record is destroyed synchronously. (The comment above this method looks familiar, eh?)
  • That being said, it looks like destroySync should be called if a record was marked to be destroyed when you attempted to materialize a record with the same id later

What version of Ember Data are you on? Is it possible this functionality doesn’t exist in the version you are on?

1 Like

Thanks @Spencer_Price. Your input was a huge help in my understanding what’s happening here. I’m on ember-data 2.15.0, which seems like it should support the behavior you pointed out.

I think I see why destroySync is not called in my scenario.

My record has an id of 32. Let’s say I do my soft-delete and then (after a couple seconds) I go to create a record that will result in 32 being reactivated/revived on the server.

When createRecord is invoked on the “new” record, _existingInternalModelForId is invoked to see if the record already exists in the store.

_this._internalModelsFor(modelName).get(id)

If I add some logging, I see that my deleted record is still in the store.

console.log(_this._internalModelsFor(modelName));

32 PM

Though, the .get(id) call does not return the model. The reason it is not returned is because that id in get(id) is null, because the record was deleted! I do not have the id anymore. As far as the user’s concerned, the record is gone and so there is no id available when we do that lookup.

After I save() my record, and the server returns my reactivated record 32, a different _existingInternalModelForId check does return the internalModel this time around. At this point, we have the id from the server, so it can find it in the identityMap

This is where the assertion is thrown.

The assertion fails the === test. I guess that check is done as an === to validate that the internal model is the same as the existing internal model? I don’t know when that would ever actually be true though. Maybe on updates?

One workaround might seem to be that I hold on to the ids after I soft delete, but the weird thing is that even if I manually pass 32 on createRecord, I still fail with a different error when calling _existingInternalModelForId. The record is found on create if I do that, but for whatever reason, it doesn’t pass the hasScheduledDestroy check.

Despite all of that, I think the fundamental problem is that, even after the record is destroyed async, the identityMap (from what I can tell) never gives up the reference to the deleted record. I fear I may have to hack at that map to clear the record out.

IIIiiiinteresting. Another shot in the dark: but, I think I read somewhere that Ember Data recently started using WeakMaps under-the-hood for the identity maps.

And, if that’s the case, I wonder if you may be unintentionally holding a reference to that record somewhere else which is why it’s still in the Identity Map.

A WeakMap will automagically remove items that have no other references via the garbage collector…and, if ember-data is leaning on the WeakMap / garbage collector in this case, that could explain the bug.

I’m having the exact same issue where it seems like model.destroyRecord() is not fully deleting the record.

I get the exact same error as OP when using ember-data 2.17.0-beta.1

In my case we have a model that uses a natural key as the id e.g. { id: "foo|bar" }. A record may be created and then later deleted, but then a new record created which will have the same natural key as a previously existing (but supposed to have been deleted) record. When the new record is saved I get the error above.

Due to a separate blocking bug around computed properties and hasMany fields in 2.13.x +, we are forced to stay on 2.12.x and on that version I get a different error than above which looks like:

Attempted to handle event deleteRecordon <model:foo|bar> while in state root.deleted.saved.

It’s probably the same issue but the codebase has evolved some.

Basically now I’m stuck with an app where a user can’t create/delete/create an object that uses a natural key for id which is a problem for us…

Any feedback appreciated.

Still seeing this issue on Ember 2.17.0 / Ember Data 2.17.0

My (ugly) hack at this is this method, which I invoke after any time I call deleteRecord on my adapter.

  forceDeleteRecord(type, id) {
    try {
      delete this.get('store').get('_identityMap')._map[type]._idToModel[id]
    } catch (e) {
      // Log this however you want.
    }
  }

I believe this is the same issue we’re seeing