Skip to content

Caching ​

Within a request, the shared identity map already serves find() without a query. Between requests, two caches keep data of the API, as with Doctrine:

  • the result cache keeps the response of a query that asks for it;
  • the second-level cache keeps the entities marked #[GraphqlOrm\Cache], read back by find(), the references and the lazy loads.

Both store in Symfony cache pools: any adapter works, Redis, APCu or the filesystem.

Choosing the pools ​

yaml
# config/packages/cache.yaml
framework:
    cache:
        pools:
            cache.graphql:
                adapter: cache.adapter.redis
                provider: '%env(REDIS_URL)%'

# config/packages/graphql_orm.yaml
graphql_orm:
    result_cache:
        pool: cache.graphql         # default: cache.app; null disables the result cache
    second_level_cache:
        enabled: true               # default: false, #[GraphqlOrm\Cache] is then ignored
        pool: cache.app             # the entities without region
        default_lifetime: 3600      # seconds
        regions:
            reference_data:
                pool: cache.graphql # default: second_level_cache.pool
                lifetime: 86400     # default: second_level_cache.default_lifetime

A region names a pool and a lifetime for some entities: the entities choose a region, the configuration of each environment chooses where it lives.

Result cache ​

php
$qb = $repository->createQueryBuilder();
$tasks = $qb
    ->where($qb->expr()->eq('done', false))
    ->getQuery()
    ->enableResultCache(lifetime: 300, key: 'tasks.open')
    ->getResult();

The response is kept as received, unless it holds errors, and hydrated again when read, through the identity map of the request. The key defaults to a hash of the query and its variables; give one to evict the result, or to share it between queries. lifetime defaults to the default lifetime of the pool. Each page of a paginated query is kept apart, so toIterable() goes through every page.

disableResultCache() sends the query anyway. With result_cache.pool: null, a query calling enableResultCache() throws a LogicException. A query served from the result cache does not keep its entities in the second-level cache: they are as old as the result.

Second-level cache ​

php
use GraphqlOrm\Mapping as GraphqlOrm;

#[GraphqlOrm\Entity(name: 'labels')]
#[GraphqlOrm\Cache(region: 'reference_data')]
class Label
{
    // ...
}

The second-level cache is disabled by default, as in Doctrine: #[GraphqlOrm\Cache] is ignored until graphql_orm.second_level_cache.enabled is true.

An entity marked #[GraphqlOrm\Cache] is kept when a query hydrates it in full (every mapped field selected): a partial entity is not. It is then read from the cache by:

  • find(), after the identity map of the request;
  • the references (getReference(), #[GraphqlReference] of Doctrine entities) and the lazy loads (GraphqlOrm\Fetch::Lazy, lazy_loading): only the identifiers not kept are queried.

Its relations are kept by their identifier, and loaded on first access, as lazy ghosts, whatever lazy_loading says: an entity read from the cache may need a query more than one read from the API. A collection is kept as the identifiers of its entities.

#[GraphqlOrm\Cache(lifetime: 600)] overrides the lifetime of the region. Without region, the entity is kept in second_level_cache.pool for default_lifetime; a region the configuration does not declare throws a LogicException. Reading an entity from the cache does not prolong it.

The queries of findBy(), findAll() or the query builder are still sent: cache their result with the result cache.

Warming up ​

graphqlorm:cache:warmup loads the entities marked #[GraphqlOrm\Cache] from the API, so that the first requests after a deployment or a clear read them from the cache:

bash
php bin/console graphqlorm:cache:warmup                  # every entity marked #[GraphqlOrm\Cache], of every manager
php bin/console graphqlorm:cache:warmup Label            # one entity (short name or FQCN)
php bin/console graphqlorm:cache:warmup --manager=crm --batch-size=500
 -------- --------- ---------------- ---------- ------ --------
  Entity   Manager   Region           Hydrated   Kept   Time
 -------- --------- ---------------- ---------- ------ --------
  Label    main      reference_data   4          4      121 ms
 -------- --------- ---------------- ---------- ------ --------

 [OK] 4 entities kept in the second-level cache.

The entities are loaded page by page, --batch-size (100 by default) a query, with the memory of a single page; with a dialect that cannot paginate, they are loaded by one query, which the command tells. "Kept" counts those now in the cache: an entity hydrated in part, or whose relation has no identifier, is not kept. Naming an entity not marked #[GraphqlOrm\Cache], or of a manager whose second-level cache is disabled, fails the command; without an entity named, the command only warns when no entity is kept in the second-level cache.

Eviction ​

The bundle does not know when the API changes: the values expire after their lifetime, unless they are evicted. Inject GraphqlCache, or call $manager->getCache():

php
use GraphqlOrm\Cache\GraphqlCache;

public function __construct(private GraphqlCache $cache) {}

$this->cache->evictEntity(Label::class, 'bug');     // one entity
$this->cache->evictEntityRegion(Label::class);      // every entity of the class
$this->cache->evictQueryResult('tasks.open');       // a result kept under this key (its first page)
$this->cache->evictQueryResults();                  // every result of the manager
$this->cache->containsEntity(Label::class, 'bug');  // false

evictEntityRegion() and evictQueryResults() work with any pool, even shared with the application: the entities of the class, or the results of the manager, get a new key, and the old ones expire. Another process sees it from its next request. A result kept by the result cache is not evicted with its entities.

From the console ​

bash
php bin/console graphqlorm:cache:clear                      # the results and the entities, of every manager
php bin/console graphqlorm:cache:clear --results            # every result
php bin/console graphqlorm:cache:clear --key=tasks.open     # the result kept under this key
php bin/console graphqlorm:cache:clear --entities           # the entities of every class marked #[GraphqlOrm\Cache]
php bin/console graphqlorm:cache:clear --class=Label        # the entities of a class
php bin/console graphqlorm:cache:clear --manager=crm        # the caches of one manager

graphqlorm:cache:stats describes the caches of each manager, without reading their values: the pool, adapter and lifetime of the result cache and of each region, the entities marked #[GraphqlOrm\Cache] with their region and lifetime, and how many times the results and the entities of each class were evicted.

Manager "main"
--------------

 ---------------- --------------- ------------ -------------- -----------
  Cache            Pool            Adapter      Lifetime       Evictions
 ---------------- --------------- ------------ -------------- -----------
  result cache     cache.graphql   Redis        pool default   1
  default region   cache.app       Filesystem   3600 s
  reference_data   cache.graphql   Redis        86400 s
 ---------------- --------------- ------------ -------------- -----------

 -------- ---------------- ---------- -----------
  Entity   Region           Lifetime   Evictions
 -------- ---------------- ---------- -----------
  Label    reference_data   86400 s    2
 -------- ---------------- ---------- -----------

Profiler ​

The toolbar gives the hits of each cache and the adapter of its pools, e.g. Result cache 3/4 hits (Redis), and the queries sent to the API. The "Cache" tab of the profiler tells where each cache lives (pool, adapter, lifetime), what it cost (values read and written, time, size), which entities it served and why, the queries it added (relations of entities read from it, loaded on first access), and the evictions with the line calling them. It warns when #[GraphqlOrm\Cache] is ignored, or when a pool is an ArrayAdapter.

Released under the MIT License.