jashkenas / backbone

Give your JS App some Backbone with Models, Views, Collections, and Events
http://backbonejs.org
MIT License
28.09k stars 5.39k forks source link

there is a way to add several models with "same" id to a collection #4205

Closed taburetkin closed 1 year ago

taburetkin commented 5 years ago

default normal behavior:

const coll = new Backbone.Collection();
coll.add({ id: 1 });
coll.add({ id: 1 });
coll.add({ id: 1 });
console.log(coll.lenght); //1

but, if we did that with model having id in defaults then all models will be added to a collection

const Model = Backbone.Model.extend({
  defaults: { id: 0 }
});
const coll = new Backbone.Collection([], { model: Model });
coll.add({  });
coll.add({  });
coll.add({  });
console.log(coll.length); // 3

working fiddle: https://jsfiddle.net/gkpLoaqx/

maybe id field from defaults should not be applied on model initialize?

prma85 commented 5 years ago

I was looking at it. Logically, you shouldn't be able to add more than one item with the same id, since the ID is unique. When you are sending the id attribute, the Backbone verifies if the id already exists or not before trying to add it. If it exists, nothing is done. The problem is that if you don't send this attribute, the Backbone does not do this verification and then, create the models with the default values, creating the bug.

To fix it, need to do the verification when creating the model and not when it is doing the add function.

taburetkin commented 5 years ago

i suppose, that attribute defined in idAattribute for a model can not be part of defaults because of the id nature . and i think that defaults should be transformed before applying to models attribute, something like this _.omit(defaults, this.idAttribute) probably this solution is not good enough in performance terms.

i can suggest this small solution link to model constructor: https://github.com/jashkenas/backbone/blob/27f7d41de1f64f6662a96f44531d7518953b1e07/backbone.js#L406

i think, that this part can be replaced with this few lines

    var defaults = this.getDefaults();
    attrs = _.defaults(_.extend({}, defaults, attrs), defaults);
    ...
},
getDefaults() {
    var defaults = _.result(this, 'defaults');
    delete defaults[this.idAttribute]; // or defaults = _.omit(defaults, this.idAttribute);
    return defaults;  
}
jgonggrijp commented 1 year ago

I've been investigating this issue, and I realized that the problem is not in Backbone, but in the hypothetical user violating the preconditions of the framework.

If you pass a raw hash of attributes to a collection, the collection must be able to establish, purely based on that hash, whether it represents a model that is already in the collection or not. It does that by passing the hash through modelId. If the idAttribute is not in the hash but in the Model.defaults, this approach does not work (unless Model.defaults is a function that is guaranteed to return a unique idAttribute on every invocation).

I could delete defaults[this.idAttribute], but if this.defaults is a plain object, that will modify the object in place. It would also be a breaking change for anyone who does not put their models in a collection and who (for whatever weird singleton pattern) actually wants all their model instances to have the same id.

I could do an _.omit (or even a _.extend({}, defaults) followed by a delete), which avoids modifying this.defaults in place but which is a disproportionally expensive safeguard against a situation that is arguably the responsibility of the user to prevent. It also has the same breaking effect for hypothetical users doing weird singleton patterns.

While the hypothetical singleton pattern thing is weird, that use case is more valid than one in which the idAttribute is defaulted to a fixed value and the user is also adding instances of the model to a collection.

I could do a hardcore refactor of Collection.set and Collection._prepareModel to prevent the aliasing models situation, but that would cause an entire cascade of breaking changes, all while the user shouldn't be doing these things in the first place.

Instead of all these things, I will simply submit a PR to document this gotcha in Model.defaults.

prantlf commented 1 year ago

I use a modified Backbone, which allows disabling the unique ID feature and enables adding multiple models with the same ID to the same collection:

idAattribute: null

A breaking change in Backbone 1.4 made this code change mode complicated. It introduced mandatory polymorphism - using idAttribute from single model instances. Previous versions used idAttribute from this.model.prototype and supported mandatory homogenous collections. I added polymorphic boolean to support polymorphism, otherwise remain homogenous.

modelId: function (attrs, idAttribute) {
  if (idAttribute === undefined || !this.polymorphic) {
    idAttribute = this.model.prototype.idAttribute;
  }
  return idAttribute !== null ? attrs[idAttribute || "id"] : null;
},
jgonggrijp commented 1 year ago

@prantlf That is interesting. But why mention it here? What was the breaking change in Backbone 1.4?

prantlf commented 1 year ago

Because this topic is: "there is a way to add several models with "same" id to a collection". The breaking change was the now mandatory polymorphism as I wrote above.

The code allowing not unique IDs like the code above, but for Backbone older than 1.4:

modelId: function(attrs) {
  var idAttribute = this.model.prototype.idAttribute;
  return idAttribute !== null ? attrs[idAttribute || 'id'] : null;
},
jgonggrijp commented 1 year ago

@prantlf I do think that your reply is about a different issue. The current issue is about modelId being broken when a non-unique idAttribute is sourced from the model's defaults. This would be problematic regardless of whether modelId prioritizes model.idAttribute or this.model.prototype.idAttribute, which seems to be what your issue revolves around.

Are you just pointing out that a breaking change was made in 1.4, or do you also mean to request another change? In the first case, I will acknowledge that it was a breaking change. The author intended it to be a backwards-compatible enhancement, but changed existing tests, which is generally a clear indication of breaking changes. See https://github.com/jashkenas/backbone/pull/3966.

In the second case, please check out #3966 and the other issues linked from there. You might find the comments in there illuminating, and you might find a more appropriate place to respond as well. If you still want to suggest changes to Backbone after that, I encourage you to open a new ticket or pull request.

prantlf commented 1 year ago

It appears that I misunderstood this issue. I thought and it still kind of looks to me that it's about using the id attribute with values that are not unique, which isn't possible with the vanilla Backbone.Model. That's why I thought that a code change would be helpful and put it here. I'm sorry for the trouble. Thanks for your patience and looking up the original pull request.

I'm not sure if disabling the ID with idAttribute:null should be included in Backbone and that's why I haven't opened a pull request for it. I need it in my codebase, which hasn't always evolved in the right direction, and I'm not so happy about it. But it works.

jgonggrijp commented 1 year ago

Please feel welcome to open a new ticket for your usecase. Maybe we can come up with a better solution!