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

Upgraded IdentityMap and RecordMap #4664

Merged
merged 3 commits into from
Dec 13, 2016
Merged

Conversation

runspired
Copy link
Contributor

@runspired runspired commented Nov 17, 2016

When pushing records into the store (no materialization), we only needed to load the associated modelClass for "key" purposes. This PR shifts ember-data to consistently using a normalized modelName as the key, making it possible to push internal models, assemble record arrays, and (potentially in the future) even make requests without any access to the modelClass.

Step two of this effort (a separate PR) will be to enable the parsing of modelClass to avoid needing to load the classes for related models. Combined, this gives us a happy story for initial render in apps with pre-loaded data and complicated relationship graphs.

In addition, this paved an easier path forward for #4584

  • refactor typeMap => recordMap

  • refactor to using modelName instead of guid as the class identifier for recordMap.

  • refactor InternalModel to not need modelClass at instantiation.

  • remove calls to modelFor where now possible

  • remove typeMapFor

  • consider deprecation notices for InternalModel and buildInternalModel in case private APIs were abusing them and now need to refactor from modelClass to modelName.

  • (will be separate PR, but needed to complete this effort) refactor relationship setup to not need to lookup the related modelClass to prevent "pulling the string" on the entire graph.

UPDATES

I believe this is ready to ship but want to document a few things:

  • ClassMeta is not unit tested (yet) but it is implicitly tested as it exposes an identical interface to the pojo that was our TypeMap before.
  • @stefanpenner rightly points out areas we should be doing more to encapsulate things in this class, but this should be a follow up PR (that also moves to direct unit testing)
  • A temporary compromise has been made to give store to new ClassMeta(store) instead of owner in order to support the ability for it to resolve the modelClass via store.modelFor. Longer term this should be owner store.modelFor should instead use classMeta.modelClass to resolve the modelClass if necessary.
  • with this PR, _push does not require modelClass at all. The relationship PR is related to this "lazy push" story but is not required for it.

@runspired
Copy link
Contributor Author

cc @stefanpenner got the first big part done, should have this PR done by EOD, hoping to at least have started the relationship PR that needs to accompany. Would like thoughts on the new location of ClassMeta, especially as I'm envisioning this as a spot to refactor what was my intended use of Schema.

for (let i = 0; i < records.length; i++) {
record = records[i];
record.unloadRecord();
record.destroy(); // maybe within unloadRecord
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 copied this over from the existing unloadAll functionality, but I suspect this is actually buggy an we want to reset the length of this._records to 0 here. I left it as is with the guess that unloadRecord removes it from the array on it's own (which would also be why we slice).

Copy link
Member

Choose a reason for hiding this comment

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

ya, I think (but we should double check with someone else) we want to set this._records.length = 0 just after let records = this._records.slice();

@runspired runspired changed the title [WIP] A faster and lazier store._push story. A faster and lazier store._push story. Nov 18, 2016
}

/* deprecated way of accessing modelClass */
get type() {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

we still use "type" enough places that I haven't pulled the trigger on an actual deprecation notice yet, want to keep this quieter until we're closer to the end of internals cleanup.

Copy link
Member

Choose a reason for hiding this comment

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

this may be a bit zalgo, as if there is no modelClass via type call, this will be null.

Copy link
Member

Choose a reason for hiding this comment

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

Wont the getter for modelClass lazily lookup the model class or is there something I'm missing?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@bmac I think the issue raised is that if there is (literally) no modelClass to resolve, that it will always be null, and ergo we will perpetually keep trying to resolve it with every "get" of type or modelClass.

@stefanpenner
Copy link
Member

stefanpenner commented Nov 18, 2016

I am excited about this, but I would love to review this thoroughly, although I may not have cycles until tomorrow or sat. But will try for sooner

Copy link
Member

@stefanpenner stefanpenner left a comment

Choose a reason for hiding this comment

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

I'm on-board with this direction, left some thoughts some of which should be addressed. Feel free to ping me if you want to discuss more.

}

destroy() {
this._store = null;
Copy link
Member

Choose a reason for hiding this comment

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

we should add the conventional isDestroying isDestroyed fields, so when debugging we can quickly see what is up.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

👍

return null;
}
return this.store.modelFor(this.modelName);
}),
Copy link
Member

Choose a reason for hiding this comment

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

readOnly

Copy link
Contributor Author

Choose a reason for hiding this comment

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

👍

Copy link
Contributor Author

Choose a reason for hiding this comment

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

resolved

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@stefanpenner making this change made our tests fail :'(

Copy link
Member

Choose a reason for hiding this comment

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

for good reasons, or test bugs?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed

Copy link
Member

Choose a reason for hiding this comment

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

This doesn't do what you think it is. (the last arg is dropped, but i suspect we want it to actually run)

I suspect you want:

type: computed('modelName', function() {
  // ...
}).readOnly()

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Indeed, wanted this.


return typeMap;
recordMap = new ClassMeta(this, modelName);
recordMaps[modelName] = recordMap;
Copy link
Member

Choose a reason for hiding this comment

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

should this be an argument to ClassMeta ?

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 originally was going to pass in owner, but wanted to piggy-pack on the modelFor behavior (for now). I think longer term modelFor would move into this object itself.

Copy link
Member

Choose a reason for hiding this comment

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

seems good.

let id = internalModel.id;

internalModel.updateRecordArrays();

if (id) {
delete typeMap.idToRecord[id];
delete recordMap.idToRecord[id];
Copy link
Member

Choose a reason for hiding this comment

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

should we add removeRecordById to classMeta / recordMap ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes.

}

let loc = typeMap.records.indexOf(internalModel);
typeMap.records.splice(loc, 1);
let loc = recordMap.records.indexOf(internalModel);
Copy link
Member

Choose a reason for hiding this comment

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

should we this also be pushed into classMeta / recordMap rather then exposing the internals?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes.

}

get metadata() {
return this._metadata || (this._metadata = new EmptyObject());
Copy link
Member

Choose a reason for hiding this comment

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

this may also be like above

Copy link
Contributor Author

Choose a reason for hiding this comment

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

unlike above, the way this one works we don't deal with an "what if always resolves to null?" situation.

Copy link
Member

Choose a reason for hiding this comment

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

so its never inspected for empty-ness, always to append/mutate?

}

/* deprecated way of accessing modelClass */
get type() {
Copy link
Member

Choose a reason for hiding this comment

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

this may be a bit zalgo, as if there is no modelClass via type call, this will be null.

for (let i = 0; i < records.length; i++) {
record = records[i];
record.unloadRecord();
record.destroy(); // maybe within unloadRecord
Copy link
Member

Choose a reason for hiding this comment

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

ya, I think (but we should double check with someone else) we want to set this._records.length = 0 just after let records = this._records.slice();

@@ -104,13 +104,12 @@ const {
@class InternalModel
*/
export default class InternalModel {
constructor(modelClass, id, store, data) {
constructor(modelName, id, store, data) {
heimdall.increment(new_InternalModel);
Copy link
Member

Choose a reason for hiding this comment

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

should the internalModel have a direct reference to its classMeta / internalClass, that way get modelClass() can simply delegate to it.

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 agree but wanted to limit the scope of the initial PR

Copy link
Member

Choose a reason for hiding this comment

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

sounds reasonable

Copy link
Contributor Author

Choose a reason for hiding this comment

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

resolved

getReference: function(type, id) {
return this._internalModelForId(type, id).recordReference;
getReference(modelName, id) {
modelName = this._classKeyFor(modelName);
Copy link
Member

Choose a reason for hiding this comment

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

override argument value makes debugging annoying

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed, hadn't thought about that, will refactor to use a new variable name. I'd also like to bring up (as a separate PR) that it would be nice to officially deprecate support for camelCase modelName usage.

Copy link
Member

Choose a reason for hiding this comment

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

seems fine, as long as we don't forget

@stefanpenner
Copy link
Member

@runspired given this PR, what remains re: loading modelClass spuriously during cache load?

@stefanpenner
Copy link
Member

@emberjs/ember-data-contributors we would love more sets of eyes on this, we don't want to unilaterally sneak something like this in.

@bmac
Copy link
Member

bmac commented Nov 21, 2016

I'm 👍 on the direction of this change.

@runspired
Copy link
Contributor Author

@stefanpenner with these changes, you no longer load a modelClass for anything not related to

  • iterating attributes / relationships
  • adapter calls
  • serializer calls

Iteration currently only happens during create/update/delete data flows, so this means that for what we wanted as a first pass this achieves that.

@stefanpenner
Copy link
Member

@runspired nice, have you tested this in our app. I want to make sure we didn't forget anything obviously for those offending code-paths.

@workmanw
Copy link

workmanw commented Nov 21, 2016

Not surprisingly, this does seem to break some ember-data-model-fragment tests. I'll look into getting that fixed once this lands. One question though, you said there is also a coming PR for part two of this effort. Do you plan to squeeze that into the same ember-data release? If so, I'll wait to investigate until the effort is complete.

Edit: There are also some other breaking tests that seem related to serializer changes for model-fragments. Just looking for the dust to settle before I dive in.

@stefanpenner
Copy link
Member

stefanpenner commented Nov 21, 2016

@workmanw is there an effort to get model-fragments using public APIs and/or derive the needed public API's for model-fragments to be successful? We would love to not cause you extra work, but the intimacy between the two projects appears quite fragile.

@workmanw
Copy link

@stefanpenner Sorry I missed your response. To be perfectly honest, I wouldn't even know where to begin. I don't have a commanding knowledge of the ember-data internals. I'm not the only maintainer nor the original author, so I can't speak for everyone, but I would be open to trying to make this more durable.

The biggest challenges we have in ember-data-model-fragments are 1) we interact heavily with the internal model and 2) we have to monkey patch part of the ember-data public API. For example, we override DS.Model.rollbackAttributes, DS.Model. createSnapshot, DS.model.adapterDidCommit, etc. Most of the monkey patching is just trying to keep the state of the model and it's fragments in sync.

If you have idea's on how ember-data's API could be evolved to help facilitate this, I'd love to hear them.

@stefanpenner
Copy link
Member

stefanpenner commented Nov 27, 2016

@workmanw a good start is thoroughly documenting all the things that require private API usage (and why). That way those more familiar with ember-data can help with what would need to change.

@stefanpenner
Copy link
Member

@runspired let me know when/if you ready for another code-review

@runspired runspired changed the title A faster and lazier store._push story. Upgraded IdentityMap and RecordMap Dec 9, 2016

/* deprecated way of accessing modelClass */
get type() {
throw new Error('RecordMap.type is no longer available');
Copy link
Contributor Author

Choose a reason for hiding this comment

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

historically we've had type stashed away, but @igorT was skeptical it should have been there. It is "nice to have" but we should think it through more carefully. I added this error just to see if anything in the codebase depended on it being there, it doesn't appear that anything did!

@runspired runspired mentioned this pull request Dec 9, 2016
@runspired runspired force-pushed the feat/silent-push branch 2 times, most recently from a89c7ee to 22971f9 Compare December 12, 2016 01:24
@method clear
*/
clear() {
let recordMaps = this._map;
Copy link
Member

Choose a reason for hiding this comment

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

Do we need to actually clear these, our should we just "release" the dominator?

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 believe we must specifically clear them because clear is responsible for tearing down the internal models. This is a refactor that preserves existing logic for teardown, but it is absolutely possible we were doing an excessive amount of work on teardown in the past.

Copy link
Member

Choose a reason for hiding this comment

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

ah ok, lets punt.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@stefanpenner all but 2 tests still pass with use simply releasing, and they look to be two tests that are testing whether internal-model teardown things are done.

return null;
}
return this.store.modelFor(this.modelName);
}),
Copy link
Member

Choose a reason for hiding this comment

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

This doesn't do what you think it is. (the last arg is dropped, but i suspect we want it to actually run)

I suspect you want:

type: computed('modelName', function() {
  // ...
}).readOnly()

@method clear
*/
clear() {
let recordMaps = this._map;
Copy link
Member

Choose a reason for hiding this comment

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

ah ok, lets punt.

@bmac
Copy link
Member

bmac commented Dec 12, 2016

This looks good to me @runspired. Do you mind rebasing it when you have a chance?

… of guid as the class identifier for recordMap
@runspired
Copy link
Contributor Author

@bmac rebased!

@bmac bmac merged commit 63d725d into emberjs:master Dec 13, 2016
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 this pull request may close these issues.

None yet

4 participants