Using Doctrine’s L2 Cache in Symfony

Avoiding database calls with Doctrine’s Level-2 caching. Push your application to the next level.

.com software
3 min readNov 14, 2022
Image by Pixabay

There is an amazing feature in Doctrine ORM called the “Second level cache.” According to Doctrine’s documentation:

The Second Level Cache is designed to reduce the amount of necessary database access. It sits between your application and the database to avoid the number of database hits as much as possible.

With the feature turned on:

(…) entities will be first searched in cache and if they are not found, a database query will be fired and then the entity result will be stored in a cache provider.

With 2nd level cache enabled, you can avoid database calls and vastly speed up your application.

I’m already using this functionality in a few applications and didn’t encounter any issues. On the contrary, it’s working great, especially for rarely written entities like a “City” entity.

To enable the cache you have to:

1. turn it on:

# config/packages/doctrine.yaml

doctrine:
# …
orm:
# …
second_level_cache:
enabled: true

2. define and configure named cache region(s):

# config/packages/doctrine.yaml

doctrine:
# …
orm:
# …
second_level_cache:
enabled: true
regions:
write_rare:
# expire automatically after 10 days
lifetime: 864000
# let's use app's main cache pool
# (in my case it's using Redis)
cache_driver: { type: service, id: cache.app }

append_only:
# expire automatically after 100 days
lifetime: 8640000
# let's use app's main cache pool
# (in my case it's using Redis)
cache_driver: { type: service, id: cache.app }

3. decide how to cache the data and assign the region to a relationship or entire entity:

/**
* @ORM\Entity
* @ORM\Cache(usage="READ_ONLY", region="append_only")
*/
class City
{
}
/**
* @ORM\Entity
* @ORM\Cache(usage="READ_ONLY", region="append_only")
*/
class User
{
// …

/**
* @ORM\OneToMany(targetEntity=Post::class, mappedBy="author")
* @ORM\Cache(usage="NONSTRICT_READ_WRITE", region="write_rare")
*/
private Collection $posts;
}

Example usage

<?php

$user = new User();
$user->setName('John Doe');

// put user to cache
$entityManager->persist($user);
$entityManager->flush();
$entityManager->clear();

unset($user);

// load user from the cache
// avoid database call
$user = $entityManager->find(User::class, 1);

// create a new post
$post = new Post();
$post->setTitle('Awesome title');

$user->addPost($post);

$entityManager->persist($post);
$entityManager->flush();
$entityManager->clear();

// load user posts from the cache
// avoid database call

foreach ($user->getPosts() as $post) {
echo $post->getTitle();
}

Manual cache eviction

<?php

$cache = $entityManager->getCache();

// prune entity cache
$cache->evictEntity(User::class, $user->getId());

// prune entity's relationship cache
$cache->evictCollection(User::class, 'posts', $user->getId());

// prune entire region of a relationship
$cache->evictCollectionRegion(User::class, 'posts');

Caching modes

Caching modes were described pretty well in the Doctrine’s documentation. There are three, in short:

  • READ_ONLY — use it for append-only data that do not change (i.e. addressing data, event sourcing events,) or you are planning to evict cache by yourself,
  • NONSTRICT_READ_WRITE — use it when you need to update the data, but you do not plan to use concurrency locks,
  • READ_WRITE — the slowest one, employs locks before update/delete operations. The region’s service implementation must support locking. (there is only filelock region type available at the moment of writing.)

Doctrine’s 2nd level cache is a relatively cheap method of increasing performance. With just a few lines of configuration, you can reduce your database load and speed up your Symfony application significantly.

--

--

.com software

Father • PHP developer • entrepreneur • working for a €1bn unicorn startup as a backend engineer >>> https://bit.ly/dotcom-software