Skip to content
New issue

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

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

Already on GitHub? # to your account

Update modelId() to Allow Polymorphic Collections with Different, Unknown, Model.idAttributes #3966

Merged
merged 2 commits into from
Dec 13, 2019

Conversation

amiller-gh
Copy link
Contributor

For Issue: #3965

Changes

  • modelId() now takes the idAttribute of the model in question as the second argument
  • modelId() will use the passed idAttribute before falling back to Collection.prototype.model.idAttribute and then 'id'
  • Retains backwards compatibility
  • Update tests to show default modelId() fallbacks
  • Update tests to show default polymorphic collection behavior

Rationale

Right now modelId(attrs) is only passed a plain-object representation of the model's attributes. This makes it difficult for application developers to create n-degree polymorphic Collection subclasses.

Contrived Example:

// A collection of mixed entity types that maintains sort order based 
// on their joinDate property

var Entities = Collection.extend({
  modelId(attrs){
    // What ID attribute to use? Not determined at runtime.
  },
  comparator: 'joinDate'
});


// These are only two entity types with different idAttributes, but assume 
// There may be up to (n) different entity types, each with different idAttributes, 
// determined by the database backing this example application.

var Company = Model.extend({
  idAttribute: 'companyId'
});

var Customer = Model.extend({
  idAttribute: 'customerId'
});

// Proceeding to add and remove Customers, Companies, etc, to the Entities
// Collection will mess up the `_byId` cache because the Entities collection
// is not aware of all the different possibilities for `idAttribute` at runtime and 
// a `modelId` method cannot be written to satisfy the needs of this app.

By making the modelId callback take the form modelId(attrs, model), where the second argument is the model in question, Backbone can retain backwards compatibility but enable the default implementation to be:

// Always give precedence to the provided model's idAttribute. 
// Fall back to the Collection's idAttribute, and then to the default value `id`.

Entities.modelId = function modelId(attrs, idAttribute){
  return attrs[(idAttribute || this.model.prototype.idAttribute || 'id'];
};

It still maintains backwards compatibility: For any implementation that is not already polymorphic, Collections will still function normally – models will have their idAttribute set when added to the Collection by the Collection's model constructor. Those that are already polymorphic, or have some type of non-standard idAttributes, will already have this modelId method overwritten.

 - modelId now takes the model in question as the second argument
 - modelId will look at the model in question's idAttribute before falling back to Collection.prototype.model.modelId and then 'id'
 - Makes basic polymorphic collection work out of the box while retaining backwards compatability
modelId: function(attrs) {
return attrs[this.model.prototype.idAttribute || 'id'];
modelId: function(attrs, model) {
return attrs[(model && model.idAttribute) || this.model.prototype.idAttribute || 'id'];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be shortened to just attrs[model.idAttribute || 'id'];?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nevermind, just saw the note about maintaining backward compatibility, sorry :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of passing the entire model, you could pass the idAttribute of the model.

Then you could more easily implement a get that would work with your example.

collection.get({companyId:1234}, 'companyId');
collection.get({customerId:4321}, 'customerId');
 get: function(obj, idAttribute) {
      if (obj == null) return void 0;
      var id = this.modelId(this._isModel(obj) ? obj.attributes : obj, idAttribute);
      return this._byId[obj] || this._byId[id] || this._byId[obj.cid];
    },
modelId: function (attrs, idAttribute) {
    return attrs[idAttribute || this.model.prototype.idAttribute || 'id'];
},

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found a major hole in this PR because of this comment, thank you!

The _byId cache stores models in the collection by both cid and id, so by modifying modelId to return the id of polymorphic elements, we run the risk of cache conflicts. Ex: A mixed entities collection containing both companies and users where there is a company with companyId: 1 and a user with userId: 1. This could cause issues.

To fix this, the _byId cache would need to track models internally by the unique combination of idAttribute and id: _byId[model.idAttribute + this.modelId(model.attributes, model.idAttribute)] = model.

However, then Collection.get will no longer be able to find models by id at all any more.

We'd need to tweak Collection.get to do something like this:

    // Get a model from the set by cid, id, model object with id or cid
    // properties, or an attributes object that is transformed through modelId.
    // When fetching by id, use the optional idAttribute if provided, otherwise the 
    // provided model's idAttribute, or the collection's template model's idAttribute
     get: function(obj, idAttribute) {
      if (obj == null) return void 0;
      idAttribute = idAttribute || obj.idAttribute  || this.model.prototype.idAttribute;
      return this._byId[obj] ||
        this._byId[idAttribute + obj] ||
        this._byId[idAttribute + this.modelId(obj.attributes || obj, idAttribute)] ||
        obj.cid && this._byId[obj.cid];
    },

This just got a little more complicated, but what do you think? Messing with how models are stored in _byId is a major change...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although, that conflicting id concern is assuming polymorphic models can have conflicting ids, an assumption that Backbone already seems to ignore: http://backbonejs.org/#Collection-model

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, Backbone as it is assumes polymorphic collections won't contain models of different types with the same id: http://jsfiddle.net/53oo2bo9/

That doesn't feel intuitive to me, but as it is now, this PR will still work fine :)

I like the idea of just passing the idAttribute, will update the PR

@craigmichaelmartin
Copy link
Contributor

👍

@amiller-gh
Copy link
Contributor Author

Another benefit of this change: Allows collections using component generators to use the appropriate idAttribute of its possible models.

http://jsfiddle.net/53oo2bo9/4/

@amiller-gh amiller-gh changed the title Update modelId() to Enable Polymorphic Collections Out of the Box Update modelId() to Allow Polymorphic Collections with Different, Unknown, Model.idAttributes Feb 25, 2016
@amiller-gh
Copy link
Contributor Author

@caseywebdev, pulling you in because of your original work on modelId: #3133

Also, wondering if it may be time to talk about this again in a separate thread: #2043 (see minor freak out on above line comment for context)

@jashkenas jashkenas merged commit b3a2813 into jashkenas:master Dec 13, 2019
jgonggrijp added a commit that referenced this pull request Feb 25, 2022
jgonggrijp added a commit that referenced this pull request Feb 25, 2022
# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants