Using Doctrine’s L2 Cache in Symfony

.com software on 2022-10-08

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

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:

doctrine-l2-enable.yml

# config/packages/doctrine.yaml

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

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

doctrine-l2-regions.yml

# 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:

doctrine-l2-city.php

<?php

use Doctrine\ORM\Mapping as ORM;

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

doctrine-l2-user.php

<?php

use Doctrine\ORM\Mapping as ORM;

/**
 * @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

doctrine-l2-example.php

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

doctrine-l2-example-evict.php

<?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:

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.

Reference