Problem/Motivation
#2997123: Cacheability of normalized computed fields' properties is not captured during serialization added the ability for computed field properties to contribute cacheable metadata to the normalization. However, entity reference fields are treated totally different by json:api module, since they must be converted to a "Relationship object" which references a UUID and resource type. As a result, there is no way for computed ER fields to express cache metadata.
Similarly, computed fields usually wish to declare their cacheable metadata on the field item list level, as that is the level where the computation takes place.
Steps to reproduce
Create a computed ER field on an entity and observe it cannot vary by cache tags, context or TTL.
Proposed resolution
Bubble the field item list cacheable metadata (if any) in ResourceObjectNormalizer.
Remaining tasks
Extend test coverage.
User interface changes
None.
API changes
None... well, we potentially change the visibility of the helper static method for getting the data reference property.
Data model changes
None.
Release notes snippet
TBD.
Original report
Hi,
I'm having an issue with a computed field (entity reference type) being displayed on JSON:API resources. The problem is that it's always cached, although it should change as I change for ex. categories or tags.
What I want is to have a computed/virtual field with similar articles for given article (depending on common categories and tags). For retrieving similar articles I use module called Similar By Terms => https://www.drupal.org/project/similarterms I want these similar articles to be outputted on JSON:API resources... and it works good, but it's cached all the time.
Here's my code:
function my_module_entity_bundle_field_info_alter(array &$fields, EntityTypeInterface $entityType, string $bundle): void {
if ($entityType->id() === 'node' && $bundle === 'article') {
$fields['field_similar_articles'] = BaseFieldDefinition::create('entity_reference')
->setName('field_similar_articles')
->setLabel('Similar articles')
->setTargetEntityTypeId('node')
->setTargetBundle('article')
->setComputed(TRUE)
->setRevisionable(FALSE)
->setTranslatable(FALSE)
->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED)
->setClass(SimilarArticlesComputedField::class);
}
}class SimilarArticlesComputedField extends EntityReferenceFieldItemList {
use ComputedItemListTrait;
protected function computeValue(): void {
$similarArticles = <array of articles' entities get from the view>
foreach ($similarArticles as $key => $similarArticle) {
$this->list[$key] = $this->createItem($key, $similarArticle);
}
}
}
As I said, JSON:API resource displays field_similar_articles field correctly, but when I change some article's category or tag then output of this field in JSON:API resource should change as well, but it's not.
Let's say there is Article 1, 2, 3, 4 and 5.
Article 2, 3 and 4 are similar articles to the Article 1 (because they share common tags/categories).
When I go to this resources /jsonapi/node/article/{article-1-uuid} then I can see Article 2, 3 and 4 in section field_similar_articles and also in included section.
So far, so good.
But then, let's say that I delete tags and categories for Article 2 and once again go to the upper mentioned resource... I still can see Article 2, 3 and 4 in section field_similar_articles... but Article 2 should not be there. What's interesting, in the included section Article 2 disappears, which is good.
Could you please tell me what I can do about it ... and will it be possible after applying this patch (I'm using Drupal ver 9.2.10) from this issue?
Issue fork drupal-3252278
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #3
bbralaIt might help to know what the cache tags are if you ask for a single article with that field. There are probably some tags missing regarding the related articles in the computed field which could mean cache it not refreshed when needed.
Comment #4
tivi22 commentedHi, thanks for your feedback. Do you mean that similar articles -
<array of articles' entities get from the view>- might miss some cache tags? Isn't it a problem in a caching of a computed field itself? At least, that's what I thought :) Escpecially that there was a whole discussion about it here. P.S. I didn't apply this patch yet.Comment #5
tivi22 commentedHi, do you have any ideas? As I said, in my JSON:API resource I've got two sections that I'm interested in:
data->relationships->field_similar_articlesandincluded. As I mentioned, in these sections I return similar articles to given articles (based on common categories and labels). I implemented half-solution which invalidates cache tags after each article save (add/update), delete + on each category/label delete. Cache tags that I invalidate are:taxonomy_term:{tid}... and it works good for similar articles inincludedsection... but data indata->relationships->field_similar_articlesis still cached... I've figured out that when I also invalidateconfig:jsonapi_resource_config_listcache tag then it works as expected... but I'm not sure if it's a good aproach (to invalidate this cache tag on each article save/delete + category/label delete actions).Any ideas?
Just a reminder,
field_similar_articlesis a computed field with references to entities (similar articles), and my code looks like this:Is it possible to add some extra cache tags to this class/method?
I've tried something like this, but it didn't work (invalidating
foo:barcache tag didn't solve the problem):Comment #6
tivi22 commentedComment #7
tivi22 commentedComment #8
tivi22 commentedComment #9
tivi22 commentedP.S. When I go to the
/jsonapi/node/article/{article-1-uuid}resource I can see the following cache tags:config:filter.format.basic_htmlconfig:jsonapi_extras.settingsconfig:jsonapi_resource_config_listconfig:user.role.anonymoushttp_responsenode:{nid}taxonomy_term:{tid}Comment #10
osopolar@tivi22 did you make any progress on that?
Comment #11
tivi22 commented@osopolar Not yet, unfortunatelly. For now, all I did was a workaround = clearing
config:jsonapi_resource_config_listcache tag on each article's add/update/delete. I know it's not the best solution, but it works :)Comment #14
bradjones1Comment #15
bradjones1Comment #17
bradjones1Need to do a test on
instanceof CacheableDependencyInterfacesimilar to howNormalizerBase::addCacheableDependency()does.Comment #18
bradjones1Added conditional... needs tests & maintainer review of concept.
Comment #19
bradjones1Comment #20
bradjones1Also ran into this question today - how to handle cacheable empty references? Updated PR incoming, which also touches attributes (that is, all other fields than ERs) and introduces an interface for field item lists to provide this data.
Comment #21
bbralaGreat to have cachability fixed, added a few comments. But this would be great :) I agree on the follow up to fix any fields that would benifit from thsi.
Comment #22
bradjones1Comment #23
bradjones1Comment #24
gaurav_manerkar commented@bradjones1
I have applied the changes from MR to my codebase but it doesnt work for me. Cache metadata is not visible in header.
Comment #25
bradjones1This does not do anything on its own, rather it enables a rather "expert mode" type of manipulation of cache metadata for field types/field properties which require it.
Comment #26
gaurav_manerkar commentedHow i can add cache metadata to my computed ER field ?
Am i missing anything here?
Comment #27
gaurav_manerkar commentedHi,
I have managed to get it working.
Comment #28
tivi22 commented@gaurav.manerkar Please, describe how did you handle this :)
Comment #29
gaurav_manerkar commented@tivi22
Refer https://tech.kinksters.dating/posts/2022-08-13-cacheability-computed-dru...
Comment #30
tivi22 commentedThanks @gaurav.manerkar
Comment #31
br0kenI did another trick to propagate cache metadata to the JSON:API normalization of a computed reference.
protected ?CacheableMetadata $cacheMetadata = NULL;to the computed field class.accessmethod by the computed field class:computeValue:Comment #33
bradjones1As noted in the comments in #31 that's a hack and while it works is not a definitive solution.
This now conflicts with #3100732: Allow specifying metadata on JSON:API objects but is relatively easy to resolve; they are both swimming in the same direction in that they contribute to the cacheability of the normalization.
Comment #34
andypostComment #37
jan.stoecklerAnother attempt with a slightly different approach which might not need an additional interface for empty field item lists, let's see how it goes.
Would appreciate some review, but leaving at 'needs work' due to missing test coverage.
Comment #38
bradjones1@jan.stockler - Thanks for the MR, however I'm curious how you handle the empty case? It seems to require special handling, unless I'm missing something obvious here.
Comment #43
jan.stoeckler@bradjones1
Can you elaborate on why empty field item lists need special handling, i seem to be missing or not fully grasping something on that front?
Comment #44
bradjones1Rebased my MR, no regressions there, this still needs specific test coverage.
Comment #46
bradjones1Empty lists are empty for a reason. However, if we can't attach cacheability data to that emptiness, then when the empty condition changes (e.g., due to something that does not otherwise cache-invalidate the entity) then you'll never see the new, populated data.
Comment #49
duivw commentedOne thing the previous MR's fail to account for are computed fields with differing cache contexts. This is currently not used in core, but is supported, see
Drupal\entity_test\Plugin\Field\ComputedTestCacheableStringItemList. Perhaps my addition can be rebased on the already existing cache bubbling for entity reference fields.Comment #50
bradjones1Thanks for taking a look and catching that. Feel free to give a shot at merging the MRs, as it were, or I'll try to loop back around on this "soon."
Comment #52
tstoecklerComing back to this after a while and trying to condense the different approaches here. I think the confusion between @bradjones1 and @duivw on the one hand and @jan.stoeckler and myself on the other is that as far as I can tell @bradjones1 and @duivw, you are tackling the issue on the field-item level. @jan.stoeckler and I are tackling the issue on the field-item-list level. On the field-item-list level it is unnecessary to differentiate explicitly between empty and non-empty lists. If a field item list specifies its cacheability it should take care to do so even if it is empty. Thus, I think that is the preferable approach versus introducing a new special-cased interface just for this. Also, I think the list level is the more intuitive one when talking about computed fields, because to get a computed field you always have to create an item list class which uses
ComputedItemListTrait. It is valid, however, to allow bubbling cacheable metadata on the item level, as well. I would have expected that code to be introduced intoFieldItemNormalizer, however, and not intoFieldNormalizeras in your branch, @bradjones1, or inResourceObjectNormalizer, as in your branch, @duivw. Since both of your branches are lacking test coverage as of yet, I will add some test coverage for item-level cacheable metadata to your branch, @jan.stoeckler, and by that verify if adding some handling toFieldItemNormalizeris in fact possible. Hope that we can drive this issue forward and would love to hear some thoughts. Please let me know if I missed anything or misrepresented the status quo.Edit: Seeing now that all branches do in fact update
ResourceObjectNormalizerso that point is moot or at least confusing. Had missed that before, sorry.Comment #53
tstoecklerLooking at the issue metadata and stumbled over #21. Looked into any existing computed field item lists and found path alias and content moderation state. So I opened #3570814: Attach route_match cache tag to PathFieldItemList instances and #3570817: Attach cacheable metadata to ModerationStateFieldItemList instances for that, so removing the tag.
Comment #54
tstoecklerComment #55
tstoecklerIn rebasing the branch from @jan.stoeckler I found #3304772: Cache tags from Computed fields do not bubble up to Entity render array (because there was a conflict with the commit from that issue). Over there we added test coverage for the render API to properly bubble cache metadata from the item list metadata. That test coverage is something we can re-use now that that's landed to verify the same thing works in JSON:API.
In my opinion that suggests that fixing support for cache metadata on the item level should be a separate issue where we add test for both the render API and JSON:API.
I hope that makes sense and that you agree with that conclusion. I'm really not trying to railroad this issue, but I do think that that is a relevant precedent. In any case would love to hear some thoughts on this.
Comment #56
tstoecklerOK, rebased this issue now and with #3304772: Cache tags from Computed fields do not bubble up to Entity render array providing the basic setup for the test infrastructure this is now a lot smaller and hopefully easy to review.
Comment #57
tstoecklerComment #58
tstoecklerAdded a draft change notice now.
Comment #59
tstoecklerUpdated the issue summary.
Comment #60
bbralaYay, this is awesome. I didn't spot issues, only thing i see that might not be adressed is https://www.drupal.org/project/drupal/issues/3252278#comment-16204241
Which mentioned cache contexts. Can we verify that this is not an issue?
Comment #61
tstoecklerCool, thanks for taking a look. When working on this I was also wondering why the additional cache context doesn't require any changes in the test code and was afraid something was broken. It's not actually and everything works fine, it's just that the field item uses
url.query_args:computed_test_cacheable_integer_fieldas a cache context and JSON:API responses already addurl.query_argsby default (see\Drupal\Tests\jsonapi\Functional\ResourceTestBase::getExpectedCacheContexts()) so that gets optimized away.I can look into updating that to use a different cache context, though, so that the test coverage becomes more explicit. Marking needs work for that.
Comment #62
tstoecklerAlrighty, used a different cache context now and that properly requires adding that to the test expectations.
Comment #63
bbralaAwesome, thanks! As per #60, setting RTBC.
Comment #68
godotislateI had a concern in
ResourceTestBasethat$cacheability->addCacheableDependency(CacheableMetadata::createFromObject($field_item_list));, when $field_item_list does not implement CacheableDependencyInterface, meant that max-age 0 bubbles. But that's not part of the response, so it seems fine, and a similar thing is already done with the field properties.Committed 16b3297 and pushed to main and committed 4ba0eff and pushed to 11.x. Thanks!
Comment #70
wim leers🤩
Awesome work!