Closed (fixed)
Project:
Drupal core
Version:
8.7.x-dev
Component:
entity system
Priority:
Normal
Category:
Task
Assigned:
Unassigned
Issue tags:
Reporter:
Created:
6 Dec 2017 at 13:08 UTC
Updated:
5 Dec 2018 at 17:29 UTC
Jump to comment: Most recent, Most recent file
Comments
Comment #2
amateescu commentedThis should do it.
Comment #4
amateescu commentedEasy fix.
Comment #5
amateescu commentedI discussed this issue briefly with @Berdir in IRC and he proposed a
pre_loadhook instead, which would allow modules for example to load a pending revision of an entity before the default revision is loaded by the storage, similar to how the static/persistent caching logic works.This would mean there won't be any "swapping" of revisions anymore, and we would simply have the pending revision returned along with default revisions in case of a multiple load.
Comment #6
larowlanHi, can you update the issue summary to indicate why this is preferred over the implementing hook checking $entity->isDefaultRevision()?
I assume there is a reason, e.g. performance or cache or similar. Its not clear why a new hook is preferred.
Couple of minor observations - thanks
Any reason not to call ModuleHandlerInterface->invokeAll()?
Or failing that ModuleHandlerInterface->invoke()
Can you elaborate more on this?
Comment #8
amateescu commentedThanks @larowlan for the review!
Here's a new patch with the approach described in #5, which also fixes the two review points from #6.
No interdiff because it was written from scratch.
Comment #10
amateescu commentedGotta love unit tests..
Comment #11
amateescu commentedTesting out this patch in combination with #2924218: Clearing the persistent entity cache every time we switch between workspaces is super wasteful and #1596472: Replace hard coded static cache of entities with cache backends revealed that we need special handling for pre-loading in
\Drupal\Core\Entity\ContentEntityStorageBase::loadUnchanged, because that method tries to get entities directly from the persistent cache before going through the regular entity load process.Comment #12
amateescu commentedOne small problem that I have with this patch is that "pre_load" is inconsistent with other hook names, like "presave" and "predelete". Should we change it to "preload"?
Comment #13
timmillwood@amateescu yes 😉
Comment #14
amateescu commentedYeah, the more I think about it, the more I dislike 'pre_load'. Done :)
Comment #15
timmillwoodUbernit: move the comment down to precede the actual line calling hook_entity_preload().
Is this tested?
Also, does this mean
$storage->loadMultiple()will load all entities without going through hook_entity_preload()?Comment #16
berdir#15.2 The hook is called when you do loadMultiple(), that's exactly what this code is about. Imagine you have 10 nodes, and 2 of them have a workspace association and you load them, then the storage needs to load the other 8 afterwards. Means an additional query for that but we can't do much about that. Don't know about test coverage.
This looks pretty good to me.
Was wondering about the order of arguments for hook_entity_preload(), seems a bit strange to have $entity_type_if afterwards but that's consistent with hook_entity_load().
Also, we might want to profile this. The new hook is called for all entities, not just content entities, so that's quite a few additional hooks being called I think. What I'm wondering is if there's really a use case for this or is it something we should only support on content entities?
Comment #17
amateescu commentedHere's a 8.5.x version of this patch, without the workspace bits, just for manual performance testing purposes.
Comment #18
amateescu commentedHere's a new test-only patch which also contains a module that counts hook invocations for
hook_entity_preload(),hook_entity_load()andhook_entity_storage_load().Testing steps: enable the
entity_test_preloadmodule, visit some pages of the site and then run this drush command to see the results:Comment #19
amateescu commented@timmillwood did a bit of profiling using the test module from #18 on a site with ~9000 entities, and the results show that @Berdir was right to question the performance implications of this new hook being fired for all entities.
Patch from #18:
Patch modified locally to only run for content entities:
So here's an update to #14 which makes this hook fire only for content entities by default.
Comment #21
amateescu commentedSome tests change are no longer necessary :)
Comment #22
hchonovSo using the storage load method, which loads an entity in its default revision leads in that case to
$storage->load($id)->isDefaultRevision() === FALSE?Additionally if you do this then it becomes impossible to load the entity in its default revision.
Moreover can this work together with #2620980-109: Add static and persistent caching to ContentEntityStorageBase::loadRevision() .2 :
?
Comment #23
amateescu commentedThat's the basic architectural foundation of the workspace module, that it gives you a workspace-specific revision (if it exists) instead of the default one, when you are in a non-default workspace.
That will be possible after #2968452: Add a way to execute a function in the context of a specific workspace, by executing the storage load in the context of the
liveworkspace.When you are in a non-default workspace, for almost all purposes, the default revision should always be replaced with the workspace-specific revision, if one exists.
Comment #25
hchonovOk, then I understand. I am afraid that this might introduce some bugs when used, so it will be more of a feature - use it with caution.
I think it would be useful to pass the information, that now we are loading the unchanged entity. This will be needed when #2620980: Add static and persistent caching to ContentEntityStorageBase::loadRevision() is ready, as it introduces entity revision cache and a
loadRevisionUnchangedmethod. But knowing this I think it would be best to provide this information to the hook implementations here instead of doing it later, so that the adjustment will be as easy as possible.Comment #26
amateescu commentedNot sure I understand what's the suggestion here :) Anyway, something that I forgot to mention in #23 is that "revision load" queries are not modified at all when you are in a non-default workspace, so
$storage->loadRevision(5)will always give you the revision with ID 5 regardless of the currently active workspace.Comment #27
hchonovI've meant that we have to pass the information about loading the unchanged entity to the hook implementations. So in your case probably you'll have to load the revision through
loadRevisionUnchangedinstead throughloadRevision.Comment #28
amateescu commentedSorry, I still don't understand what #25 is about..
Comment #29
hchonovWhen we add a static cache for entity revisions to the storage, then
loadRevisionwill be returning the entities from that static cache and not like now always new unchanged objects.If there is an implemented
hook_entity_preload, which is preloading an entity by loading it throughloadRevisionthen we'll always get the entity revision from the static cache.Later during the saving process we need the unchanged entity and we use
loadUnchangedto retrieve it. Now the implementedhook_entity_preloadshould not return the entity revision from the static cache, but a new unchanged object. If the hook implementation returns the entity revision from the static cache then you'll be unable of detecting changes because e.g.CEB::hasTranslationChangeswill be comparing the same entity object with itself. So insideCEB::hasTranslationChanges$this === $originalwill evaluate toTRUE, but it shouldn't. In order to solve this problem implementations ofhook_entity_preloadshould have the information whether they are being called fromloador fromloadUnchangedand in the second case load the unchanged entity revision instead the one from the static cache. For this to happen we need to pass one more parameter$unchangedto the hook implementations.Comment #30
amateescu commentedThanks, that's much more helpful :)
As far as I see,
\Drupal\Core\Entity\ContentEntityStorageBase::loadUnchanged()resets the static cache before doing any other operation, so isn't the job of #2620980: Add static and persistent caching to ContentEntityStorageBase::loadRevision() to also reset the static revision cache for the given$entity_id? That way we won't get into the problem described in #29.Comment #31
amateescu commentedBump! It would be nice to have this performance improvement for Workspaces in 8.7 :)
@hchonov, does my comment in #30 address your concerns about the usage of the (future) static revision cache?
Comment #32
amateescu commented@tstoeckler seems to agree with what I said in #30: #2620980-114: Add static and persistent caching to ContentEntityStorageBase::loadRevision()
Comment #33
catchShouldn't this have an accessCheck(FALSE)?
If preLoad is only in ContentEntityStorageBase shouldn't we override this method to add this logic instead of adding it here? Or if it needs to be here, add a comment?
Comment #34
amateescu commentedThanks for the review!
Re #33:
1. I really really have to learn at some point when to add
accessCheck(FALSE)to an entity query :)2. It needs to be there because we have to add the preloaded entity objects to the static cache, which is only done in the base entity storage class, and it helps custom storages so they don't have to override and rewrite some code that we already have in the base class. Rewrote that comment to document all this.
Comment #36
amateescu commentedI forgot to rename the hook implementation during the reroll :/
Comment #37
berdirDid some profiling as well on a site that is *not* actually using workspaces and despite 108 additional calls to invokeAll(), the time difference was for that case only 0.3%. It also reported almost 5% difference on the IO wait time, which this shouldn't influence in any way, so clearly the variance is much higher than the difference that is reported here. And that doesn't even include the considerable function call overhead of active profiling. So I think the performance impact is not an issue with the current patch.
The only thing I'd like to put up for discussion is whether we really need the entity type specific hook. Are there really enough use cases to make it worth doubling the additional invokeAll() calls? So far all CRUD hooks are always invoked for both variants, but we do have examples of entity hooks where we don't do that, e.g. hook_entity_view_mode_alter(). I don't have a strong opinion myself, it depends on what we consider to be more important I guess: consistency with other entity hooks vs. performance.
Setting to RTBC as that seems like a question for @catch or a framework manager.
Comment #39
amateescu commentedI think it's a very good idea to drop the entity type specific hook. IMO, performance is more important than consistency here because this hook has quite a narrow purpose that very few modules need.
Comment #40
catch+1 on dropping the entity-type specific hook here. I don't have any further objections, but also the tests haven't finished running yet so not committing right now.
Comment #42
catchCommitted 8293016 and pushed to 8.7.x. Thanks!
Comment #43
amateescu commentedYay, #2924218: Clearing the persistent entity cache every time we switch between workspaces is super wasteful is now unblocked. Both this issue and that one are a huge performance improvement for workspaces :)
Comment #44
jibranWe added a new hook so we should add a change notice for this.
Comment #45
amateescu commentedAdded a CR: https://www.drupal.org/node/3015367
Comment #46
hchonovWe just encountered a small problem while working on the patch for the revision cache in #2620980-122: Add static and persistent caching to ContentEntityStorageBase::loadRevision(). Probably someone could take a look and sign-off the change there or we do it in a follow-up of the current issue so that the other patch keeps the main focus on the revision cache?
The problem there is that we would require that the
preLoadoccurs before the retrieval from the static cache, otherwise under some circumstances the entity in the default revision might already be present in the entity cache and therefore thepreLoadhooks will not be executed and thus the entity will not be exchanged in "preLoad" anymore.