Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Saving a new record that returns a existing record now fails. #4972

Closed
arenoir opened this issue May 4, 2017 · 50 comments · Fixed by #6272
Closed

Saving a new record that returns a existing record now fails. #4972

arenoir opened this issue May 4, 2017 · 50 comments · Fixed by #6272

Comments

@arenoir
Copy link
Contributor

arenoir commented May 4, 2017

So this is certainly an edge case but in the past was a nice feature.

I have a model address quite simple 4 attributes lineOne, city, state and zip. When creating a new address the server checks to see if a duplicate exists and if so returns the duplicate with the 201 created http code.

This all works fine unless the duplicate is already loaded. If it is loaded I get the following error.

You cannot update the id index of an InternalModel once set. Attempted to update 66.

In versions of ember data 2.11 and prior this worked without issue.

@arenoir
Copy link
Contributor Author

arenoir commented May 4, 2017

I am thinking this being an edge case I will just break into a custom ajax request.

@arenoir arenoir closed this as completed May 4, 2017
@hernanvicente
Copy link

Similar problem here, we were having this trouble too, in our case our ember application has a bank account model and the user was able to add and delete accounts, but in our API if an account was deleted by the user the backend handled to only deactivate the record in the database.

Later if the user creates again the same account, the API activates the record in the database and returns it with 201 status code and we're getting the same error, then we solved it, unloading the record from the store inside the destroyRecord function.

It's worth mentioning that we haven't this problem with ember data 2.8.1 and it starts to fail when we did upgrade to 2.13.1

Here a chunk of our controller code

import Ember from 'ember';

export default Ember.Controller.extend({
  actions: {
    save(bankAccount) {
      let self = this;
      bankAccount.save().then(function() {
        self.transitionToRoute('bank-accounts.index');
      }, function(response){});
    },
    delete(bankAccount) {
      let self = this;
      bankAccount.destroyRecord().then(function() {
        self.store.unloadRecord(bankAccount);
        self.transitionToRoute('bank-accounts.index');
      }, function(response){});
    }
  }
});

For us the trick was to unload the duplicated record that was already loaded into the store.

@arenoir arenoir reopened this Jun 7, 2017
@arenoir
Copy link
Contributor Author

arenoir commented Jun 7, 2017

I'm reopening this because I think it should be discussed more

@stevesims
Copy link

I'm seeing this Assertion Failed error too - although the circumstances are slightly different.

With our app the server will send push updates to the client. I suspect I'm seeing this when the pushed version of the data gets sent back before the save of the record gets it's results sent back (or processed). Unfortunately though I can't verify that is exactly what is actually happening, however I do know that the id being reported in the assertion failure does not match up to any records in the store before the save is called.

@arenoir
Copy link
Contributor Author

arenoir commented Jun 20, 2017

If anyone is interested here is my workaround. The adapter and serializer api are quite well thought out. 👍

@rondale-sc
Copy link
Contributor

@arenoir Why is the run.next(() => { resolve(payload) }); necessary? (the run.next specifically)

@arenoir
Copy link
Contributor Author

arenoir commented Jun 22, 2017

Didn't work without it. Moving the promise to the next run loop ensures all the removal promises finish before loading the payload.

@arenoir
Copy link
Contributor Author

arenoir commented Jun 22, 2017

@rondale-sc ideally store.unloadRecord would return a promise, but I'm guessing the use of observers is the issue.

@rondale-sc
Copy link
Contributor

@arenoir Ah, well this appears to have resolved my issue. Seems a little janky, but feels like as good a place as any. Thank you for working on this and sharing back your solution! 🍻

I think this exposes (in my app anyway) some weird bugs that were previously invisible. Good, but a bit frustrating.

@xomaczar
Copy link
Contributor

@arenoir Which ember-data version your fix works on - I am on 2.13.2 and even after unloading I still get the same error.

@xomaczar
Copy link
Contributor

xomaczar commented Jun 30, 2017

Seems like in e-d@2.13 store.unloadRecord does not remove id from internal-model-map
w/o reaching into private stuff.

// hack
store._removeFromIdMap(existing._internalModel);

@dhilipsiva
Copy link

I am not sure if this is relevant. But I suspect the jQuery version might be the culprit. The same piece of code did not work for me. But it worked for my friend.

Here are my versions:

Ember: 2.11.0
Ember Data: 2.12.2
jQuery: 3.2.1
Ember Simple Auth: 1.2.2

And here is my friend's version:

Ember: 2.11.0
Ember Data: 2.12.2
jQuery: 1.11.3
Ember Simple Auth: 1.2.2

As you can see for yourself, only the jQuery version is different.
This is the piece of code that had the error: https://github.com/appknox/irene/blob/410-implement-team/app/components/create-team.coffee

@roomle-build
Copy link

I have the same problem here! I had to use the fix from @xomaczar. Will there be a fix in ember-data or is it an issue with our code?

@juleshwar
Copy link

I've just started learning Ember. This question may sound unprofessional so bear with me.
What do I put in for 'existing' in the hack given by @xomaczar ? Because if I leave it as 'existing' my program says " 'existing' is not defined ".

@dhilipsiva
Copy link

@akajulie I believe its existing model object. If I am not wrong 😛

@juleshwar
Copy link

juleshwar commented Aug 16, 2017

@dhilipsiva It worked :)
Thanks :D

@stefanpenner
Copy link
Member

stefanpenner commented Aug 23, 2017

#5126 merged, and I back-ported to beta/release .. I'll release by tomorrow. Sorry for the delay.

@xomaczar
Copy link
Contributor

xomaczar commented Oct 3, 2017

Can this be closed then?

@pangratz
Copy link
Member

pangratz commented Oct 5, 2017

Closing since this seems resolved. Please reopen if this is still an issue.

@pangratz pangratz closed this as completed Oct 5, 2017
@villander
Copy link
Contributor

I have same issue and fix with this.get('store')._removeFromIdMap(model._internalModel);

Do you have any forecast to correct this error? Has anyone ever debugged to find the bug?

@thec0keman
Copy link

We are having a similar issue here. In the past unloadRecord was sufficient. I'm not sure if this helps clarify things or not, but in our case we are requesting and unloading the same record multiple times before unloadRecord stops being effective.

@pgasiorowski
Copy link

pgasiorowski commented Dec 7, 2017

After upgrading to 2.16 I stumbled upon this in slightly different use case where a websocket notification with payload comes in quicker then response for model.save().
What happens is that the websocket handler attempts to load the records with this.get('store').findRecord(modelClass, id) which upsets model.save() at the time it receives the data from server

EDIT: My error is

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

I guess it's something different?

@iamdtang
Copy link

Just ran into this scenario. I chose to unloadRecord in my serializer:

export default JSONSerializer.extend({
  normalizeCreateRecordResponse(store, primaryModelClass, payload, id, requestType) {
    let { modelName } = primaryModelClass;
    id = this.extractId(primaryModelClass, payload);

    if (store.hasRecordForId(modelName, id)) {
      let record = store.peekRecord(modelName, id);
      store.unloadRecord(record);
    }

    return this._super(store, primaryModelClass, payload, id, requestType);
  }
});

@ghost
Copy link

ghost commented Dec 12, 2017

We are seeing a similar issue in our application. We are using the latest stable ember-data (2.17.0). I am going to follow up in the ember data slack to determine how we can collaborate getting this fixed as it breaks many things in our app.

@jamesdixon
Copy link

@efx did you get any feedback from the folks in the ember data channel?

@ghost
Copy link

ghost commented Dec 18, 2017

@jamesdixon I did not ( original question ). I have searched through the repo and found many different unloadAll issues. I started a gist to track them. I and my team would like to contribute resolving the issues. I think the biggest need is reproducing with failing tests or ember-twiddle's. At some point, I think it would be most productive to touch base with a core team member so we determine the best approaching to fixing the underlying bug(s).

@erichaus
Copy link

erichaus commented Dec 25, 2017

fyi I have this same issue that breaks after 2.12.X and I've brought it up here #5237, hoping we get some movement on this as it's a blocker for us launching our app

@erichaus
Copy link

erichaus commented Dec 25, 2017

@bmac @hjdivad the solution from @villander resolved the issue but seems like a hack, any feedback on this?

this.get('store')._removeFromIdMap(model._internalModel);

the block of code:

  _removeFriendship: task(function* (friendship, isDelete=false) {
    if (isDelete) {
      yield friendship.deleteRecord();
    } else {
      yield friendship.destroyRecord();
    }

    friendship.unloadRecord();

    // hack to prevent unload record on existing internal model issue.
    this.get('store')._removeFromIdMap(friendship._internalModel);

  }).drop(),

@GCheung55
Copy link

I'm seeing this on 2.18.0 in development but not production build.

store.unloadRecord(record) works to resolve the issue but seems like destroyRecord should be doing the unloading.

@erichaus
Copy link

fyi it did work prior to 2.12.X but for now this line let us move along this.get('store')._removeFromIdMap(friendship._internalModel);

@runspired
Copy link
Contributor

Closing this issue as it is effectively identical to #5237 in that users must implement a client-id solution to avoid trolling themselves by accidentally creating multiple instances of models with the same ID.

Ultimately both this and #5237 and manifestations of #1829

@erichaus
Copy link

@runspired can you elaborate? First time hearing about client-id solution, why wouldn't ember-data be handling this as it did pre 2.13.X

users must implement a client-id solution to avoid trolling themselves by accidentally creating multiple instances of models with the same ID.

@runspired
Copy link
Contributor

@erichonkanen this didn’t work pre 2.13 either, we simply didn’t assert it. The result was that you’d end up with two separate records in separate states.

#1829 has details of how to solve this issue long term. It can be fully implemented in userland, and primarily has not been implemented in ED because the answer is API specific.

@erichaus
Copy link

@runspired something happened in 2.13.X then, maybe it was creating duplicate records but functionally still working? when we were on 2.12.X our app worked fine but once we moved past it I had to implement that _internalRecord hack otherwise it doesn't work.. have talked to other ppl that had same issue at emberconf and consensus was "something broke after 2.12"

I'll take a look at how to implement the client-id in the mean time thanks!

@iriand
Copy link

iriand commented Mar 21, 2018

I like @skaterdav85 suggestion with serializer, but it has an issue: unloadRecord is async and returns promise so in case server returns the same ID you have to wait for unloadRecord finished

export default JSONSerializer.extend({
  normalizeCreateRecordResponse(store, primaryModelClass, payload, id, requestType) {
    let { modelName } = primaryModelClass;
    id = this.extractId(primaryModelClass, payload);

    if (store.hasRecordForId(modelName, id)) {
      let record = store.peekRecord(modelName, id);
      store.unloadRecord(record).then(() => {
        return this._super(store, primaryModelClass, payload, id, requestType);
      });
    } else {
      return this._super(store, primaryModelClass, payload, id, requestType);
    }
  }
});

@lolmaus
Copy link
Contributor

lolmaus commented Mar 21, 2018

@iriand, AFAIK, you can't run super asynchronously.

@iriand
Copy link

iriand commented Mar 21, 2018

@lolmaus I have found both codes not working properly. Found that ID is not extracted, so the record wasn't unloaded right. Also, after a deeper review found that unoadRecord doesn't return promise.
Extracting ID value from payload resolved the issue using code from @skaterdav85

@billdami
Copy link

@skaterdav85 looking to use a solution like you posted above, using the serializer to unload the local model from the store if the id already exists, however this doesn't seem to work in a situation where the response for POST contains a side-loaded model which has an embedded relationship to the created model, such that the JSON for the newly created model is duplicated twice in the response data (yes, this isn't very efficient, but we can't change the API response at this point), e.g.:

//POST /foo

{
    "foo": {
        "id": 999,
        "name": "..."
    },
    "bar": {
        "id": 123,
        "name": "...",
        "foos": [
            {"id": 777, "name": "..."},
            {"id": 888, "name": "..."},
            {"id": 999, "name": "..."}
        ]
    }
}

(where foo is the newly created model)

Here, it appears that the bar model gets pushed into the store first, including its embedded child models, which includes the newly created foo model of ID 999.

Any ideas where to override/extend ember-data's logic so this type of response can be allowed, or at least silently swallow the "XXX id is already used" exception and treat it as a successful request?

@shidel-dev
Copy link

@billdami I am having the same issue as you, but with JSONAPI.

Given a request to create a "child" record. the response is like.

      {
        data: {
          id: "1",
          type: "child",
          attributes: {
            id: "1",
            name: "child1"
          }
        },
        relationhips: {
          parent: {
            type: "parent",
            id: "2"
          }
        },
        included: [
          {
            id: "2",
            type: "parent",
            attributes: {
              name: "parent"
            },
            relationships: {
              children: [{id: "1", type: "child"}]
            }
          },
          {
            id: "1",
            type: "child",
            attributes: {
              id: "1",
              name: "child1"
            },
            relationships: {
              parent: {
                id: "2",
                type: "parent"
              }
            }
          }
        ]
      }

notice that the child is in the included records, and also in data.
I have tried multiple 2.X ember data versions, and none seem to support this correctly.

in older ember-data versions the record ends up in the store twice, in newer versions it raises an error.

@g-cassie
Copy link

g-cassie commented Apr 7, 2020

I just wrestled with a similar issue on Ember Data 3.17 with the new identifiers API @runspired landed above. On 3.17 it seems like if you create a record through store.createRecord() and then delete it and recreate it again with store.createRecord().

However, if to create records, you do something like below, this does not work so well.

adapter.ajax('/api/my_special_endpoint_that_makes_a_widget', 'POST', {...}).then(data => {
   store.pushPayload(data);
})

I set out to make it so when a record is deleted, it's as though it never existed and here is what I came up with.

model.destroyRecord().then(() => {
        model._internalModel.dematerializeRecord();
        model._internalModel.destroySync();
        // something happens in the runloop after this which will revive the identifier in the identifierCache if you don't wait for the next loop
        Ember.run.next(() => {
          model.get('store').identifierCache.forgetRecordIdentifier(model._internalModel.identifier);
        });

This seems to make things work as expected. Very early stages of figuring this out so there may be more problems lurking but thought I'd post it up here to see if anyone has done anything similar or whether this approach creates any major problems I haven't discovered yet.

@runspired
Copy link
Contributor

runspired commented Apr 7, 2020

@g-cassie feel free to reach out on discord, I suspect the issue is much simpler to resolve than this and can be resolved without using any of these private APIs.

Also I think you are hitting the issue where destroyRecord does not also unload. You might need to just unload.

@g-cassie
Copy link

g-cassie commented Apr 7, 2020

Just a quick update for anyone who stumbles across this. There is a known issue where destroyRecord does not actually unload the record so doing the following may help with clearing out old models (and therefore avoiding issues when new models recycle ids)

model.destroyRecord().then(() => {
   model.unloadRecord();
});

In our particular case, we hit on an unrelated bug where due to our model having two async relationships, unloadRecord does not successfully remove the record from ember-data. This causes an error when we create a new record with the same ID. Once this bug is fixed, the destroy/unload combo should do the trick. Until then, my hack above does seem to work.

Big thanks to @runspired for helping get to the bottom of this.

@driesdl
Copy link

driesdl commented Feb 18, 2021

@g-cassie is this bug fixed and in what version?

@erichaus
Copy link

@g-cassie running unloadRecord() did help prevent an issue where recreating a record using same natural key would throw upon record.destroyRecord() with "in state root.deleted.saved"

Still seeing a different issue where recreating a record that exists in AnotherRecord.hasMany() will duplicate and result in two of the same record appearing (one being stale/old?)

ember-data 3.20.5

For now I'm just adding filtering when I display that hasMany in a template

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.