SecondLevelCache throws exception when there's a not cached relation in the entity #7323

Open
opened 2026-01-22 15:49:54 +01:00 by admin · 2 comments
Owner

Originally created by @KDederichs on GitHub (Feb 13, 2024).

Bug Report

Q A
BC Break no
Version 2.18.0

Summary

When you have a cached entity that references a non cached entity, you'll get an exception.

Current behavior

Trying to call any find method on the entity repository of the cached entity will result in:

In DefaultQueryCache.php line 340:
                                                                                                                                
  Attempted to call an undefined method named "getCacheRegion" of class "Doctrine\ORM\Persisters\Entity\BasicEntityPersister".  

How to reproduce

#[Entity(repositoryClass: FooRepository::class)]
#[Cache(usage: 'NONSTRICT_READ_WRITE', region: 'write_rare')]
class Foo {
    #[Id]
    #[Column(type: UuidType::NAME)]
    private Uuid $id;

    #[OneToOne(mappedBy: 'foo', targetEntity: Bar::class, fetch: 'EXTRA_LAZY')]
    private ?Bar $bar = null;
}

#[Entity(repositoryClass: BarRepository::class)]
class Bar {
    #[Id]
    #[Column(type: UuidType::NAME)]
    private Uuid $id;

    #[OneToOne(inversedBy: 'bar', targetEntity: Foo::class)]
    #[JoinColumn(nullable: false, onDelete: 'CASCADE')]
    private Foo $foo;
}

Now call for example findAll() on FooRepository

Expected behavior

No exception

Originally created by @KDederichs on GitHub (Feb 13, 2024). ### Bug Report <!-- Fill in the relevant information below to help triage your issue. --> | Q | A |------------ | ------ | BC Break | no | Version | 2.18.0 #### Summary When you have a cached entity that references a non cached entity, you'll get an exception. #### Current behavior Trying to call any `find` method on the entity repository of the cached entity will result in: ``` In DefaultQueryCache.php line 340: Attempted to call an undefined method named "getCacheRegion" of class "Doctrine\ORM\Persisters\Entity\BasicEntityPersister". ``` #### How to reproduce ```php #[Entity(repositoryClass: FooRepository::class)] #[Cache(usage: 'NONSTRICT_READ_WRITE', region: 'write_rare')] class Foo { #[Id] #[Column(type: UuidType::NAME)] private Uuid $id; #[OneToOne(mappedBy: 'foo', targetEntity: Bar::class, fetch: 'EXTRA_LAZY')] private ?Bar $bar = null; } #[Entity(repositoryClass: BarRepository::class)] class Bar { #[Id] #[Column(type: UuidType::NAME)] private Uuid $id; #[OneToOne(inversedBy: 'bar', targetEntity: Foo::class)] #[JoinColumn(nullable: false, onDelete: 'CASCADE')] private Foo $foo; } ``` Now call for example `findAll()` on `FooRepository` #### Expected behavior No exception
Author
Owner

@Housik commented on GitHub (Jan 23, 2025):

I have same issue, tried to debug code and result is -> all associated classes must implement #[ORM\Cache] too, otherwise exception is thrown. Not sure, if this is written in documentation.

In DefaultQueryCache.php is following code:

// root entity association
if ($rootAlias === $parentAlias) {
    // Cancel put result if association put fail
    $assocInfo = $this->storeAssociationCache($key, $assoc, $assocValue);
    if ($assocInfo === null) {
        return false;
    }

    $data[$index]['associations'][$name] = $assocInfo;

    continue;
}

Exception is thrown, because in following code method getCachedRegion() is not defined on $assocPersister. The reason is the association class does not implement #[ORM\Cache] attribute. In this case, $assocPersister is an instance of BasicEntityPersister class not having getCachedRegion() method:

line 303:

/**
 * @return mixed[]|null
 * @phpstan-return array{targetEntity: class-string, type: mixed, list?: array[], identifier?: array}|null
 */
private function storeAssociationCache(QueryCacheKey $key, AssociationMapping $assoc, mixed $assocValue): array|null
{
    $assocPersister = $this->uow->getEntityPersister($assoc->targetEntity);
    $assocMetadata  = $assocPersister->getClassMetadata();
    $assocRegion    = $assocPersister->getCacheRegion();

If I understand it correctly, storeAssociationCache() method should not be executed at all - or better, store association to cache should not be done, if association entity is not marked as #[ORM\Cache]

But as I said, temporary solution is to add #[ORM\Cache] to all associations of cached class (to the class definition, close to #[ORM\Entity])

@Housik commented on GitHub (Jan 23, 2025): I have same issue, tried to debug code and result is -> all associated classes must implement `#[ORM\Cache]` too, otherwise exception is thrown. Not sure, if this is written in documentation. In `DefaultQueryCache.php` is following code: ```php // root entity association if ($rootAlias === $parentAlias) { // Cancel put result if association put fail $assocInfo = $this->storeAssociationCache($key, $assoc, $assocValue); if ($assocInfo === null) { return false; } $data[$index]['associations'][$name] = $assocInfo; continue; } ``` Exception is thrown, because in following code method `getCachedRegion()` is not defined on $assocPersister. The reason is the association class does not implement `#[ORM\Cache]` attribute. In this case, $assocPersister is an instance of `BasicEntityPersister` class not having `getCachedRegion()` method: line 303: ```php /** * @return mixed[]|null * @phpstan-return array{targetEntity: class-string, type: mixed, list?: array[], identifier?: array}|null */ private function storeAssociationCache(QueryCacheKey $key, AssociationMapping $assoc, mixed $assocValue): array|null { $assocPersister = $this->uow->getEntityPersister($assoc->targetEntity); $assocMetadata = $assocPersister->getClassMetadata(); $assocRegion = $assocPersister->getCacheRegion(); ``` If I understand it correctly, `storeAssociationCache()` method should not be executed at all - or better, store association to cache should not be done, if association entity is not marked as `#[ORM\Cache]` **But as I said, temporary solution is to add `#[ORM\Cache]` to all associations of cached class (to the class definition, close to `#[ORM\Entity]`)**
Author
Owner

@svolikmartin commented on GitHub (Nov 11, 2025):

So I got myself into this same problem, but here are some more peculiarities
This problem only propagates when:

  • you have OneToOne (it does not affect OneToMany and ManyToOne, collections etc)
  • the relation is stored in other/child entity (unique column with FK in other table)
  • you load via findOneBy()

If you load via find(id) or queryBuilder and where/setParameter(), it's okay, even if I do leftJoin on some property which has Cache Attribute, it won't cache it until you get child entity straightforward, so it works fine and does not matter if you do/don't have Cache in children. The fact that queryBuilder does not use SLC is another problem resp. missing feature.

But if I load via findOneBy() - in my case using serverId (uuid), it automatically tries to load and cache all children OneToOne properties with mappedBy (meaning the parent ID is in other table with FK and unique index), even if they are marked as EXTRA_LAZY, and if child does not have Cache it will throw this error.

#[ORM\Cache(usage: 'NONSTRICT_READ_WRITE')]
class Supplier
{
    #[ORM\ManyToOne(targetEntity: Account::class, inversedBy: 'suppliers')]
    public private(set) ?Account $master; // this is always OK

    #[ORM\OneToOne(targetEntity: SupplierProfile::class, mappedBy: 'supplier', cascade: ['persist'], fetch: 'EXTRA_LAZY')]
    public ?SupplierProfile $profile; // only this will throw error if used with findBy() or findOneBy()

    #[ORM\OneToOne(targetEntity: Image::class, fetch: 'EXTRA_LAZY')]
    public ?Image $logo; // this is always OK, if I add #[ORM\Cache] it is cached when first loaded
}

I also found som irregularities when using find(id) method:

  • if I use id 11, it does return a correct entity but does not cache it
  • if I use id 55 id does return a correct entity and cache it
  • if I use findBy() with server_id na use uuid (which is row/entity with id 11) it suddenly does cache it and also loads and cache all children properties (OneToOne with mappedBy)

I know that this feature is marked as experimental and has some bugs, but I expected at least some consistency in what and when is cached 😆

Edit: I found out why id 11 was not cached - it was already in UnitOfWork loaded via Account->suppliers because I was authenticated as account which is master to this particular ID. If I used other account, suddenly ID 11 is, in fact, in Redis.
This is due to that Doctrine performs the following internally (simplified):

  • Check first-level cache (the EM’s UnitOfWork) – if the entity is already managed.
  • Check second-level cache (Redis) – if enabled and configured for that entity.
  • If not cached, query the DB and, if the entity is cacheable, put it into Redis.
@svolikmartin commented on GitHub (Nov 11, 2025): So I got myself into this same problem, but here are some more peculiarities This problem only propagates when: - you have OneToOne (it does not affect OneToMany and ManyToOne, collections etc) - the relation is stored in other/child entity (unique column with FK in other table) - you load via findOneBy() If you load via find(id) or queryBuilder and where/setParameter(), it's okay, even if I do leftJoin on some property which has Cache Attribute, it won't cache it until you get child entity straightforward, so it works fine and does not matter if you do/don't have Cache in children. The fact that queryBuilder does not use SLC is another problem resp. missing feature. But if I load via findOneBy() - in my case using serverId (uuid), it automatically tries to load and cache all children OneToOne properties with mappedBy (meaning the parent ID is in other table with FK and unique index), even if they are marked as EXTRA_LAZY, and if child does not have Cache it will throw this error. ``` #[ORM\Cache(usage: 'NONSTRICT_READ_WRITE')] class Supplier { #[ORM\ManyToOne(targetEntity: Account::class, inversedBy: 'suppliers')] public private(set) ?Account $master; // this is always OK #[ORM\OneToOne(targetEntity: SupplierProfile::class, mappedBy: 'supplier', cascade: ['persist'], fetch: 'EXTRA_LAZY')] public ?SupplierProfile $profile; // only this will throw error if used with findBy() or findOneBy() #[ORM\OneToOne(targetEntity: Image::class, fetch: 'EXTRA_LAZY')] public ?Image $logo; // this is always OK, if I add #[ORM\Cache] it is cached when first loaded } ``` I also found som irregularities when using find(id) method: - if I use id 11, it does return a correct entity but does **not** cache it - if I use id 55 id does return a correct entity and cache it - if I use findBy() with server_id na use uuid (which is row/entity with id 11) it suddenly does cache it and also loads and cache all children properties (OneToOne with mappedBy) I know that this feature is marked as experimental and has some bugs, but I expected at least some consistency in what and when is cached 😆 Edit: I found out why id 11 was not cached - it was already in UnitOfWork loaded via Account->suppliers because I was authenticated as account which is master to this particular ID. If I used other account, suddenly ID 11 is, in fact, in Redis. This is due to that Doctrine performs the following internally (simplified): - Check first-level cache (the EM’s UnitOfWork) – if the entity is already managed. - Check second-level cache (Redis) – if enabled and configured for that entity. - If not cached, query the DB and, if the entity is cacheable, put it into Redis.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: doctrine/archived-orm#7323