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

Command icon 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

tivi22 created an issue. See original summary.

Version: 9.2.x-dev » 9.3.x-dev

Drupal 9.1.10 (June 4, 2021) and Drupal 9.2.10 (November 24, 2021) were the last bugfix releases of those minor version series. Drupal 9 bug reports should be targeted for the 9.3.x-dev branch from now on, and new development or disruptive changes should be targeted for the 9.4.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

bbrala’s picture

It 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.

tivi22’s picture

Hi, 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.

tivi22’s picture

Hi, 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_articles and included. 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 in included section... but data in data->relationships->field_similar_articles is still cached... I've figured out that when I also invalidate config:jsonapi_resource_config_list cache 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_articles is a computed field with references to entities (similar articles), and my code looks like this:

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);
    }
  }

}

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:bar cache tag didn't solve the problem):

class SimilarArticlesComputedField extends EntityReferenceFieldItemList {

  use ComputedItemListTrait;

  protected function computeValue(): void {
    $similarArticles = <array of articles' entities get from the view>

    foreach ($similarArticles as $key => $similarArticle) {
      $similarArticle->addCacheTags(['foo:bar')]);
      $this->list[$key] = $this->createItem($key, $similarArticle);
    }
  }

}
tivi22’s picture

Issue summary: View changes
tivi22’s picture

Issue summary: View changes
tivi22’s picture

Issue summary: View changes
tivi22’s picture

P.S. When I go to the /jsonapi/node/article/{article-1-uuid} resource I can see the following cache tags:

  • config:filter.format.basic_html
  • config:jsonapi_extras.settings
  • config:jsonapi_resource_config_list
  • config:user.role.anonymous
  • http_response
  • node:{nid}
  • taxonomy_term:{tid}
osopolar’s picture

@tivi22 did you make any progress on that?

tivi22’s picture

@osopolar Not yet, unfortunatelly. For now, all I did was a workaround = clearing config:jsonapi_resource_config_list cache tag on each article's add/update/delete. I know it's not the best solution, but it works :)

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.15 was released on June 1st, 2022 and is the final full bugfix release for the Drupal 9.3.x series. Drupal 9.3.x will not receive any further development aside from security fixes. Drupal 9 bug reports should be targeted for the 9.4.x-dev branch from now on, and new development or disruptive changes should be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

bradjones1 made their first commit to this issue’s fork.

bradjones1’s picture

Title: JSON:API + computed field cache problem » Computed entity reference fields cannot express cacheable metadata
Status: Active » Needs review

bradjones1’s picture

Status: Needs review » Needs work

Need to do a test on instanceof CacheableDependencyInterface similar to how NormalizerBase::addCacheableDependency() does.

bradjones1’s picture

Issue tags: +Needs tests

Added conditional... needs tests & maintainer review of concept.

bradjones1’s picture

bradjones1’s picture

Title: Computed entity reference fields cannot express cacheable metadata » Complete JSON:API's handling of field (including computed fields) cache metadata
Issue summary: View changes

Also 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.

bbrala’s picture

Great 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.

bradjones1’s picture

Title: Complete JSON:API's handling of field (including computed fields) cache metadata » Complete JSON:API's handling of field (including computed fields) cache metadata, including empty fields
bradjones1’s picture

Title: Complete JSON:API's handling of field (including computed fields) cache metadata, including empty fields » Complete JSON:API's handling of field (including computed & empty fields) cache metadata
gaurav_manerkar’s picture

@bradjones1
I have applied the changes from MR to my codebase but it doesnt work for me. Cache metadata is not visible in header.

bradjones1’s picture

Cache metadata is not visible in header.

This 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.

gaurav_manerkar’s picture

How i can add cache metadata to my computed ER field ?

foreach ($data as $key => $nid) {
    $item = $this->createItem($key, $nid);
    item->get('target_id')->addCacheTags(["node_list:foo_bar"]);
    $this->list[$key] = $item;
}

Am i missing anything here?

gaurav_manerkar’s picture

Hi,

I have managed to get it working.

tivi22’s picture

@gaurav.manerkar Please, describe how did you handle this :)

tivi22’s picture

Thanks @gaurav.manerkar

br0ken’s picture

I did another trick to propagate cache metadata to the JSON:API normalization of a computed reference.

  1. Add protected ?CacheableMetadata $cacheMetadata = NULL; to the computed field class.
  2. Implement the access method by the computed field class:
        public function access($operation = 'view', AccountInterface $account = NULL, $return_as_object = FALSE): AccessResultInterface|bool {
          $access = parent::access($operation, $account, TRUE);
        
          if ($return_as_object) {
            // Here you witness a pure hack. The thing is that JSON:API
            // normalization does not compute cacheable metadata for
            // computed relations like this one.
            /** @see \Drupal\jsonapi\JsonApiResource\ResourceIdentifier */
            /** @see \Drupal\jsonapi\Normalizer\ResourceIdentifierNormalizer */
            // However, thanks to the access check, its result is added
            // as a cacheable dependency to the normalization.
            /** @see \Drupal\jsonapi\Normalizer\ResourceObjectNormalizer::serializeField() */
            $this->ensureComputedValue();
            \assert($this->cacheMetadata instanceof CacheableMetadata);
            $access->addCacheableDependency($this->cacheMetadata);
        
            return $access;
          }
        
          return $access->isAllowed();
        }
        
  3. Collect cache metadata within the computeValue:
        protected function computeValue(): void {
          $entity = $this->getEntity();
          $cache_tags = [
            // The list tag is needed to ensure the JSON:API normalization cache
            // will be rebuilt on adding a new or deleting an existing node.
            /** @see \Drupal\Core\Entity\EntityBase::getListCacheTagsToInvalidate() */
            'node_list:article',
          ];
          $delta = 0;
        
          foreach ([20, 24, 13] as $id) {
            $this->list[] = $this->createItem($delta++, $id);
            $cache_tags[] = 'node:' . $id;
          }
        
          $this->cacheMetadata = new CacheableMetadata();
          $this->cacheMetadata->setCacheTags($cache_tags);
        }
        

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.9 was released on December 7, 2022 and is the final full bugfix release for the Drupal 9.4.x series. Drupal 9.4.x will not receive any further development aside from security fixes. Drupal 9 bug reports should be targeted for the 9.5.x-dev branch from now on, and new development or disruptive changes should be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

bradjones1’s picture

As 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.

andypost’s picture

Version: 9.5.x-dev » 11.x-dev

jan.stoeckler made their first commit to this issue’s fork.

jan.stoeckler’s picture

Another 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.

bradjones1’s picture

Category: Support request » Task

@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.

bradjones1 changed the visibility of the branch 3252278-jsonapi--computed to hidden.

bradjones1 changed the visibility of the branch 3252278-jsonapi-computed-field-item-list-cache-metadata to hidden.

bradjones1 changed the visibility of the branch 11.x to hidden.

jan.stoeckler’s picture

@bradjones1

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.

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?

bradjones1’s picture

Rebased my MR, no regressions there, this still needs specific test coverage.

bradjones1 changed the visibility of the branch 3252278-jsonapi-computed-field-item-list-cache-metadata to active.

bradjones1’s picture

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?

Empty 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.

duivw made their first commit to this issue’s fork.

duivw’s picture

One 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.

bradjones1’s picture

Thanks 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."

Version: 11.x-dev » main

Drupal core is now using the main branch as the primary development branch. New developments and disruptive changes should now be targeted to the main branch.

Read more in the announcement.

tstoeckler’s picture

Coming 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 into FieldItemNormalizer, however, and not into FieldNormalizer as in your branch, @bradjones1, or in ResourceObjectNormalizer, 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 to FieldItemNormalizer is 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 ResourceObjectNormalizer so that point is moot or at least confusing. Had missed that before, sorry.

tstoeckler’s picture

Issue tags: -Needs followup

Looking 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.

tstoeckler’s picture

tstoeckler’s picture

In 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.

tstoeckler’s picture

Status: Needs work » Needs review
Issue tags: -Needs tests

OK, 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.

tstoeckler’s picture

tstoeckler’s picture

Issue tags: -Needs change record

Added a draft change notice now.

tstoeckler’s picture

Issue summary: View changes

Updated the issue summary.

bbrala’s picture

Yay, 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?

tstoeckler’s picture

Status: Needs review » Needs work

Cool, 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_field as a cache context and JSON:API responses already add url.query_args by 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.

tstoeckler’s picture

Status: Needs work » Needs review

Alrighty, used a different cache context now and that properly requires adding that to the test expectations.

bbrala’s picture

Status: Needs review » Reviewed & tested by the community

Awesome, thanks! As per #60, setting RTBC.

  • godotislate committed 16b32971 on main
    fix: #3252278 Complete JSON:API's handling of field (including computed...

  • godotislate committed 4ba0eff8 on 11.x
    fix: #3252278 Complete JSON:API's handling of field (including computed...
godotislate’s picture

Version: main » 11.x-dev
Status: Reviewed & tested by the community » Fixed

I had a concern in ResourceTestBase that $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!

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

wim leers’s picture

🤩

Awesome work!

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.